From a79ca4783e1f9f80b63c625dc06a3379826913d5 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 17 Jan 2023 23:12:07 -0500 Subject: [PATCH 001/185] Update AvaloniaDictionary API --- .../Collections/AvaloniaDictionary.cs | 18 ++- .../AvaloniaDictionaryExtensions.cs | 112 ++++++++++++++++++ .../Collections/IAvaloniaDictionary.cs | 13 ++ .../IAvaloniaReadOnlyDictionary.cs | 14 +++ 4 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs create mode 100644 src/Avalonia.Base/Collections/IAvaloniaDictionary.cs create mode 100644 src/Avalonia.Base/Collections/IAvaloniaReadOnlyDictionary.cs diff --git a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs index 35a391f2cb..d4c7137fdc 100644 --- a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs +++ b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs @@ -14,11 +14,7 @@ namespace Avalonia.Collections /// /// The type of the dictionary key. /// The type of the dictionary value. - public class AvaloniaDictionary : IDictionary, - IDictionary, - INotifyCollectionChanged, - INotifyPropertyChanged - where TKey : notnull + public class AvaloniaDictionary : IAvaloniaDictionary where TKey : notnull { private Dictionary _inner; @@ -29,6 +25,14 @@ namespace Avalonia.Collections { _inner = new Dictionary(); } + + /// + /// Initializes a new instance of the class. + /// + public AvaloniaDictionary(int capacity) + { + _inner = new Dictionary(capacity); + } /// /// Occurs when the collection changes. @@ -62,6 +66,10 @@ namespace Avalonia.Collections object ICollection.SyncRoot => ((IDictionary)_inner).SyncRoot; + IEnumerable IReadOnlyDictionary.Keys => _inner.Keys; + + IEnumerable IReadOnlyDictionary.Values => _inner.Values; + /// /// Gets or sets the named resource. /// diff --git a/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs b/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs new file mode 100644 index 0000000000..e350a019d4 --- /dev/null +++ b/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using Avalonia.Reactive; + +namespace Avalonia.Collections +{ + /// + /// Defines extension methods for working with s. + /// + public static class AvaloniaDictionaryExtensions + { + /// + /// Invokes an action for each item in a collection and subsequently each item added or + /// removed from the collection. + /// + /// The key type of the collection items. + /// The value type of the collection items. + /// The collection. + /// + /// An action called initially for each item in the collection and subsequently for each + /// item added to the collection. The parameters passed are the index in the collection and + /// the item. + /// + /// + /// An action called for each item removed from the collection. The parameters passed are + /// the index in the collection and the item. + /// + /// + /// An action called when the collection is reset. This will be followed by calls to + /// for each item present in the collection after the reset. + /// + /// + /// Indicates if a weak subscription should be used to track changes to the collection. + /// + /// A disposable used to terminate the subscription. + internal static IDisposable ForEachItem( + this IAvaloniaReadOnlyDictionary collection, + Action added, + Action removed, + Action reset, + bool weakSubscription = false) + where TKey : notnull + { + void Add(IEnumerable items) + { + foreach (KeyValuePair pair in items) + { + added(pair.Key, pair.Value); + } + } + + void Remove(IEnumerable items) + { + foreach (KeyValuePair pair in items) + { + removed(pair.Key, pair.Value); + } + } + + NotifyCollectionChangedEventHandler handler = (_, e) => + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + Add(e.NewItems!); + break; + + case NotifyCollectionChangedAction.Move: + case NotifyCollectionChangedAction.Replace: + Remove(e.OldItems!); + int newIndex = e.NewStartingIndex; + if(newIndex > e.OldStartingIndex) + { + newIndex -= e.OldItems!.Count; + } + Add(e.NewItems!); + break; + + case NotifyCollectionChangedAction.Remove: + Remove(e.OldItems!); + break; + + case NotifyCollectionChangedAction.Reset: + if (reset == null) + { + throw new InvalidOperationException( + "Reset called on collection without reset handler."); + } + + reset(); + Add(collection); + break; + } + }; + + Add(collection); + + if (weakSubscription) + { + return collection.WeakSubscribe(handler); + } + else + { + collection.CollectionChanged += handler; + + return Disposable.Create(() => collection.CollectionChanged -= handler); + } + } + } +} diff --git a/src/Avalonia.Base/Collections/IAvaloniaDictionary.cs b/src/Avalonia.Base/Collections/IAvaloniaDictionary.cs new file mode 100644 index 0000000000..b79cfe2b9c --- /dev/null +++ b/src/Avalonia.Base/Collections/IAvaloniaDictionary.cs @@ -0,0 +1,13 @@ +using System.Collections; +using System.Collections.Generic; + +namespace Avalonia.Collections +{ + public interface IAvaloniaDictionary + : IDictionary, + IAvaloniaReadOnlyDictionary, + IDictionary + where TKey : notnull + { + } +} diff --git a/src/Avalonia.Base/Collections/IAvaloniaReadOnlyDictionary.cs b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyDictionary.cs new file mode 100644 index 0000000000..d772de2f59 --- /dev/null +++ b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyDictionary.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; + +namespace Avalonia.Collections +{ + public interface IAvaloniaReadOnlyDictionary + : IReadOnlyDictionary, + INotifyCollectionChanged, + INotifyPropertyChanged + where TKey : notnull + { + } +} From 253ecd028d58112fc04eb79317b0cc053807cb97 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 18 Jan 2023 02:05:34 -0500 Subject: [PATCH 002/185] Introduce ThemeVariant API --- .../Controls/IResourceDictionary.cs | 6 + src/Avalonia.Base/Controls/IResourceNode.cs | 7 +- .../Controls/ResourceDictionary.cs | 91 ++++++++++++- .../Controls/ResourceNodeExtensions.cs | 125 +++++++++++++++--- src/Avalonia.Base/StyledElement.cs | 41 +++++- .../Styling/IGlobalThemeVariantProvider.cs | 22 +++ src/Avalonia.Base/Styling/StyleBase.cs | 6 +- src/Avalonia.Base/Styling/Styles.cs | 6 +- src/Avalonia.Base/Styling/ThemeVariant.cs | 65 +++++++++ .../Styling/ThemeVariantTypeConverter.cs | 23 ++++ src/Avalonia.Controls/Application.cs | 62 ++++++++- src/Avalonia.Controls/ThemeVariantScope.cs | 23 ++++ src/Avalonia.Controls/TopLevel.cs | 52 ++++++-- .../Diagnostics/Controls/Application.cs | 46 ++++++- .../Diagnostics/DevToolsOptions.cs | 8 +- .../ViewModels/ControlDetailsViewModel.cs | 3 +- .../Diagnostics/Views/MainWindow.xaml | 4 +- .../Diagnostics/Views/MainWindow.xaml.cs | 8 +- .../Internal/ResourceSelectorConverter.cs | 2 +- .../StaticResourceExtension.cs | 21 ++- .../Styling/ResourceInclude.cs | 5 +- .../Styling/StyleInclude.cs | 4 +- 22 files changed, 552 insertions(+), 78 deletions(-) create mode 100644 src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs create mode 100644 src/Avalonia.Base/Styling/ThemeVariant.cs create mode 100644 src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs create mode 100644 src/Avalonia.Controls/ThemeVariantScope.cs diff --git a/src/Avalonia.Base/Controls/IResourceDictionary.cs b/src/Avalonia.Base/Controls/IResourceDictionary.cs index 3a68dde31e..2bd1f65638 100644 --- a/src/Avalonia.Base/Controls/IResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/IResourceDictionary.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Avalonia.Styling; #nullable enable @@ -13,5 +14,10 @@ namespace Avalonia.Controls /// Gets a collection of child resource dictionaries. /// IList MergedDictionaries { get; } + + /// + /// Gets a collection of merged resource dictionaries that are specifically keyed and composed to address theme scenarios. + /// + IDictionary ThemeDictionaries { get; } } } diff --git a/src/Avalonia.Base/Controls/IResourceNode.cs b/src/Avalonia.Base/Controls/IResourceNode.cs index d6c900f97f..d2fa3c7af3 100644 --- a/src/Avalonia.Base/Controls/IResourceNode.cs +++ b/src/Avalonia.Base/Controls/IResourceNode.cs @@ -1,5 +1,5 @@ -using System; -using Avalonia.Metadata; +using Avalonia.Metadata; +using Avalonia.Styling; namespace Avalonia.Controls { @@ -23,6 +23,7 @@ namespace Avalonia.Controls /// Tries to find a resource within the object. /// /// The resource key. + /// Theme used to select theme dictionary. /// /// When this method returns, contains the value associated with the specified key, /// if the key is found; otherwise, null. @@ -30,6 +31,6 @@ namespace Avalonia.Controls /// /// True if the resource if found, otherwise false. /// - bool TryGetResource(object key, out object? value); + bool TryGetResource(object key, ThemeVariant? theme, out object? value); } } diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index d6197c50c6..85e4487ba9 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -1,9 +1,12 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Templates; +using Avalonia.Media; +using Avalonia.Styling; namespace Avalonia.Controls { @@ -15,6 +18,7 @@ namespace Avalonia.Controls private Dictionary? _inner; private IResourceHost? _owner; private AvaloniaList? _mergedDictionaries; + private AvaloniaDictionary? _themeDictionary; /// /// Initializes a new instance of the class. @@ -69,14 +73,14 @@ namespace Avalonia.Controls _mergedDictionaries.ForEachItem( x => { - if (Owner is object) + if (Owner is not null) { x.AddOwner(Owner); } }, x => { - if (Owner is object) + if (Owner is not null) { x.RemoveOwner(Owner); } @@ -88,6 +92,34 @@ namespace Avalonia.Controls } } + public IDictionary ThemeDictionaries + { + get + { + if (_themeDictionary == null) + { + _themeDictionary = new AvaloniaDictionary(2); + _themeDictionary.ForEachItem( + (_, x) => + { + if (Owner is not null) + { + x.AddOwner(Owner); + } + }, + (_, x) => + { + if (Owner is not null) + { + x.RemoveOwner(Owner); + } + }, + () => throw new NotSupportedException("Dictionary reset not supported")); + } + return _themeDictionary; + } + } + bool IResourceNode.HasResources { get @@ -152,16 +184,47 @@ namespace Avalonia.Controls return false; } - public bool TryGetResource(object key, out object? value) + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) { if (TryGetValue(key, out value)) return true; + if (_themeDictionary is not null) + { + IResourceProvider? themeResourceProvider; + if (theme is not null) + { + if (_themeDictionary.TryGetValue(theme, out themeResourceProvider) + && themeResourceProvider.TryGetResource(key, theme, out value)) + { + return true; + } + + var themeInherit = theme.InheritVariant; + while (themeInherit is not null) + { + if (_themeDictionary.TryGetValue(themeInherit, out themeResourceProvider) + && themeResourceProvider.TryGetResource(key, theme, out value)) + { + return true; + } + + themeInherit = themeInherit.InheritVariant; + } + } + + if (_themeDictionary.TryGetValue(ThemeVariant.Default, out themeResourceProvider) + && themeResourceProvider.TryGetResource(key, theme, out value)) + { + return true; + } + } + if (_mergedDictionaries != null) { for (var i = _mergedDictionaries.Count - 1; i >= 0; --i) { - if (_mergedDictionaries[i].TryGetResource(key, out value)) + if (_mergedDictionaries[i].TryGetResource(key, theme, out value)) { return true; } @@ -248,7 +311,7 @@ namespace Avalonia.Controls var hasResources = _inner?.Count > 0; - if (_mergedDictionaries is object) + if (_mergedDictionaries is not null) { foreach (var i in _mergedDictionaries) { @@ -256,6 +319,14 @@ namespace Avalonia.Controls hasResources |= i.HasResources; } } + if (_themeDictionary is not null) + { + foreach (var i in _themeDictionary.Values) + { + i.AddOwner(owner); + hasResources |= i.HasResources; + } + } if (hasResources) { @@ -273,7 +344,7 @@ namespace Avalonia.Controls var hasResources = _inner?.Count > 0; - if (_mergedDictionaries is object) + if (_mergedDictionaries is not null) { foreach (var i in _mergedDictionaries) { @@ -281,6 +352,14 @@ namespace Avalonia.Controls hasResources |= i.HasResources; } } + if (_themeDictionary is not null) + { + foreach (var i in _themeDictionary.Values) + { + i.RemoveOwner(owner); + hasResources |= i.HasResources; + } + } if (hasResources) { diff --git a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs index 0bf1073098..4b0bab0c92 100644 --- a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs @@ -1,6 +1,4 @@ using System; -using Avalonia.Data.Converters; -using Avalonia.LogicalTree; using Avalonia.Reactive; using Avalonia.Styling; @@ -41,21 +39,66 @@ namespace Avalonia.Controls control = control ?? throw new ArgumentNullException(nameof(control)); key = key ?? throw new ArgumentNullException(nameof(key)); - IResourceNode? current = control; + return control.TryFindResource(key, null, out value); + } + + /// + /// Finds the specified resource by searching up the logical tree and then global styles. + /// + /// The control. + /// Theme used to select theme dictionary. + /// The resource key. + /// The resource, or if not found. + public static object? FindResource(this IResourceHost control, ThemeVariant? theme, object key) + { + control = control ?? throw new ArgumentNullException(nameof(control)); + key = key ?? throw new ArgumentNullException(nameof(key)); + + if (control.TryFindResource(key, theme, out var value)) + { + return value; + } + + return AvaloniaProperty.UnsetValue; + } + + /// + /// Tries to the specified resource by searching up the logical tree and then global styles. + /// + /// The control. + /// The resource key. + /// Theme used to select theme dictionary. + /// On return, contains the resource if found, otherwise null. + /// True if the resource was found; otherwise false. + public static bool TryFindResource(this IResourceHost control, object key, ThemeVariant? theme, out object? value) + { + control = control ?? throw new ArgumentNullException(nameof(control)); + key = key ?? throw new ArgumentNullException(nameof(key)); + + IResourceHost? current = control; while (current != null) { - if (current.TryGetResource(key, out value)) + if (current.TryGetResource(key, theme, out value)) { return true; } - current = (current as IStyleHost)?.StylingParent as IResourceNode; + current = (current as IStyleHost)?.StylingParent as IResourceHost; } value = null; return false; } + + /// + public static bool TryGetResource(this IResourceHost control, object key, out object? value) + { + control = control ?? throw new ArgumentNullException(nameof(control)); + key = key ?? throw new ArgumentNullException(nameof(key)); + + return control.TryGetResource(key, null, out value); + } public static IObservable GetResourceObservable( this IResourceHost control, @@ -95,24 +138,49 @@ namespace Avalonia.Controls protected override void Initialize() { _target.ResourcesChanged += ResourcesChanged; + if (_target is StyledElement themeStyleable) + { + themeStyleable.PropertyChanged += PropertyChanged; + } } protected override void Deinitialize() { _target.ResourcesChanged -= ResourcesChanged; + if (_target is StyledElement themeStyleable) + { + themeStyleable.PropertyChanged -= PropertyChanged; + } } protected override void Subscribed(IObserver observer, bool first) { - observer.OnNext(Convert(_target.FindResource(_key))); + observer.OnNext(GetValue()); } private void ResourcesChanged(object? sender, ResourcesChangedEventArgs e) { - PublishNext(Convert(_target.FindResource(_key))); + PublishNext(GetValue()); + } + + private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == StyledElement.ActualThemeVariantProperty) + { + PublishNext(GetValue()); + } } - private object? Convert(object? value) => _converter?.Invoke(value) ?? value; + private object? GetValue() + { + if (_target is not StyledElement themeStyleable + || !_target.TryFindResource(_key, themeStyleable.ActualThemeVariant, out var value)) + { + value = _target.FindResource(_key) ?? AvaloniaProperty.UnsetValue; + } + + return _converter?.Invoke(value) ?? value; + } } private class FloatingResourceObservable : LightweightObservableBase @@ -134,7 +202,7 @@ namespace Avalonia.Controls _target.OwnerChanged += OwnerChanged; _owner = _target.Owner; - if (_owner is object) + if (_owner is not null) { _owner.ResourcesChanged += ResourcesChanged; } @@ -148,43 +216,68 @@ namespace Avalonia.Controls protected override void Subscribed(IObserver observer, bool first) { - if (_target.Owner is object) + if (_target.Owner is not null) { - observer.OnNext(Convert(_target.Owner.FindResource(_key))); + observer.OnNext(GetValue()); } } private void PublishNext() { - if (_target.Owner is object) + if (_target.Owner is not null) { - PublishNext(Convert(_target.Owner.FindResource(_key))); + PublishNext(GetValue()); } } private void OwnerChanged(object? sender, EventArgs e) { - if (_owner is object) + if (_owner is not null) { _owner.ResourcesChanged -= ResourcesChanged; } + if (_owner is StyledElement styleable) + { + styleable.PropertyChanged += PropertyChanged; + } _owner = _target.Owner; - if (_owner is object) + if (_owner is not null) { _owner.ResourcesChanged += ResourcesChanged; } + if (_owner is StyledElement styleable2) + { + styleable2.PropertyChanged += PropertyChanged; + } PublishNext(); } + private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == StyledElement.ActualThemeVariantProperty) + { + PublishNext(); + } + } + private void ResourcesChanged(object? sender, ResourcesChangedEventArgs e) { PublishNext(); } - private object? Convert(object? value) => _converter?.Invoke(value) ?? value; + private object? GetValue() + { + if (!(_target.Owner is StyledElement themeStyleable) + || !_target.Owner.TryFindResource(_key, themeStyleable.ActualThemeVariant, out var value)) + { + value = _target.Owner?.FindResource(_key) ?? AvaloniaProperty.UnsetValue; + } + + return _converter?.Invoke(value) ?? value; + } } } } diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 6043175eee..d23c585299 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -71,6 +71,23 @@ namespace Avalonia public static readonly StyledProperty ThemeProperty = AvaloniaProperty.Register(nameof(Theme)); + /// + /// Defines the property. + /// + public static readonly StyledProperty ActualThemeVariantProperty = + AvaloniaProperty.Register( + nameof(ThemeVariant), + inherits: true, + defaultValue: ThemeVariant.Light); + + /// + /// Defines the property. + /// + public static readonly StyledProperty RequestedThemeVariantProperty = + AvaloniaProperty.Register( + nameof(ThemeVariant), + defaultValue: ThemeVariant.Default); + private static readonly ControlTheme s_invalidTheme = new ControlTheme(); private int _initCount; private string? _name; @@ -257,6 +274,15 @@ namespace Avalonia set => SetValue(ThemeProperty, value); } + /// + /// Gets the UI theme that is currently used by the element, which might be different than the . + /// + /// + /// If current control is contained in the ThemeVariantScope, TopLevel or Application with non-default RequestedThemeVariant, that value will be returned. + /// Otherwise, current OS theme variant is returned. + /// + public ThemeVariant ActualThemeVariant => GetValue(ActualThemeVariantProperty); + /// /// Gets the styled element's logical children. /// @@ -439,11 +465,11 @@ namespace Avalonia void IResourceHost.NotifyHostedResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e); /// - bool IResourceNode.TryGetResource(object key, out object? value) + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) { value = null; - return (_resources?.TryGetResource(key, out value) ?? false) || - (_styles?.TryGetResource(key, out value) ?? false); + return (_resources?.TryGetResource(key, theme, out value) ?? false) || + (_styles?.TryGetResource(key, theme, out value) ?? false); } /// @@ -621,6 +647,13 @@ namespace Avalonia if (change.Property == ThemeProperty) OnControlThemeChanged(); + else if (change.Property == RequestedThemeVariantProperty) + { + if (change.GetNewValue() is {} themeVariant && themeVariant != ThemeVariant.Default) + SetValue(ActualThemeVariantProperty, themeVariant); + else + ClearValue(ActualThemeVariantProperty); + } } private protected virtual void OnControlThemeChanged() @@ -658,7 +691,7 @@ namespace Avalonia { var theme = Theme; - // Explitly set Theme property takes precedence. + // Explicitly set Theme property takes precedence. if (theme is not null) return theme; diff --git a/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs b/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs new file mode 100644 index 0000000000..2467d99b3b --- /dev/null +++ b/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs @@ -0,0 +1,22 @@ +using System; +using Avalonia.Controls; +using Avalonia.Metadata; + +namespace Avalonia.Styling; + +/// +/// Interface for an application host element with a root theme variant. +/// +[Unstable] +public interface IGlobalThemeVariantProvider : IResourceHost +{ + /// + /// Gets the UI theme variant that is used by the control (and its child elements) for resource determination. + /// + ThemeVariant ActualThemeVariant { get; } + + /// + /// Raised when the theme variant is changed on the element or an ancestor of the element. + /// + event EventHandler? ActualThemeVariantChanged; +} diff --git a/src/Avalonia.Base/Styling/StyleBase.cs b/src/Avalonia.Base/Styling/StyleBase.cs index e8fc40ca4c..7dfa516bce 100644 --- a/src/Avalonia.Base/Styling/StyleBase.cs +++ b/src/Avalonia.Base/Styling/StyleBase.cs @@ -74,16 +74,16 @@ namespace Avalonia.Styling public event EventHandler? OwnerChanged; - public bool TryGetResource(object key, out object? result) + public bool TryGetResource(object key, ThemeVariant? themeVariant, out object? result) { - if (_resources is not null && _resources.TryGetResource(key, out result)) + if (_resources is not null && _resources.TryGetResource(key, themeVariant, out result)) return true; if (_children is not null) { for (var i = 0; i < _children.Count; ++i) { - if (_children[i].TryGetResource(key, out result)) + if (_children[i].TryGetResource(key, themeVariant, out result)) return true; } } diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs index 1b1886335f..5d5b1617aa 100644 --- a/src/Avalonia.Base/Styling/Styles.cs +++ b/src/Avalonia.Base/Styling/Styles.cs @@ -115,16 +115,16 @@ namespace Avalonia.Styling } /// - public bool TryGetResource(object key, out object? value) + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) { - if (_resources != null && _resources.TryGetResource(key, out value)) + if (_resources != null && _resources.TryGetResource(key, theme, out value)) { return true; } for (var i = Count - 1; i >= 0; --i) { - if (this[i].TryGetResource(key, out value)) + if (this[i].TryGetResource(key, theme, out value)) { return true; } diff --git a/src/Avalonia.Base/Styling/ThemeVariant.cs b/src/Avalonia.Base/Styling/ThemeVariant.cs new file mode 100644 index 0000000000..d9cb123925 --- /dev/null +++ b/src/Avalonia.Base/Styling/ThemeVariant.cs @@ -0,0 +1,65 @@ +using System; +using System.ComponentModel; +using System.Text; +using Avalonia.Platform; + +namespace Avalonia.Styling; + +[TypeConverter(typeof(ThemeVariantTypeConverter))] +public sealed record ThemeVariant(object Key) +{ + public ThemeVariant(object key, ThemeVariant? inheritVariant) + : this(key) + { + InheritVariant = inheritVariant; + } + + public static ThemeVariant Default { get; } = new(nameof(Default)); + public static ThemeVariant Light { get; } = new(nameof(Light)); + public static ThemeVariant Dark { get; } = new(nameof(Dark)); + + public ThemeVariant? InheritVariant { get; init; } + + public override string ToString() + { + return Key.ToString() ?? $"ThemeVariant {{ Key = {Key} }}"; + } + + public override int GetHashCode() + { + return Key.GetHashCode(); + } + + public bool Equals(ThemeVariant? other) + { + return Key == other?.Key; + } + + public static ThemeVariant FromPlatformThemeVariant(PlatformThemeVariant themeVariant) + { + return themeVariant switch + { + PlatformThemeVariant.Light => Light, + PlatformThemeVariant.Dark => Dark, + _ => throw new ArgumentOutOfRangeException(nameof(themeVariant), themeVariant, null) + }; + } + + public PlatformThemeVariant? ToPlatformThemeVariant() + { + if (this == Light) + { + return PlatformThemeVariant.Light; + } + else if (this == Dark) + { + return PlatformThemeVariant.Dark; + } + else if (InheritVariant is { } inheritVariant) + { + return inheritVariant.ToPlatformThemeVariant(); + } + + return null; + } +} diff --git a/src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs b/src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs new file mode 100644 index 0000000000..4da1b495f5 --- /dev/null +++ b/src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.ComponentModel; +using System.Globalization; + +namespace Avalonia.Styling; + +public class ThemeVariantTypeConverter : TypeConverter +{ + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + { + return sourceType == typeof(string); + } + + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + return value switch + { + nameof(ThemeVariant.Light) => ThemeVariant.Light, + nameof(ThemeVariant.Dark) => ThemeVariant.Dark, + _ => new ThemeVariant(value) + }; + } +} diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 5b652cce19..58cc02e8c5 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -4,6 +4,7 @@ using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Input.Raw; @@ -28,7 +29,7 @@ namespace Avalonia /// method. /// - Tracks the lifetime of the application. /// - public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost, IApplicationPlatformEvents + public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IGlobalThemeVariantProvider, IApplicationPlatformEvents { /// /// The application-global data templates. @@ -49,10 +50,22 @@ namespace Avalonia public static readonly StyledProperty DataContextProperty = StyledElement.DataContextProperty.AddOwner(); + /// + public static readonly StyledProperty ActualThemeVariantProperty = + StyledElement.ActualThemeVariantProperty.AddOwner(); + + /// + public static readonly StyledProperty RequestedThemeVariantProperty = + StyledElement.RequestedThemeVariantProperty.AddOwner(); + /// public event EventHandler? ResourcesChanged; - public event EventHandler? UrlsOpened; + /// + public event EventHandler? UrlsOpened; + + /// + public event EventHandler? ActualThemeVariantChanged; /// /// Creates an instance of the class. @@ -75,6 +88,19 @@ namespace Avalonia set { SetValue(DataContextProperty, value); } } + /// + public ThemeVariant? RequestedThemeVariant + { + get => GetValue(RequestedThemeVariantProperty); + set => SetValue(RequestedThemeVariantProperty, value); + } + + /// + public ThemeVariant ActualThemeVariant + { + get => GetValue(ActualThemeVariantProperty); + } + /// /// Gets the current instance of the class. /// @@ -191,11 +217,11 @@ namespace Avalonia public virtual void Initialize() { } /// - bool IResourceNode.TryGetResource(object key, out object? value) + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) { value = null; - return (_resources?.TryGetResource(key, out value) ?? false) || - Styles.TryGetResource(key, out value); + return (_resources?.TryGetResource(key, theme, out value) ?? false) || + Styles.TryGetResource(key, theme, out value); } void IResourceHost.NotifyHostedResourcesChanged(ResourcesChangedEventArgs e) @@ -222,10 +248,15 @@ namespace Avalonia FocusManager = new FocusManager(); InputManager = new InputManager(); + var settings = AvaloniaLocator.Current.GetRequiredService(); + settings.ColorValuesChanged += OnColorValuesChanged; + OnColorValuesChanged(settings, settings.GetColorValues()); + AvaloniaLocator.CurrentMutable .Bind().ToTransient() .Bind().ToConstant(this) .Bind().ToConstant(this) + .Bind().ToConstant(this) .Bind().ToConstant(FocusManager) .Bind().ToConstant(InputManager) .Bind().ToTransient() @@ -290,5 +321,26 @@ namespace Avalonia set => SetAndRaise(NameProperty, ref _name, value); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == RequestedThemeVariantProperty) + { + if (change.GetNewValue() is {} themeVariant && themeVariant != ThemeVariant.Default) + SetValue(ActualThemeVariantProperty, themeVariant); + else + ClearValue(ActualThemeVariantProperty); + } + else if (change.Property == ActualThemeVariantProperty) + { + ActualThemeVariantChanged?.Invoke(this, EventArgs.Empty); + } + } + + private void OnColorValuesChanged(object? sender, PlatformColorValues e) + { + SetValue(ActualThemeVariantProperty, ThemeVariant.FromPlatformThemeVariant(e.ThemeVariant), BindingPriority.Template); + } } } diff --git a/src/Avalonia.Controls/ThemeVariantScope.cs b/src/Avalonia.Controls/ThemeVariantScope.cs new file mode 100644 index 0000000000..b9724251c7 --- /dev/null +++ b/src/Avalonia.Controls/ThemeVariantScope.cs @@ -0,0 +1,23 @@ +using Avalonia.Styling; + +namespace Avalonia.Controls +{ + /// + /// Decorator control that isolates controls subtree with locally defined . + /// + public class ThemeVariantScope : Decorator + { + /// + /// Gets or sets the UI theme variant that is used by the control (and its child elements) for resource determination. + /// The UI theme you specify with ThemeVariant can override the app-level ThemeVariant. + /// + /// + /// Setting RequestedThemeVariant to will apply parent's actual theme variant on the current scope. + /// + public ThemeVariant? RequestedThemeVariant + { + get => GetValue(RequestedThemeVariantProperty); + set => SetValue(RequestedThemeVariantProperty, value); + } + } +} diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index c0265e28b9..e92b310057 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Metadata; using Avalonia.Controls.Notifications; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; +using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; @@ -96,6 +97,7 @@ namespace Avalonia.Controls private readonly IKeyboardNavigationHandler? _keyboardNavigationHandler; private readonly IPlatformRenderInterface? _renderInterface; private readonly IGlobalStyles? _globalStyles; + private readonly IGlobalThemeVariantProvider? _applicationThemeHost; private readonly PointerOverPreProcessor? _pointerOverPreProcessor; private readonly IDisposable? _pointerOverPreProcessorSubscription; private readonly IDisposable? _backGestureSubscription; @@ -114,16 +116,6 @@ namespace Avalonia.Controls { KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(KeyboardNavigationMode.Cycle); AffectsMeasure(ClientSizeProperty); - - TransparencyLevelHintProperty.Changed.AddClassHandler( - (tl, e) => - { - if (tl.PlatformImpl != null) - { - tl.PlatformImpl.SetTransparencyLevelHint((WindowTransparencyLevel)e.NewValue!); - tl.HandleTransparencyLevelChanged(tl.PlatformImpl.TransparencyLevel); - } - }); } /// @@ -161,6 +153,7 @@ namespace Avalonia.Controls _keyboardNavigationHandler = TryGetService(dependencyResolver); _renderInterface = TryGetService(dependencyResolver); _globalStyles = TryGetService(dependencyResolver); + _applicationThemeHost = TryGetService(dependencyResolver); Renderer = impl.CreateRenderer(this); @@ -191,6 +184,11 @@ namespace Avalonia.Controls _globalStyles.GlobalStylesAdded += ((IStyleHost)this).StylesAdded; _globalStyles.GlobalStylesRemoved += ((IStyleHost)this).StylesRemoved; } + if (_applicationThemeHost is { }) + { + SetValue(ActualThemeVariantProperty, _applicationThemeHost.ActualThemeVariant, BindingPriority.Template); + _applicationThemeHost.ActualThemeVariantChanged += GlobalActualThemeVariantChanged; + } ClientSize = impl.ClientSize; FrameSize = impl.FrameSize; @@ -315,6 +313,13 @@ namespace Avalonia.Controls set => SetValue(TransparencyBackgroundFallbackProperty, value); } + /// + public ThemeVariant? RequestedThemeVariant + { + get => GetValue(RequestedThemeVariantProperty); + set => SetValue(RequestedThemeVariantProperty, value); + } + /// /// Occurs when physical Back Button is pressed or a back navigation has been requested. /// @@ -413,6 +418,24 @@ namespace Avalonia.Controls return visual == null ? null : visual.VisualRoot as TopLevel; } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == TransparencyLevelHintProperty) + { + if (PlatformImpl != null) + { + PlatformImpl.SetTransparencyLevelHint(change.GetNewValue()); + HandleTransparencyLevelChanged(PlatformImpl.TransparencyLevel); + } + } + else if (change.Property == ActualThemeVariantProperty) + { + PlatformImpl?.SetFrameThemeVariant(change.GetNewValue().ToPlatformThemeVariant() ?? PlatformThemeVariant.Light); + } + } + /// /// Creates the layout manager for this . /// @@ -437,6 +460,10 @@ namespace Avalonia.Controls _globalStyles.GlobalStylesAdded -= ((IStyleHost)this).StylesAdded; _globalStyles.GlobalStylesRemoved -= ((IStyleHost)this).StylesRemoved; } + if (_applicationThemeHost is { }) + { + _applicationThemeHost.ActualThemeVariantChanged -= GlobalActualThemeVariantChanged; + } Renderer?.Dispose(); Renderer = null!; @@ -589,6 +616,11 @@ namespace Avalonia.Controls _inputManager?.ProcessInput(e); } + private void GlobalActualThemeVariantChanged(object? sender, EventArgs e) + { + SetValue(ActualThemeVariantProperty, ((IGlobalThemeVariantProvider)sender!).ActualThemeVariant, BindingPriority.Template); + } + private void SceneInvalidated(object? sender, SceneInvalidatedEventArgs e) { _pointerOverPreProcessor?.SceneInvalidated(e.DirtyRect); diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs index 00173dbb35..7426c4e2ed 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs @@ -1,19 +1,22 @@ using System; using Avalonia.Controls; +using Avalonia.Styling; using Lifetimes = Avalonia.Controls.ApplicationLifetimes; -using App = Avalonia.Application; namespace Avalonia.Diagnostics.Controls { class Application : AvaloniaObject - , Input.ICloseable + , Input.ICloseable, IDisposable { - private readonly App _application; + private readonly Avalonia.Application _application; public event EventHandler? Closed; - public Application(App application) + public static readonly StyledProperty RequestedThemeVariantProperty = + StyledElement.RequestedThemeVariantProperty.AddOwner(); + + public Application(Avalonia.Application application) { _application = application; @@ -33,9 +36,12 @@ namespace Avalonia.Diagnostics.Controls Lifetimes.ISingleViewApplicationLifetime single => (single.MainView as Visual)?.VisualRoot?.Renderer, _ => null }; + + RequestedThemeVariant = application.RequestedThemeVariant; + _application.PropertyChanged += ApplicationOnPropertyChanged; } - internal App Instance => _application; + internal Avalonia.Application Instance => _application; /// /// Defines the property. @@ -114,5 +120,35 @@ namespace Avalonia.Diagnostics.Controls /// Gets the root of the visual tree, if the control is attached to a visual tree. /// internal Rendering.IRenderer? RendererRoot { get; } + + /// + public ThemeVariant? RequestedThemeVariant + { + get => GetValue(RequestedThemeVariantProperty); + set => SetValue(RequestedThemeVariantProperty, value); + } + + public void Dispose() + { + _application.PropertyChanged -= ApplicationOnPropertyChanged; + } + + private void ApplicationOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == Avalonia.Application.RequestedThemeVariantProperty) + { + RequestedThemeVariant = e.GetNewValue(); + } + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == RequestedThemeVariantProperty) + { + _application.RequestedThemeVariant = change.GetNewValue(); + } + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs index 5fc274a4e9..3cfb0246eb 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs @@ -1,4 +1,6 @@ -using Avalonia.Input; +using System; +using Avalonia.Input; +using Avalonia.Styling; namespace Avalonia.Diagnostics { @@ -42,8 +44,8 @@ namespace Avalonia.Diagnostics = Conventions.DefaultScreenshotHandler; /// - /// Gets or sets whether DevTools should use the dark mode theme + /// Gets or sets whether DevTools theme. /// - public bool UseDarkMode { get; set; } + public ThemeVariant? ThemeVariant { get; set; } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index 8bff9ccde0..1951914273 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -123,7 +123,8 @@ namespace Avalonia.Diagnostics.ViewModels private static (object resourceKey, bool isDynamic)? GetResourceInfo(object? value) { - if (value is StaticResourceExtension staticResource) + if (value is StaticResourceExtension staticResource + && staticResource.ResourceKey != null) { return (staticResource.ResourceKey, false); } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml index 6c1da3ec00..748c2cc313 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml @@ -9,9 +9,9 @@ - + - + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index dbc4c98f78..4768c88f75 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -263,13 +263,9 @@ namespace Avalonia.Diagnostics.Views public void SetOptions(DevToolsOptions options) { (DataContext as MainViewModel)?.SetOptions(options); - - if (options.UseDarkMode) + if (options.ThemeVariant is { } themeVariant) { - if (Styles[0] is SimpleTheme st) - { - st.Mode = SimpleThemeMode.Dark; - } + RequestedThemeVariant = themeVariant; } } diff --git a/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs b/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs index a492dfed3a..e46b9276fc 100644 --- a/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs +++ b/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs @@ -9,7 +9,7 @@ namespace Avalonia.Dialogs.Internal { public object Convert(object key, Type targetType, object parameter, CultureInfo culture) { - TryGetResource((string)key, out var value); + TryGetResource((string)key, null, out var value); return value; } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index 84b4f3bdba..cdd344becc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using Avalonia.Controls; using Avalonia.Markup.Data; @@ -7,6 +8,8 @@ using Avalonia.Markup.Xaml.Converters; using Avalonia.Markup.Xaml.XamlIl.Runtime; using Avalonia.Styling; +#nullable enable + namespace Avalonia.Markup.Xaml.MarkupExtensions { public class StaticResourceExtension @@ -20,12 +23,18 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions ResourceKey = resourceKey; } - public object ResourceKey { get; set; } + public object? ResourceKey { get; set; } public object ProvideValue(IServiceProvider serviceProvider) { + if (ResourceKey is not { } resourceKey) + { + throw new ArgumentException("StaticResourceExtension.ResourceKey must be set."); + } + var stack = serviceProvider.GetService(); var provideTarget = serviceProvider.GetService(); + var themeVariant = (provideTarget.TargetObject as StyledElement)?.ActualThemeVariant; var targetType = provideTarget.TargetProperty switch { @@ -36,14 +45,14 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions if (provideTarget.TargetObject is Setter { Property: not null } setter) { - targetType = setter.Property.PropertyType; + targetType = setter.Property?.PropertyType; } // Look upwards though the ambient context for IResourceNodes // which might be able to give us the resource. foreach (var parent in stack.Parents) { - if (parent is IResourceNode node && node.TryGetResource(ResourceKey, out var value)) + if (parent is IResourceNode node && node.TryGetResource(resourceKey, themeVariant, out var value)) { return ColorToBrushConverter.Convert(value, targetType); } @@ -60,12 +69,12 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions return AvaloniaProperty.UnsetValue; } - throw new KeyNotFoundException($"Static resource '{ResourceKey}' not found."); + throw new KeyNotFoundException($"Static resource '{resourceKey}' not found."); } - private object GetValue(StyledElement control, Type targetType) + private object GetValue(StyledElement control, Type? targetType) { - return ColorToBrushConverter.Convert(control.FindResource(ResourceKey), targetType); + return ColorToBrushConverter.Convert(control.FindResource(ResourceKey!), targetType); } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs index 595b37f7d1..4ff105cf1f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using Avalonia.Controls; +using Avalonia.Styling; #nullable enable @@ -74,11 +75,11 @@ namespace Avalonia.Markup.Xaml.Styling remove => Loaded.OwnerChanged -= value; } - bool IResourceNode.TryGetResource(object key, out object? value) + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) { if (!_isLoading) { - return Loaded.TryGetResource(key, out value); + return Loaded.TryGetResource(key, theme, out value); } value = null; diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index b87aa64297..27367fce5e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -91,11 +91,11 @@ namespace Avalonia.Markup.Xaml.Styling } } - public bool TryGetResource(object key, out object? value) + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) { if (!_isLoading) { - return Loaded.TryGetResource(key, out value); + return Loaded.TryGetResource(key, theme, out value); } value = null; From bd9c9783ab1b6110b26db8bf29ce9f044da71c64 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 18 Jan 2023 02:05:40 -0500 Subject: [PATCH 003/185] Parse ThemeVariant compile time and merge ThemeDictionaries --- .../AvaloniaXamlIlLanguageParseIntrinsics.cs | 20 ++++ .../XamlMergeResourceGroupTransformer.cs | 100 ++++++++++++++++-- .../AvaloniaXamlIlWellKnownTypes.cs | 4 + 3 files changed, 118 insertions(+), 6 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index 925bf0a4fa..365a07a7f6 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -291,6 +291,26 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return true; } + if (type.Equals(types.ThemeVariant)) + { + var variantText = text.Trim(); + var foundConstProperty = types.ThemeVariant.Properties.FirstOrDefault(p => + p.Name == variantText && p.PropertyType == types.ThemeVariant); + var themeVariantTypeRef = new XamlAstClrTypeReference(node, types.ThemeVariant, false); + if (foundConstProperty is not null) + { + result = new XamlStaticExtensionNode(new XamlAstObjectNode(node, node.Type), themeVariantTypeRef, foundConstProperty.Name); + return true; + } + + result = new XamlAstNewClrObjectNode(node, themeVariantTypeRef, types.ThemeVariantConstructor, + new List() + { + new XamlConstantNode(node, context.Configuration.WellKnownTypes.String, variantText) + }); + return true; + } + result = null; return false; } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs index 8c83c74248..db8d604154 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs @@ -4,6 +4,8 @@ using System.Linq; using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; using XamlX.Ast; using XamlX.IL.Emitters; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers; #nullable enable @@ -72,14 +74,20 @@ internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer } } - var manipulationGroup = new XamlManipulationGroupNode(node, new List()); + if (!mergeSourceNodes.Any()) + { + return node; + } + + var manipulationGroup = new List(); foreach (var sourceNode in mergeSourceNodes) { var (originalAssetPath, propertyNode) = AvaloniaXamlIncludeTransformer.ResolveSourceFromXamlInclude(context, "MergeResourceInclude", sourceNode, true); if (originalAssetPath is null) { - return node; + return context.ParseError( + $"Node MergeResourceInclude is unable to resolve \"{originalAssetPath}\" path.", propertyNode, node); } var targetDocument = context.Documents.FirstOrDefault(d => @@ -99,15 +107,95 @@ internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer $"MergeResourceInclude can only include another ResourceDictionary", propertyNode, node); } - manipulationGroup.Children.Add(singleRootObject.Manipulation); + manipulationGroup.Add(singleRootObject.Manipulation); } + + // Order of resources is defined by ResourceDictionary.TryGetResource. + // It is read by following priority: + // - own resources. + // - own theme dictionaries. + // - merged dictionaries. + // We need to maintain this order when we inject "compiled merged" resources. + // Doing this by injecting merged dictionaries in the beginning, so it can be overwritten by "own resources". + // MergedDictionaries are read first, so we need ot inject our merged values in the beginning. + var children = resourceDictionaryManipulation.Children; + children.InsertRange(0, manipulationGroup); - if (manipulationGroup.Children.Any()) + // Flatten resource assignments. + for (var i = 0; i < children.Count; i++) { - // MergedDictionaries are read first, so we need ot inject our merged values in the beginning. - resourceDictionaryManipulation.Children.Insert(0, manipulationGroup); + if (children[i] is XamlManipulationGroupNode group) + { + children.RemoveAt(i); + children.AddRange(group.Children); + i--; // step back, so new items can be reiterated. + } + } + + // Merge "ThemeDictionaries" as well. + for (var i = children.Count - 1; i >= 0; i--) + { + if (children[i] is XamlPropertyAssignmentNode assignmentNode + && assignmentNode.Property.Name == "ThemeDictionaries" + && assignmentNode.Values.Count == 2 + && assignmentNode.Values[0] is {} key + && assignmentNode.Values[1] is XamlValueWithManipulationNode + { + Manipulation: XamlObjectInitializationNode + { + Manipulation: XamlManipulationGroupNode valueGroup + } + }) + { + for (var j = i - 1; j >= 0; j--) + { + if (children[j] is XamlPropertyAssignmentNode sameKeyPrevAssignmentNode + && sameKeyPrevAssignmentNode.Property.Name == "ThemeDictionaries" + && sameKeyPrevAssignmentNode.Values.Count == 2 + && sameKeyPrevAssignmentNode.Values[1] is XamlValueWithManipulationNode + { + Manipulation: XamlObjectInitializationNode + { + Manipulation: XamlManipulationGroupNode sameKeyPrevValueGroup + } + } + && ThemeVariantNodeEquals(context, key, sameKeyPrevAssignmentNode.Values[0])) + { + sameKeyPrevValueGroup.Children.AddRange(valueGroup.Children); + children.RemoveAt(i); + break; + } + } + } } return node; } + + public static bool ThemeVariantNodeEquals(AstGroupTransformationContext context, IXamlAstValueNode left, IXamlAstValueNode right) + { + if (left is XamlConstantNode leftConst + && right is XamlConstantNode rightConst) + { + return leftConst.Constant == rightConst.Constant; + } + if (left is XamlStaticExtensionNode leftStaticExt + && right is XamlStaticExtensionNode rightStaticExt) + { + return leftStaticExt.Type.GetClrType().GetFullName() == rightStaticExt.Type.GetClrType().GetFullName() + && leftStaticExt.Member == rightStaticExt.Member; + } + if (left is XamlAstNewClrObjectNode leftClrObjectNode + && right is XamlAstNewClrObjectNode rightClrObjectNode) + { + var themeVariant = context.GetAvaloniaTypes().ThemeVariant; + return leftClrObjectNode.Type.GetClrType() == themeVariant + && leftClrObjectNode.Type == rightClrObjectNode.Type + && leftClrObjectNode.Constructor == rightClrObjectNode.Constructor + && ThemeVariantNodeEquals(context, leftClrObjectNode.Arguments.Single(), + leftClrObjectNode.Arguments.Single()); + } + + return false; + } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index aab6239a35..f9e14a7641 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -67,6 +67,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlConstructor FontFamilyConstructorUriName { get; } public IXamlType Thickness { get; } public IXamlConstructor ThicknessFullConstructor { get; } + public IXamlType ThemeVariant { get; } + public IXamlConstructor ThemeVariantConstructor { get; } public IXamlType Point { get; } public IXamlConstructor PointFullConstructor { get; } public IXamlType Vector { get; } @@ -188,6 +190,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers Uri = cfg.TypeSystem.GetType("System.Uri"); FontFamily = cfg.TypeSystem.GetType("Avalonia.Media.FontFamily"); FontFamilyConstructorUriName = FontFamily.GetConstructor(new List { Uri, XamlIlTypes.String }); + ThemeVariant = cfg.TypeSystem.GetType("Avalonia.Styling.ThemeVariant"); + ThemeVariantConstructor = ThemeVariant.GetConstructor(new List { XamlIlTypes.String }); (IXamlType, IXamlConstructor) GetNumericTypeInfo(string name, IXamlType componentType, int componentCount) { From 1d69936c7946a0d9df7cc5c014ec0a5b4093587c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 18 Jan 2023 02:06:00 -0500 Subject: [PATCH 004/185] Update default themes, use ThemeDictionaries --- src/Avalonia.Themes.Fluent/Accents/Base.xaml | 511 +++++- .../Accents/BaseDark.xaml | 178 -- .../Accents/BaseLight.xaml | 181 -- .../Accents/FluentControlResources.xaml | 1551 +++++++++++++++++ .../Accents/FluentControlResourcesDark.xaml | 643 ------- .../Accents/FluentControlResourcesLight.xaml | 638 ------- .../Controls/FluentControls.xaml | 1 + .../Controls/ThemeVariantScope.xaml | 9 + src/Avalonia.Themes.Fluent/FluentTheme.xaml | 5 +- .../FluentTheme.xaml.cs | 53 +- src/Avalonia.Themes.Simple/Accents/Base.xaml | 185 +- .../Accents/BaseDark.xaml | 38 - .../Accents/BaseLight.xaml | 37 - .../Controls/SimpleControls.xaml | 1 + .../Controls/ThemeVariantScope.xaml | 8 + src/Avalonia.Themes.Simple/SimpleTheme.xaml | 6 +- .../SimpleTheme.xaml.cs | 70 +- src/Avalonia.Themes.Simple/SimpleThemeMode.cs | 8 - 18 files changed, 2187 insertions(+), 1936 deletions(-) delete mode 100644 src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml delete mode 100644 src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml create mode 100644 src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml delete mode 100644 src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml delete mode 100644 src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml create mode 100644 src/Avalonia.Themes.Fluent/Controls/ThemeVariantScope.xaml delete mode 100644 src/Avalonia.Themes.Simple/Accents/BaseDark.xaml delete mode 100644 src/Avalonia.Themes.Simple/Accents/BaseLight.xaml create mode 100644 src/Avalonia.Themes.Simple/Controls/ThemeVariantScope.xaml delete mode 100644 src/Avalonia.Themes.Simple/SimpleThemeMode.cs diff --git a/src/Avalonia.Themes.Fluent/Accents/Base.xaml b/src/Avalonia.Themes.Fluent/Accents/Base.xaml index 479bcd8531..7512fa4cfa 100644 --- a/src/Avalonia.Themes.Fluent/Accents/Base.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/Base.xaml @@ -2,38 +2,481 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="using:System" xmlns:converters="using:Avalonia.Controls.Converters"> - - - #FFF0F0F0 - #FF000000 - #FF6D6D6D - #FF3399FF - #FFFFFFFF - #FF0066CC - #FFFFFFFF - #FF000000 - avares://Avalonia.Themes.Fluent/Assets#Inter - 14 - - - True - 1 - 2 - 10,6,6,5 - 20 - 20 - 8,5,8,6 - - - 3 - 5 - - - scaleX(0.125) translateX(-2px) - scaleY(0.125) translateY(-2px) - - - - - + + avares://Avalonia.Themes.Fluent/Assets#Inter + 14 + + + True + 1 + 2 + 10,6,6,5 + 20 + 20 + 8,5,8,6 + + + 3 + 5 + + + scaleX(0.125) translateX(-2px) + scaleY(0.125) translateY(-2px) + + + + + + + + + + #FFFFFFFF + #33FFFFFF + #99FFFFFF + #CCFFFFFF + #66FFFFFF + #FF000000 + #33000000 + #99000000 + #CC000000 + #66000000 + #FF171717 + #FF000000 + #33000000 + #66000000 + #CC000000 + #FFCCCCCC + #FF7A7A7A + #FFCCCCCC + #FFF2F2F2 + #FFE6E6E6 + #FFF2F2F2 + #FFFFFFFF + #FF767676 + #19000000 + #33000000 + #C50500 + + #17000000 + #2E000000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #FFFFFFFF + + + + 374 + 0,2,0,2 + 1 + -1,0,-1,0 + 32 + 64 + 456 + 0 + 1 + 0 + + 12,11,12,12 + 96 + 40 + 758 + + + 0 + + + 0,4,0,4 + + + 12,0,12,0 + + + + #FF000000 + #33000000 + #99000000 + #CC000000 + #66000000 + #FFFFFFFF + #33FFFFFF + #99FFFFFF + #CCFFFFFF + #66FFFFFF + #FFF2F2F2 + #FF000000 + #33000000 + #66000000 + #CC000000 + #FF333333 + #FF858585 + #FF767676 + #FF171717 + #FF1F1F1F + #FF2B2B2B + #FFFFFFFF + #FF767676 + #19FFFFFF + #33FFFFFF + #FFF000 + + #18FFFFFF + #30FFFFFF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #FF000000 + + + 374 + 0,2,0,2 + 1 + -1,0,-1,0 + 32 + 64 + 456 + 0 + 1 + 0 + + 12,11,12,12 + 96 + 40 + 758 + + + 0 + + + 0,4,0,4 + + + 12,0,12,0 + + diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml deleted file mode 100644 index 0192fb1b54..0000000000 --- a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml +++ /dev/null @@ -1,178 +0,0 @@ - - - #FF000000 - #33000000 - #99000000 - #CC000000 - #66000000 - #FFFFFFFF - #33FFFFFF - #99FFFFFF - #CCFFFFFF - #66FFFFFF - #FFF2F2F2 - #FF000000 - #33000000 - #66000000 - #CC000000 - #FF333333 - #FF858585 - #FF767676 - #FF171717 - #FF1F1F1F - #FF2B2B2B - #FFFFFFFF - #FF767676 - #19FFFFFF - #33FFFFFF - #FFF000 - - #18FFFFFF - #30FFFFFF - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #FF000000 - - - 374 - 0,2,0,2 - 1 - -1,0,-1,0 - 32 - 64 - 456 - 0 - 1 - 0 - - 12,11,12,12 - 96 - 40 - 758 - - - 0 - - - 0,4,0,4 - - - 12,0,12,0 - diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml deleted file mode 100644 index a9e5ed949a..0000000000 --- a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml +++ /dev/null @@ -1,181 +0,0 @@ - - - #FFFFFFFF - #33FFFFFF - #99FFFFFF - #CCFFFFFF - #66FFFFFF - #FF000000 - #33000000 - #99000000 - #CC000000 - #66000000 - #FF171717 - #FF000000 - #33000000 - #66000000 - #CC000000 - #FFCCCCCC - #FF7A7A7A - #FFCCCCCC - #FFF2F2F2 - #FFE6E6E6 - #FFF2F2F2 - #FFFFFFFF - #FF767676 - #19000000 - #33000000 - #C50500 - - #17000000 - #2E000000 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #FFFFFFFF - - - - 374 - 0,2,0,2 - 1 - -1,0,-1,0 - 32 - 64 - 456 - 0 - 1 - 0 - - 12,11,12,12 - 96 - 40 - 758 - - - 0 - - - 0,4,0,4 - - - 12,0,12,0 - diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml new file mode 100644 index 0000000000..a9bc622221 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml @@ -0,0 +1,1551 @@ + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 64 + 1 + 1 + 11,5,11,7 + Normal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0,4,0,4 + + + 0 + + + 4 + 0 + + + 1 + 32 + 0,0 + 12,0,0,0 + 12,4,12,4 + + + + + + + + + + + + + + + + + + 11,9,11,10 + 11,4,11,7 + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4 + 2 + 0 + + + + + + + + + + + + + + + + + + + + + + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 12 + 1 + + + + 8,5,8,7 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 24 + 12,0,12,0 + 12,0,12,0 + SemiLight + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + 16 + 8 + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 32 + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 64 + 1 + 1 + 11,5,11,7 + Normal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0,4,0,4 + + + 0 + + + 4 + 0 + + + 1 + 32 + 0,0 + 12,0,0,0 + 12,4,12,4 + + + + + + + + + + + + + + + + + + 11,9,11,10 + 11,4,11,7 + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4 + 2 + 0 + + + + + + + + + + + + + + + + + + + + + + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 12 + 1 + + + + 8,5,8,7 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 24 + 12,0,12,0 + 12,0,12,0 + SemiLight + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + 16 + 8 + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 32 + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml deleted file mode 100644 index 810065fc9b..0000000000 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ /dev/null @@ -1,643 +0,0 @@ - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 64 - 1 - 1 - 11,5,11,7 - Normal - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 - 0,4,0,4 - - - 0 - - - 4 - 0 - - - 1 - 32 - 0,0 - 12,0,0,0 - 12,4,12,4 - - - - - - - - - - - - - - - - - - 11,9,11,10 - 11,4,11,7 - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 4 - 2 - 0 - - - - - - - - - - - - - - - - - - - - - - 0 - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 12 - 1 - - - - 8,5,8,7 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 24 - 12,0,12,0 - 12,0,12,0 - SemiLight - - - - - - - - - - - - - - - - - - - - - - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - 16 - 8 - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - 32 - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml deleted file mode 100644 index bccc47b9b8..0000000000 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ /dev/null @@ -1,638 +0,0 @@ - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 64 - 1 - 1 - 11,5,11,7 - Normal - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 - 0,4,0,4 - - - 0 - - - 4 - 0 - - - 1 - 32 - 0,0 - 12,0,0,0 - 12,4,12,4 - - - - - - - - - - - - - - - - - - 11,9,11,10 - 11,4,11,7 - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 4 - 2 - 0 - - - - - - - - - - - - - - - - - - - - - - 0 - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 12 - 1 - - - - 8,5,8,7 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 24 - 12,0,12,0 - 12,0,12,0 - SemiLight - - - - - - - - - - - - - - - - - - - - - 0 - - - - - - - - - - - - - - - - - - - - - - - - - - 16 - 8 - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - 32 - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - - 1 - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 2c3550a72f..532b0cff1b 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -69,6 +69,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Controls/ThemeVariantScope.xaml b/src/Avalonia.Themes.Fluent/Controls/ThemeVariantScope.xaml new file mode 100644 index 0000000000..21a5506b88 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Controls/ThemeVariantScope.xaml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml index 44ca60e2fa..e83257fd9f 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -6,13 +6,10 @@ + - - - - diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs index a8297953a8..95539bc08a 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs @@ -6,12 +6,6 @@ using Avalonia.Styling; namespace Avalonia.Themes.Fluent { - public enum FluentThemeMode - { - Light, - Dark, - } - public enum DensityStyle { Normal, @@ -23,10 +17,6 @@ namespace Avalonia.Themes.Fluent /// public class FluentTheme : Styles { - private readonly IResourceDictionary _baseDark; - private readonly IResourceDictionary _fluentDark; - private readonly IResourceDictionary _baseLight; - private readonly IResourceDictionary _fluentLight; private readonly Styles _compactStyles; /// @@ -37,13 +27,8 @@ namespace Avalonia.Themes.Fluent { AvaloniaXamlLoader.Load(sp, this); - _baseDark = (IResourceDictionary)GetAndRemove("BaseDark"); - _fluentDark = (IResourceDictionary)GetAndRemove("FluentDark"); - _baseLight = (IResourceDictionary)GetAndRemove("BaseLight"); - _fluentLight = (IResourceDictionary)GetAndRemove("FluentLight"); _compactStyles = (Styles)GetAndRemove("CompactStyles"); - - EnsureThemeVariants(); + EnsureCompactStyles(); object GetAndRemove(string key) @@ -54,22 +39,10 @@ namespace Avalonia.Themes.Fluent return val; } } - - public static readonly StyledProperty ModeProperty = - AvaloniaProperty.Register(nameof(Mode)); - + public static readonly StyledProperty DensityStyleProperty = AvaloniaProperty.Register(nameof(DensityStyle)); - /// - /// Gets or sets the mode of the fluent theme (light, dark). - /// - public FluentThemeMode Mode - { - get => GetValue(ModeProperty); - set => SetValue(ModeProperty, value); - } - /// /// Gets or sets the density style of the fluent theme (normal, compact). /// @@ -82,11 +55,6 @@ namespace Avalonia.Themes.Fluent protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); - - if (change.Property == ModeProperty) - { - EnsureThemeVariants(); - } if (change.Property == DensityStyleProperty) { @@ -94,23 +62,6 @@ namespace Avalonia.Themes.Fluent } } - private void EnsureThemeVariants() - { - var themeVariantResource1 = Mode == FluentThemeMode.Dark ? _baseDark : _baseLight; - var themeVariantResource2 = Mode == FluentThemeMode.Dark ? _fluentDark : _fluentLight; - var dict = Resources.MergedDictionaries; - if (dict.Count == 0) - { - dict.Add(themeVariantResource1); - dict.Add(themeVariantResource2); - } - else - { - dict[0] = themeVariantResource1; - dict[1] = themeVariantResource2; - } - } - private void EnsureCompactStyles() { if (DensityStyle == DensityStyle.Compact) diff --git a/src/Avalonia.Themes.Simple/Accents/Base.xaml b/src/Avalonia.Themes.Simple/Accents/Base.xaml index bffdbd8a27..0c1354e475 100644 --- a/src/Avalonia.Themes.Simple/Accents/Base.xaml +++ b/src/Avalonia.Themes.Simple/Accents/Base.xaml @@ -1,62 +1,131 @@ - - #CC119EDA - #99119EDA - #66119EDA - #33119EDA - #FF808080 - #FFFFFFFF - #FFFF0000 - #10FF0000 - - - - - - - - - - - - - - - - - 1 - 0.5 + + + + #FFFFFFFF + #FFAAAAAA + #FF888888 + #FF333333 + #FF868999 + #FFF5F5F5 + #FFC2C3C9 + #FF686868 + #FF5B5B5B + #FFF0F0F0 + #FFD0D0D0 + #FF808080 + #FF000000 + #FF086F9E - 10 - 12 - 16 + + + + + + + + + + + + + - 18 - 8 + + + + + + #FF282828 + #FF505050 + #FF808080 + #FFA0A0A0 + #FF282828 + #FF505050 + #FF686868 + #FF808080 + #FFEFEBEF + #FFA8A8A8 + #FF828282 + #FF505050 + #FFDEDEDE + #FF119EDA - 20 - 20 + + + + + + + + + + + + + + + + + + + + #CC119EDA + #99119EDA + #66119EDA + #33119EDA + #FF808080 + #FFFFFFFF + #FFFF0000 + #10FF0000 + + + + + + + + + + + + + + + + + 1 + 0.5 + + 10 + 12 + 16 + + 18 + 8 + + 20 + 20 diff --git a/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml b/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml deleted file mode 100644 index 88c2681f65..0000000000 --- a/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml +++ /dev/null @@ -1,38 +0,0 @@ - - - #FF282828 - #FF505050 - #FF808080 - #FFA0A0A0 - #FF282828 - #FF505050 - #FF686868 - #FF808080 - #FFEFEBEF - #FFA8A8A8 - #FF828282 - #FF505050 - #FFDEDEDE - #FF119EDA - - - - - - - - - - - - - - - - - - - - diff --git a/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml b/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml deleted file mode 100644 index 77166a9d8a..0000000000 --- a/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml +++ /dev/null @@ -1,37 +0,0 @@ - - - #FFFFFFFF - #FFAAAAAA - #FF888888 - #FF333333 - #FF868999 - #FFF5F5F5 - #FFC2C3C9 - #FF686868 - #FF5B5B5B - #FFF0F0F0 - #FFD0D0D0 - #FF808080 - #FF000000 - #FF086F9E - - - - - - - - - - - - - - - - - - - diff --git a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml index 093adaeab2..479db9ed09 100644 --- a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml @@ -67,6 +67,7 @@ + diff --git a/src/Avalonia.Themes.Simple/Controls/ThemeVariantScope.xaml b/src/Avalonia.Themes.Simple/Controls/ThemeVariantScope.xaml new file mode 100644 index 0000000000..a6022fb263 --- /dev/null +++ b/src/Avalonia.Themes.Simple/Controls/ThemeVariantScope.xaml @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/Avalonia.Themes.Simple/SimpleTheme.xaml b/src/Avalonia.Themes.Simple/SimpleTheme.xaml index 5b0cae7fd2..f6d6ddfec9 100644 --- a/src/Avalonia.Themes.Simple/SimpleTheme.xaml +++ b/src/Avalonia.Themes.Simple/SimpleTheme.xaml @@ -4,12 +4,8 @@ - + - - - - diff --git a/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs b/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs index 42dfafd7e0..31b3243993 100644 --- a/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs +++ b/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs @@ -4,68 +4,16 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Styling; -namespace Avalonia.Themes.Simple +namespace Avalonia.Themes.Simple; + +public class SimpleTheme : Styles { - public class SimpleTheme : Styles + /// + /// Initializes a new instance of the class. + /// + /// The parent's service provider. + public SimpleTheme(IServiceProvider? sp = null) { - public static readonly StyledProperty ModeProperty = - AvaloniaProperty.Register(nameof(Mode)); - - private readonly IResourceDictionary _simpleDark; - private readonly IResourceDictionary _simpleLight; - - /// - /// Initializes a new instance of the class. - /// - /// The parent's service provider. - public SimpleTheme(IServiceProvider? sp = null) - { - AvaloniaXamlLoader.Load(sp, this); - - _simpleDark = (IResourceDictionary)GetAndRemove("BaseDark"); - _simpleLight = (IResourceDictionary)GetAndRemove("BaseLight"); - EnsureThemeVariant(); - - object GetAndRemove(string key) - { - var val = Resources[key] - ?? throw new KeyNotFoundException($"Key {key} was not found in the resources"); - Resources.Remove(key); - return val; - } - } - - /// - /// Gets or sets the mode of the fluent theme (light, dark). - /// - public SimpleThemeMode Mode - { - get => GetValue(ModeProperty); - set => SetValue(ModeProperty, value); - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (change.Property == ModeProperty) - { - EnsureThemeVariant(); - } - } - - private void EnsureThemeVariant() - { - var themeVariantResource = Mode == SimpleThemeMode.Dark ? _simpleDark : _simpleLight; - var dict = Resources.MergedDictionaries; - if (dict.Count == 0) - { - dict.Add(themeVariantResource); - } - else - { - dict[0] = themeVariantResource; - } - } + AvaloniaXamlLoader.Load(sp, this); } } diff --git a/src/Avalonia.Themes.Simple/SimpleThemeMode.cs b/src/Avalonia.Themes.Simple/SimpleThemeMode.cs deleted file mode 100644 index 683c751f10..0000000000 --- a/src/Avalonia.Themes.Simple/SimpleThemeMode.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Avalonia.Themes.Simple -{ - public enum SimpleThemeMode - { - Light, - Dark - } -} From be22b361c84c022464a30a0ff963c96f04270df4 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 18 Jan 2023 02:12:52 -0500 Subject: [PATCH 005/185] Add theme variants specific tests --- .../Styling/ResourceDictionaryTests.cs | 8 +- .../Styling/StylesTests.cs | 2 +- .../Styling/ResourceBenchmarks.cs | 2 +- tests/Avalonia.Benchmarks/TestStyles.cs | 19 +- .../AvaloniaPropertyConverterTest.cs | 6 + .../DynamicResourceExtensionTests.cs | 2 +- .../ThemeDictionariesTests.cs | 444 ++++++++++++++++++ .../Xaml/BasicTests.cs | 4 +- .../Xaml/MergeResourceIncludeTests.cs | 103 ++++ .../Xaml/ResourceDictionaryTests.cs | 32 ++ tests/Avalonia.UnitTests/TestServices.cs | 2 +- 11 files changed, 611 insertions(+), 13 deletions(-) create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/ThemeDictionariesTests.cs diff --git a/tests/Avalonia.Base.UnitTests/Styling/ResourceDictionaryTests.cs b/tests/Avalonia.Base.UnitTests/Styling/ResourceDictionaryTests.cs index 86b1b897d4..5527eda6ee 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/ResourceDictionaryTests.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/ResourceDictionaryTests.cs @@ -29,7 +29,7 @@ namespace Avalonia.Base.UnitTests.Styling { "foo", "bar" }, }; - Assert.True(target.TryGetResource("foo", out var result)); + Assert.True(target.TryGetResource("foo", null, out var result)); Assert.Equal("bar", result); } @@ -47,7 +47,7 @@ namespace Avalonia.Base.UnitTests.Styling } }; - Assert.True(target.TryGetResource("foo", out var result)); + Assert.True(target.TryGetResource("foo", null, out var result)); Assert.Equal("bar", result); } @@ -64,7 +64,7 @@ namespace Avalonia.Base.UnitTests.Styling { "foo", "baz" }, }); - Assert.True(target.TryGetResource("foo", out var result)); + Assert.True(target.TryGetResource("foo", null, out var result)); Assert.Equal("bar", result); } @@ -86,7 +86,7 @@ namespace Avalonia.Base.UnitTests.Styling } }; - Assert.True(target.TryGetResource("foo", out var result)); + Assert.True(target.TryGetResource("foo", null, out var result)); Assert.Equal("baz", result); } diff --git a/tests/Avalonia.Base.UnitTests/Styling/StylesTests.cs b/tests/Avalonia.Base.UnitTests/Styling/StylesTests.cs index a6777c9466..c9fc86e205 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StylesTests.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StylesTests.cs @@ -108,7 +108,7 @@ namespace Avalonia.Base.UnitTests.Styling } }; - Assert.True(target.TryGetResource("foo", out var result)); + Assert.True(target.TryGetResource("foo", ThemeVariant.Dark, out var result)); Assert.Equal("bar", result); } } diff --git a/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs b/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs index 59953f457a..bc47e68bc1 100644 --- a/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs +++ b/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs @@ -44,7 +44,7 @@ namespace Avalonia.Benchmarks.Styling return new Styles { preHost, - new TestStyles(50, 3, 5), + new TestStyles(50, 3, 5, 0), postHost }; } diff --git a/tests/Avalonia.Benchmarks/TestStyles.cs b/tests/Avalonia.Benchmarks/TestStyles.cs index be2ad7d072..208f238101 100644 --- a/tests/Avalonia.Benchmarks/TestStyles.cs +++ b/tests/Avalonia.Benchmarks/TestStyles.cs @@ -1,10 +1,11 @@ -using Avalonia.Styling; +using Avalonia.Controls; +using Avalonia.Styling; namespace Avalonia.Benchmarks { public class TestStyles : Styles { - public TestStyles(int childStylesCount, int childInnerStyleCount, int childResourceCount) + public TestStyles(int childStylesCount, int childInnerStyleCount, int childResourceCount, int childThemeResourcesCount) { for (int i = 0; i < childStylesCount; i++) { @@ -18,7 +19,19 @@ namespace Avalonia.Benchmarks { childStyle.Resources.Add($"resource.{i}.{j}.{k}", null); } - + + if (childThemeResourcesCount > 0) + { + ResourceDictionary darkTheme, lightTheme; + childStyle.Resources.ThemeDictionaries[ThemeVariant.Dark] = darkTheme = new ResourceDictionary(); + childStyle.Resources.ThemeDictionaries[ThemeVariant.Light] = lightTheme = new ResourceDictionary(); + for (int k = 0; k < childThemeResourcesCount; k++) + { + darkTheme.Add($"resource.theme.{i}.{j}.{k}", null); + lightTheme.Add($"resource.theme.{i}.{j}.{k}", null); + } + } + childStyles.Add(childStyle); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs index 9c2860eb26..d4d188f584 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs @@ -142,6 +142,12 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters throw new NotImplementedException(); } + public ThemeVariant ThemeVariant + { + get { throw new NotImplementedException(); } + } + public event EventHandler ThemeVariantChanged; + public void DetachStyles() { throw new NotImplementedException(); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs index f2e1a99006..535b96420a 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs @@ -938,7 +938,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions public void AddOwner(IResourceHost owner) => Owner = owner; public void RemoveOwner(IResourceHost owner) => Owner = null; - public bool TryGetResource(object key, out object value) + public bool TryGetResource(object key, ThemeVariant themeVariant, out object value) { RequestedResources.Add(key); value = key; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/ThemeDictionariesTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/ThemeDictionariesTests.cs new file mode 100644 index 0000000000..56040c2186 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/ThemeDictionariesTests.cs @@ -0,0 +1,444 @@ +using Avalonia.Controls; +using Avalonia.Markup.Data; +using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Media; +using Avalonia.Styling; +using Moq; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests; + +public class ThemeDictionariesTests : XamlTestBase +{ + [Fact] + public void DynamicResource_Updated_When_Control_Theme_Changed() + { + var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" + + + + + + Black + + + White + + + + + + +"); + var border = (Border)themeVariantScope.Child!; + + Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); + + themeVariantScope.RequestedThemeVariant = ThemeVariant.Dark; + + Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); + } + + [Fact] + public void DynamicResource_Updated_When_Control_Theme_Changed_No_Xaml() + { + var themeVariantScope = new ThemeVariantScope + { + RequestedThemeVariant = ThemeVariant.Light, + Resources = new ResourceDictionary + { + ThemeDictionaries = + { + [ThemeVariant.Dark] = new ResourceDictionary { ["DemoBackground"] = Brushes.Black }, + [ThemeVariant.Light] = new ResourceDictionary { ["DemoBackground"] = Brushes.White } + } + }, + Child = new Border() + }; + var border = (Border)themeVariantScope.Child!; + border[!Border.BackgroundProperty] = new DynamicResourceExtension("DemoBackground"); + + DelayedBinding.ApplyBindings(border); + + Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); + + themeVariantScope.RequestedThemeVariant = ThemeVariant.Dark; + + Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); + } + + [Fact] + public void Intermediate_DynamicResource_Updated_When_Control_Theme_Changed() + { + var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" + + + + + + Black + + + White + + + + + + + +"); + var border = (Border)themeVariantScope.Child!; + + Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); + + themeVariantScope.RequestedThemeVariant = ThemeVariant.Dark; + + Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); + } + + [Fact] + public void Intermediate_StaticResource_Can_Be_Reached_From_ThemeDictionaries() + { + var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" + + + + + + Black + + + + White + + + + + + + +"); + var border = (Border)themeVariantScope.Child!; + + Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); + + themeVariantScope.RequestedThemeVariant = ThemeVariant.Dark; + + Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); + } + + [Fact(Skip = "Not implemented")] + public void StaticResource_Inside_Of_ThemeDictionaries_Should_Use_Same_Theme_Key() + { + var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" + + + + + + Black + + + White + + + + + + + + + + + + + + + + + + + +"); + var border = (Border)themeVariantScope.Child!; + + Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); + + themeVariantScope.RequestedThemeVariant = ThemeVariant.Dark; + + Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); + } + + [Fact] + public void StaticResource_Outside_Of_Dictionaries_Should_Use_Control_ThemeVariant() + { + using (AvaloniaLocator.EnterScope()) + { + var applicationThemeHost = new Mock(); + applicationThemeHost.SetupGet(h => h.ActualThemeVariant).Returns(ThemeVariant.Dark); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(applicationThemeHost.Object); + + var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" + + + + + + Black + + + White + + + + + + +"); + var border = (Border)themeVariantScope.Child!; + + themeVariantScope.RequestedThemeVariant = ThemeVariant.Light; + Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); + } + } + + [Fact] + public void Inner_ThemeDictionaries_Works_Properly() + { + var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" + + + + + + + Black + + + White + + + + + +"); + var border = (Border)themeVariantScope.Child!; + + Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); + + themeVariantScope.RequestedThemeVariant = ThemeVariant.Dark; + + Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); + } + + [Fact] + public void Inner_Resource_Can_Reference_Parent_ThemeDictionaries() + { + var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" + + + + + + Black + + + White + + + + + + + + + + + + +"); + var border = (Border)themeVariantScope.Child!; + + Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); + + themeVariantScope.RequestedThemeVariant = ThemeVariant.Dark; + + Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); + } + + [Fact] + public void DynamicResource_Can_Access_Resources_Outside_Of_ThemeDictionaries() + { + var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" + + + + + + + + + + + + Black + White + + + + +"); + var border = (Border)themeVariantScope.Child!; + + Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background)!.Color); + + themeVariantScope.RequestedThemeVariant = ThemeVariant.Dark; + + Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); + } + + [Fact] + public void Inner_Dictionary_Does_Not_Affect_Parent_Resources() + { + // It might be a nice feature, but neither Avalonia nor UWP supports it. + // Better to expect this limitation with a unit test. + var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" + + + + Red + + + + + + + + + + Black + + + White + + + + + +"); + var border = (Border)themeVariantScope.Child!; + + Assert.Equal(Colors.Red, ((ISolidColorBrush)border.Background)!.Color); + + themeVariantScope.RequestedThemeVariant = ThemeVariant.Dark; + + Assert.Equal(Colors.Red, ((ISolidColorBrush)border.Background)!.Color); + } + + [Fact] + public void Custom_Theme_Can_Be_Defined_In_ThemeDictionaries() + { + var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" + + + + + + Black + + + White + + + Pink + + + + + + +"); + var border = (Border)themeVariantScope.Child!; + + themeVariantScope.RequestedThemeVariant = new ThemeVariant("Custom"); + + Assert.Equal(Colors.Pink, ((ISolidColorBrush)border.Background)!.Color); + } + + [Fact] + public void Custom_Theme_Fallbacks_To_Inherit_Theme_DynamicResource() + { + var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" + + + + + + Black + + + + + +"); + var border = (Border)themeVariantScope.Child!; + + themeVariantScope.RequestedThemeVariant = new ThemeVariant("Custom", ThemeVariant.Dark); + + Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); + } + + [Fact] + public void Custom_Theme_Fallbacks_To_Inherit_Theme_StaticResource() + { + var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" + + + + + Custom + Dark + + + + + + + + Black + + + + + + +"); + var border = (Border)themeVariantScope.Child!; + + Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index 0cdc9ee3b1..c8be1c6d19 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -448,13 +448,13 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.True(style.Resources.Count > 0); - style.TryGetResource("Brush", out var brush); + style.TryGetResource("Brush", null, out var brush); Assert.NotNull(brush); Assert.IsAssignableFrom(brush); Assert.Equal(Colors.White, ((ISolidColorBrush)brush).Color); - style.TryGetResource("Double", out var d); + style.TryGetResource("Double", null, out var d); Assert.Equal(10.0, d); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs index 92807b2cb9..aa76756069 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; using System.Runtime.CompilerServices; using System.Xml; using Avalonia.Controls; @@ -128,4 +130,105 @@ public class MergeResourceIncludeTests Assert.Equal(Colors.Black, ((ISolidColorBrush)resources["brush5"]!).Color); Assert.Equal(Colors.White, ((ISolidColorBrush)resources["brush6"]!).Color); } + + [Fact] + public void MergeResourceInclude_Works_With_ThemeDictionaries() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources1.xaml"), @" + + + + White + Black + + + Black + White + + +"), + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources2.xaml"), @" + + + + Red + Blue + + + Blue + Red + + +"), + new RuntimeXamlLoaderDocument(@" + + + + + +"), + }; + + var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var resources = Assert.IsType(objects[2]); + Assert.Empty(resources.MergedDictionaries); + + Assert.Equal(Colors.White, Get("brush1", ThemeVariant.Light).Color); + Assert.Equal(Colors.Black, Get("brush2", ThemeVariant.Light).Color); + Assert.Equal(Colors.Black, Get("brush1", ThemeVariant.Dark).Color); + Assert.Equal(Colors.White, Get("brush2", ThemeVariant.Dark).Color); + + Assert.Equal(Colors.Red, Get("brush3", ThemeVariant.Light).Color); + Assert.Equal(Colors.Blue, Get("brush4", ThemeVariant.Light).Color); + Assert.Equal(Colors.Blue, Get("brush3", ThemeVariant.Dark).Color); + Assert.Equal(Colors.Red, Get("brush4", ThemeVariant.Dark).Color); + + ISolidColorBrush Get(string key, ThemeVariant themeVariant) + { + return resources.TryGetResource(key, themeVariant, out var res) ? + (ISolidColorBrush)res! : + throw new KeyNotFoundException(); + } + } + + [Fact] + public void MergeResourceInclude_Fails_With_ThemeDictionaries_Duplicate_Resources() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources1.xaml"), @" + + + + White + + +"), + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources2.xaml"), @" + + + + Black + + +"), + new RuntimeXamlLoaderDocument(@" + + + + + +"), + }; + + Assert.ThrowsAny(() => AvaloniaRuntimeXamlLoader.LoadGroup(documents)); + } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs index d74d85e2bc..6cab83751f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs @@ -276,6 +276,38 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Fact] + public void Closest_Resource_Should_Be_Referenced() + { + using (StyledWindow()) + { + var xaml = @" + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var windowResources = (ResourceDictionary)window.Resources; + var buttonResources = (ResourceDictionary)((Button)window.Content!).Resources; + + var brush = Assert.IsType(windowResources["Red2"]); + Assert.Equal(Colors.Red, brush.Color); + + Assert.False(windowResources.ContainsDeferredKey("Red")); + Assert.False(windowResources.ContainsDeferredKey("Red2")); + + Assert.True(buttonResources.ContainsDeferredKey("Red")); + } + } + private IDisposable StyledWindow(params (string, string)[] assets) { var services = TestServices.StyledWindow.With( diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 40306a4513..339cb1462c 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -155,7 +155,7 @@ namespace Avalonia.UnitTests private static IStyle CreateSimpleTheme() { - return new SimpleTheme { Mode = SimpleThemeMode.Light }; + return new SimpleTheme(); } private static IPlatformRenderInterface CreateRenderInterfaceMock() From 151fe0031a31c34acbd50600bba03a916a5bf20b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 18 Jan 2023 02:13:01 -0500 Subject: [PATCH 006/185] Update control catalog and samples --- samples/ControlCatalog/App.xaml | 24 +++++- samples/ControlCatalog/App.xaml.cs | 48 ++--------- samples/ControlCatalog/MainView.xaml | 19 ++++- samples/ControlCatalog/MainView.xaml.cs | 39 ++++----- samples/ControlCatalog/Models/CatalogTheme.cs | 6 +- .../Pages/DateTimePickerPage.xaml | 30 +++---- .../ControlCatalog/Pages/FlyoutsPage.axaml | 22 +++--- .../Pages/ItemsRepeaterPage.xaml | 2 +- .../ControlCatalog/Pages/SplitViewPage.xaml | 16 ++-- .../ControlCatalog/Pages/TextBlockPage.xaml | 2 +- samples/ControlCatalog/Pages/ThemePage.axaml | 79 +++++++++++++++++++ .../ControlCatalog/Pages/ThemePage.axaml.cs | 37 +++++++++ samples/IntegrationTestApp/App.axaml | 2 +- samples/PlatformSanityChecks/App.xaml | 2 +- samples/Previewer/App.xaml | 2 +- .../HamburgerMenu/HamburgerMenu.xaml | 34 +++++--- samples/Sandbox/App.axaml | 2 +- 17 files changed, 239 insertions(+), 127 deletions(-) create mode 100644 samples/ControlCatalog/Pages/ThemePage.axaml create mode 100644 samples/ControlCatalog/Pages/ThemePage.axaml.cs diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 8f32fa01dd..3b847adcbb 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -6,18 +6,34 @@ x:Class="ControlCatalog.App"> + + + + + #33000000 + #99000000 + #FFE6E6E6 + #FF000000 + + + #33FFFFFF + #99FFFFFF + #FF1F1F1F + #FFFFFFFF + + + #FF0078D7 + #FF005A9E + + - - - - diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 6c99eb5289..d71d51f068 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -16,7 +16,6 @@ namespace ControlCatalog private readonly Styles _themeStylesContainer = new(); private FluentTheme? _fluentTheme; private SimpleTheme? _simpleTheme; - private IResourceDictionary? _fluentBaseLightColors, _fluentBaseDarkColors; private IStyle? _colorPickerFluent, _colorPickerSimple; private IStyle? _dataGridFluent, _dataGridSimple; @@ -33,16 +32,12 @@ namespace ControlCatalog _fluentTheme = new FluentTheme(); _simpleTheme = new SimpleTheme(); - _simpleTheme.Resources.MergedDictionaries.Add((IResourceDictionary)Resources["FluentAccentColors"]!); - _simpleTheme.Resources.MergedDictionaries.Add((IResourceDictionary)Resources["FluentBaseColors"]!); _colorPickerFluent = (IStyle)Resources["ColorPickerFluent"]!; _colorPickerSimple = (IStyle)Resources["ColorPickerSimple"]!; _dataGridFluent = (IStyle)Resources["DataGridFluent"]!; _dataGridSimple = (IStyle)Resources["DataGridSimple"]!; - _fluentBaseLightColors = (IResourceDictionary)Resources["FluentBaseLightColors"]!; - _fluentBaseDarkColors = (IResourceDictionary)Resources["FluentBaseDarkColors"]!; - SetThemeVariant(CatalogTheme.FluentLight); + SetCatalogThemes(CatalogTheme.Fluent); } public override void OnFrameworkInitializationCompleted() @@ -61,19 +56,12 @@ namespace ControlCatalog private CatalogTheme _prevTheme; public static CatalogTheme CurrentTheme => ((App)Current!)._prevTheme; - public static void SetThemeVariant(CatalogTheme theme) + public static void SetCatalogThemes(CatalogTheme theme) { var app = (App)Current!; var prevTheme = app._prevTheme; app._prevTheme = theme; - var shouldReopenWindow = theme switch - { - CatalogTheme.FluentLight => prevTheme is CatalogTheme.SimpleDark or CatalogTheme.SimpleLight, - CatalogTheme.FluentDark => prevTheme is CatalogTheme.SimpleDark or CatalogTheme.SimpleLight, - CatalogTheme.SimpleLight => prevTheme is CatalogTheme.FluentDark or CatalogTheme.FluentLight, - CatalogTheme.SimpleDark => prevTheme is CatalogTheme.FluentDark or CatalogTheme.FluentLight, - _ => throw new ArgumentOutOfRangeException(nameof(theme), theme, null) - }; + var shouldReopenWindow = prevTheme != theme; if (app._themeStylesContainer.Count == 0) { @@ -81,36 +69,16 @@ namespace ControlCatalog app._themeStylesContainer.Add(new Style()); app._themeStylesContainer.Add(new Style()); } - - if (theme == CatalogTheme.FluentLight) - { - app._fluentTheme!.Mode = FluentThemeMode.Light; - app._themeStylesContainer[0] = app._fluentTheme; - app._themeStylesContainer[1] = app._colorPickerFluent!; - app._themeStylesContainer[2] = app._dataGridFluent!; - } - else if (theme == CatalogTheme.FluentDark) + + if (theme == CatalogTheme.Fluent) { - app._fluentTheme!.Mode = FluentThemeMode.Dark; - app._themeStylesContainer[0] = app._fluentTheme; + app._themeStylesContainer[0] = app._fluentTheme!; app._themeStylesContainer[1] = app._colorPickerFluent!; app._themeStylesContainer[2] = app._dataGridFluent!; } - else if (theme == CatalogTheme.SimpleLight) - { - app._simpleTheme!.Mode = SimpleThemeMode.Light; - app._simpleTheme.Resources.MergedDictionaries.Remove(app._fluentBaseDarkColors!); - app._simpleTheme.Resources.MergedDictionaries.Add(app._fluentBaseLightColors!); - app._themeStylesContainer[0] = app._simpleTheme; - app._themeStylesContainer[1] = app._colorPickerSimple!; - app._themeStylesContainer[2] = app._dataGridSimple!; - } - else if (theme == CatalogTheme.SimpleDark) + else if (theme == CatalogTheme.Simple) { - app._simpleTheme!.Mode = SimpleThemeMode.Dark; - app._simpleTheme.Resources.MergedDictionaries.Remove(app._fluentBaseLightColors!); - app._simpleTheme.Resources.MergedDictionaries.Add(app._fluentBaseDarkColors!); - app._themeStylesContainer[0] = app._simpleTheme; + app._themeStylesContainer[0] = app._simpleTheme!; app._themeStylesContainer[1] = app._colorPickerSimple!; app._themeStylesContainer[2] = app._dataGridSimple!; } diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 166b98436e..4eb73632c1 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -165,6 +165,9 @@ + + + @@ -198,14 +201,22 @@ Full + + + Default + Light + Dark + + - FluentLight - FluentDark - SimpleLight - SimpleDark + Fluent + Simple PlatformThemeVariant.Light, - CatalogTheme.FluentDark => PlatformThemeVariant.Dark, - CatalogTheme.SimpleLight => PlatformThemeVariant.Light, - CatalogTheme.SimpleDark => PlatformThemeVariant.Dark, - _ => throw new ArgumentOutOfRangeException() - }); + App.SetCatalogThemes(theme); + } + }; + var themeVariants = this.Get("ThemeVariants"); + themeVariants.SelectedItem = Application.Current!.RequestedThemeVariant; + themeVariants.SelectionChanged += (sender, e) => + { + if (themeVariants.SelectedItem is ThemeVariant themeVariant) + { + Application.Current!.RequestedThemeVariant = themeVariant; } }; @@ -118,25 +119,13 @@ namespace ControlCatalog private void PlatformSettingsOnColorValuesChanged(object? sender, PlatformColorValues e) { - var themes = this.Get("Themes"); - var currentTheme = (CatalogTheme?)themes.SelectedItem ?? CatalogTheme.FluentLight; - var newTheme = (currentTheme, e.ThemeVariant) switch - { - (CatalogTheme.FluentDark, PlatformThemeVariant.Light) => CatalogTheme.FluentLight, - (CatalogTheme.FluentLight, PlatformThemeVariant.Dark) => CatalogTheme.FluentDark, - (CatalogTheme.SimpleDark, PlatformThemeVariant.Light) => CatalogTheme.SimpleLight, - (CatalogTheme.SimpleLight, PlatformThemeVariant.Dark) => CatalogTheme.SimpleDark, - _ => currentTheme - }; - themes.SelectedItem = newTheme; - Application.Current!.Resources["SystemAccentColor"] = e.AccentColor1; Application.Current.Resources["SystemAccentColorDark1"] = ChangeColorLuminosity(e.AccentColor1, -0.3); Application.Current.Resources["SystemAccentColorDark2"] = ChangeColorLuminosity(e.AccentColor1, -0.5); Application.Current.Resources["SystemAccentColorDark3"] = ChangeColorLuminosity(e.AccentColor1, -0.7); - Application.Current.Resources["SystemAccentColorLight1"] = ChangeColorLuminosity(e.AccentColor1, -0.3); - Application.Current.Resources["SystemAccentColorLight2"] = ChangeColorLuminosity(e.AccentColor1, -0.5); - Application.Current.Resources["SystemAccentColorLight3"] = ChangeColorLuminosity(e.AccentColor1, -0.7); + Application.Current.Resources["SystemAccentColorLight1"] = ChangeColorLuminosity(e.AccentColor1, 0.3); + Application.Current.Resources["SystemAccentColorLight2"] = ChangeColorLuminosity(e.AccentColor1, 0.5); + Application.Current.Resources["SystemAccentColorLight3"] = ChangeColorLuminosity(e.AccentColor1, 0.7); static Color ChangeColorLuminosity(Color color, double luminosityFactor) { diff --git a/samples/ControlCatalog/Models/CatalogTheme.cs b/samples/ControlCatalog/Models/CatalogTheme.cs index 37224ed26e..79b3182d20 100644 --- a/samples/ControlCatalog/Models/CatalogTheme.cs +++ b/samples/ControlCatalog/Models/CatalogTheme.cs @@ -2,9 +2,7 @@ { public enum CatalogTheme { - FluentLight, - FluentDark, - SimpleLight, - SimpleDark + Fluent, + Simple } } diff --git a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml index 47753f56b6..fc3ad9b895 100644 --- a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml +++ b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml @@ -15,11 +15,11 @@ Spacing="16"> A simple DatePicker - - + @@ -31,7 +31,7 @@ - @@ -42,12 +42,12 @@ A DatePicker with day formatted and year hidden. - - + @@ -58,15 +58,15 @@ - + A simple TimePicker. - - + @@ -77,7 +77,7 @@ - @@ -88,11 +88,11 @@ A TimePicker with minute increments specified. - - + @@ -105,11 +105,11 @@ A TimePicker using a 12-hour clock. - - + @@ -122,11 +122,11 @@ A TimePicker using a 24-hour clock. - - + diff --git a/samples/ControlCatalog/Pages/FlyoutsPage.axaml b/samples/ControlCatalog/Pages/FlyoutsPage.axaml index c4d0bc3e67..54aa9d1b67 100644 --- a/samples/ControlCatalog/Pages/FlyoutsPage.axaml +++ b/samples/ControlCatalog/Pages/FlyoutsPage.axaml @@ -26,31 +26,31 @@ - - + diff --git a/samples/ControlCatalog/Pages/SplitViewPage.xaml b/samples/ControlCatalog/Pages/SplitViewPage.xaml index 61bfb490b8..2edd895349 100644 --- a/samples/ControlCatalog/Pages/SplitViewPage.xaml +++ b/samples/ControlCatalog/Pages/SplitViewPage.xaml @@ -32,7 +32,7 @@ - SystemControlBackgroundChromeMediumLowBrush + CatalogChromeMediumColor Red Blue Green @@ -48,7 +48,7 @@ - - + @@ -89,11 +89,11 @@ - - - - - + + + + + diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml index 6bb428e2c7..6511e2136a 100644 --- a/samples/ControlCatalog/Pages/TextBlockPage.xaml +++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml @@ -9,7 +9,7 @@ @@ -101,7 +115,7 @@ VerticalAlignment="Center" Background="{DynamicResource TabItemHeaderSelectedPipeFill}" IsVisible="False" - CornerRadius="{DynamicResource ControlCornerRadius}"/> + CornerRadius="4"/> @@ -136,18 +150,18 @@ - - - + + + diff --git a/samples/Sandbox/App.axaml b/samples/Sandbox/App.axaml index f601f9f78f..cf3e5e445a 100644 --- a/samples/Sandbox/App.axaml +++ b/samples/Sandbox/App.axaml @@ -3,6 +3,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="Sandbox.App"> - + From a0d22499cd12614daf2667b410fef62e00b7d747 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 18 Jan 2023 02:39:11 -0500 Subject: [PATCH 007/185] Fix benchmarks build --- .../Controls/ResourceDictionary.cs | 2 +- .../Themes/ThemeBenchmark.cs | 22 +++++-------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index 85e4487ba9..5123803f6e 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -192,7 +192,7 @@ namespace Avalonia.Controls if (_themeDictionary is not null) { IResourceProvider? themeResourceProvider; - if (theme is not null) + if (theme is not null && theme != ThemeVariant.Default) { if (_themeDictionary.TryGetValue(theme, out themeResourceProvider) && themeResourceProvider.TryGetResource(key, theme, out value)) diff --git a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs index 70636d1fe6..7c0a3f8bdf 100644 --- a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs @@ -29,26 +29,16 @@ namespace Avalonia.Benchmarks.Themes } [Benchmark] - [Arguments(FluentThemeMode.Dark)] - [Arguments(FluentThemeMode.Light)] - public bool InitFluentTheme(FluentThemeMode mode) + public bool InitFluentTheme() { - UnitTestApplication.Current.Styles[0] = new FluentTheme() - { - Mode = mode - }; + UnitTestApplication.Current.Styles[0] = new FluentTheme(); return ((IResourceHost)UnitTestApplication.Current).TryGetResource("SystemAccentColor", out _); } [Benchmark] - [Arguments(SimpleThemeMode.Dark)] - [Arguments(SimpleThemeMode.Light)] - public bool InitSimpleTheme(SimpleThemeMode mode) + public bool InitSimpleTheme() { - UnitTestApplication.Current.Styles[0] = new SimpleTheme() - { - Mode = mode - }; + UnitTestApplication.Current.Styles[0] = new SimpleTheme(); return ((IResourceHost)UnitTestApplication.Current).TryGetResource("ThemeAccentColor", out _); } @@ -58,7 +48,7 @@ namespace Avalonia.Benchmarks.Themes [Arguments(typeof(DatePicker))] public object FindFluentControlTheme(Type type) { - _reusableFluentTheme.TryGetResource(type, out var theme); + _reusableFluentTheme.TryGetResource(type, ThemeVariant.Default, out var theme); return theme; } @@ -68,7 +58,7 @@ namespace Avalonia.Benchmarks.Themes [Arguments(typeof(DatePicker))] public object FindSimpleControlTheme(Type type) { - _reusableSimpleTheme.TryGetResource(type, out var theme); + _reusableSimpleTheme.TryGetResource(type, ThemeVariant.Default, out var theme); return theme; } From de325add060f7dc42c813ff8d63b3affcb9def7c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 18 Jan 2023 02:40:05 -0500 Subject: [PATCH 008/185] Fix code related warnings --- src/Avalonia.Base/StyledElement.cs | 2 +- src/Avalonia.Controls/Application.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index d23c585299..5bf022cd51 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -81,7 +81,7 @@ namespace Avalonia defaultValue: ThemeVariant.Light); /// - /// Defines the property. + /// Defines the RequestedThemeVariant property. /// public static readonly StyledProperty RequestedThemeVariantProperty = AvaloniaProperty.Register( diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 58cc02e8c5..3dcba4ded9 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -95,7 +95,7 @@ namespace Avalonia set => SetValue(RequestedThemeVariantProperty, value); } - /// + /// public ThemeVariant ActualThemeVariant { get => GetValue(ActualThemeVariantProperty); From b43bd006b07b087114ea55052c2ffc8c50af2500 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 18 Jan 2023 21:03:09 -0500 Subject: [PATCH 009/185] Fix samples build --- samples/BindingDemo/App.xaml | 7 ------- samples/MobileSandbox/App.xaml | 5 +++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/samples/BindingDemo/App.xaml b/samples/BindingDemo/App.xaml index 5a8e65ed22..84f54293ef 100644 --- a/samples/BindingDemo/App.xaml +++ b/samples/BindingDemo/App.xaml @@ -2,13 +2,6 @@ xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="BindingDemo.App"> - - - - - - - diff --git a/samples/MobileSandbox/App.xaml b/samples/MobileSandbox/App.xaml index 85c97c9dbe..6fb6ae297e 100644 --- a/samples/MobileSandbox/App.xaml +++ b/samples/MobileSandbox/App.xaml @@ -1,8 +1,9 @@ + x:Class="MobileSandbox.App" + RequestedThemeVariant="Dark"> - + From 5ffd961742d888106ba582dc02a71927abc3b92c Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Mon, 23 Jan 2023 18:21:21 +0100 Subject: [PATCH 010/185] Perf: improved text wrapping --- .../TextFormatting/InterWordJustification.cs | 16 +-- .../Media/TextFormatting/TextFormatterImpl.cs | 120 ++++++++++-------- .../Media/TextFormatting/TextLayout.cs | 17 ++- .../Media/TextFormatting/TextLineBreak.cs | 15 +-- .../Media/TextFormatting/TextLineImpl.cs | 8 +- .../TextFormatting/WrappingTextLineBreak.cs | 30 +++++ .../Text/HugeTextLayout.cs | 2 +- .../TextFormatting/TextFormatterTests.cs | 5 +- 8 files changed, 124 insertions(+), 89 deletions(-) create mode 100644 src/Avalonia.Base/Media/TextFormatting/WrappingTextLineBreak.cs diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index efcd866bbc..0d85f3e7c5 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -15,9 +15,7 @@ namespace Avalonia.Media.TextFormatting public override void Justify(TextLine textLine) { - var lineImpl = textLine as TextLineImpl; - - if(lineImpl is null) + if (textLine is not TextLineImpl lineImpl) { return; } @@ -34,14 +32,9 @@ namespace Avalonia.Media.TextFormatting return; } - var textLineBreak = lineImpl.TextLineBreak; - - if (textLineBreak is not null && textLineBreak.TextEndOfLine is not null) + if (lineImpl.TextLineBreak is { TextEndOfLine: not null, IsSplit: false }) { - if (textLineBreak.RemainingRuns is null || textLineBreak.RemainingRuns.Count == 0) - { - return; - } + return; } var breakOportunities = new Queue(); @@ -107,7 +100,8 @@ namespace Avalonia.Media.TextFormatting var glyphIndex = glyphRun.FindGlyphIndex(characterIndex); var glyphInfo = shapedBuffer.GlyphInfos[glyphIndex]; - shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing); + shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, + glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing); } glyphRun.GlyphInfos = shapedBuffer.GlyphInfos; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 7505b9ccdd..c7ec28f16d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -2,7 +2,6 @@ using System; using System.Buffers; using System.Collections.Generic; -using System.Linq; using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utilities; @@ -22,68 +21,55 @@ namespace Avalonia.Media.TextFormatting public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null) { - var textWrapping = paragraphProperties.TextWrapping; - FlowDirection resolvedFlowDirection; TextLineBreak? nextLineBreak = null; - IReadOnlyList? textRuns; var objectPool = FormattingObjectPool.Instance; var fontManager = FontManager.Current; - var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, - out var textEndOfLine, out var textSourceLength); + // we've wrapped the previous line and need to continue wrapping: ignore the textSource and do that instead + if (previousLineBreak is WrappingTextLineBreak wrappingTextLineBreak + && wrappingTextLineBreak.AcquireRemainingRuns() is { } remainingRuns + && paragraphProperties.TextWrapping != TextWrapping.NoWrap) + { + return PerformTextWrapping(remainingRuns, true, firstTextSourceIndex, paragraphWidth, + paragraphProperties, previousLineBreak.FlowDirection, previousLineBreak, objectPool); + } + RentedList? fetchedRuns = null; RentedList? shapedTextRuns = null; - try { - if (previousLineBreak?.RemainingRuns is { } remainingRuns) - { - resolvedFlowDirection = previousLineBreak.FlowDirection; - textRuns = remainingRuns; - nextLineBreak = previousLineBreak; - shapedTextRuns = null; - } - else - { - shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager, - out resolvedFlowDirection); - textRuns = shapedTextRuns; + fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, out var textEndOfLine, + out var textSourceLength); - if (nextLineBreak == null && textEndOfLine != null) - { - nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection); - } - } + shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager, + out var resolvedFlowDirection); - TextLineImpl textLine; + if (nextLineBreak == null && textEndOfLine != null) + { + nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection); + } - switch (textWrapping) + switch (paragraphProperties.TextWrapping) { case TextWrapping.NoWrap: { - // perf note: if textRuns comes from remainingRuns above, it's very likely coming from this class - // which already uses an array: ToArray() won't ever be called in this case - var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray(); - - textLine = new TextLineImpl(textRunArray, firstTextSourceIndex, textSourceLength, + var textLine = new TextLineImpl(shapedTextRuns.ToArray(), firstTextSourceIndex, + textSourceLength, paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); textLine.FinalizeLine(); - break; + return textLine; } case TextWrapping.WrapWithOverflow: case TextWrapping.Wrap: { - textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, - paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager); - break; + return PerformTextWrapping(shapedTextRuns, false, firstTextSourceIndex, paragraphWidth, + paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool); } default: - throw new ArgumentOutOfRangeException(nameof(textWrapping)); + throw new ArgumentOutOfRangeException(nameof(paragraphProperties.TextWrapping)); } - - return textLine; } finally { @@ -108,15 +94,16 @@ namespace Avalonia.Media.TextFormatting for (var i = 0; i < textRuns.Count; i++) { var currentRun = textRuns[i]; + var currentRunLength = currentRun.Length; - if (currentLength + currentRun.Length < length) + if (currentLength + currentRunLength < length) { - currentLength += currentRun.Length; + currentLength += currentRunLength; continue; } - var firstCount = currentRun.Length >= 1 ? i + 1 : i; + var firstCount = currentRunLength >= 1 ? i + 1 : i; if (firstCount > 1) { @@ -128,13 +115,13 @@ namespace Avalonia.Media.TextFormatting var secondCount = textRuns.Count - firstCount; - if (currentLength + currentRun.Length == length) + if (currentLength + currentRunLength == length) { var second = secondCount > 0 ? objectPool.TextRunLists.Rent() : null; if (second != null) { - var offset = currentRun.Length >= 1 ? 1 : 0; + var offset = currentRunLength >= 1 ? 1 : 0; for (var j = 0; j < secondCount; j++) { @@ -653,7 +640,7 @@ namespace Avalonia.Media.TextFormatting /// /// The empty text line. public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, - TextParagraphProperties paragraphProperties, FontManager fontManager) + TextParagraphProperties paragraphProperties) { var flowDirection = paragraphProperties.FlowDirection; var properties = paragraphProperties.DefaultTextRunProperties; @@ -675,21 +662,21 @@ namespace Avalonia.Media.TextFormatting /// Performs text wrapping returns a list of text lines. /// /// + /// Whether can be reused to store the split runs. /// The first text source index. /// The paragraph width. /// The text paragraph properties. /// /// The current line break if the line was explicitly broken. /// A pool used to get reusable formatting objects. - /// The font manager to use. /// The wrapped text line. - private static TextLineImpl PerformTextWrapping(IReadOnlyList textRuns, int firstTextSourceIndex, - double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection, - TextLineBreak? currentLineBreak, FormattingObjectPool objectPool, FontManager fontManager) + private static TextLineImpl PerformTextWrapping(List textRuns, bool canReuseTextRunList, + int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, + FlowDirection resolvedFlowDirection, TextLineBreak? currentLineBreak, FormattingObjectPool objectPool) { if (textRuns.Count == 0) { - return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, fontManager); + return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties); } if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) @@ -819,13 +806,37 @@ namespace Avalonia.Media.TextFormatting try { - var textLineBreak = postSplitRuns?.Count > 0 ? - new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) : - null; + TextLineBreak? textLineBreak; + if (postSplitRuns?.Count > 0) + { + List remainingRuns; + + // reuse the list as much as possible: + // if canReuseTextRunList == true it's coming from previous remaining runs + if (canReuseTextRunList) + { + remainingRuns = textRuns; + remainingRuns.Clear(); + } + else + { + remainingRuns = new List(); + } - if (textLineBreak is null && currentLineBreak?.TextEndOfLine != null) + for (var i = 0; i < postSplitRuns.Count; ++i) + { + remainingRuns.Add(postSplitRuns[i]); + } + + textLineBreak = new WrappingTextLineBreak(null, resolvedFlowDirection, remainingRuns); + } + else if (currentLineBreak?.TextEndOfLine is { } textEndOfLine) { - textLineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection); + textLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection); + } + else + { + textLineBreak = null; } var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength, @@ -833,6 +844,7 @@ namespace Avalonia.Media.TextFormatting textLineBreak); textLine.FinalizeLine(); + return textLine; } finally diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 4923cdbe32..8e85c10bba 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -416,9 +416,11 @@ namespace Avalonia.Media.TextFormatting width = lineWidth; } - if (left > textLine.Start) + var start = textLine.Start; + + if (left > start) { - left = textLine.Start; + left = start; } height += textLine.Height; @@ -427,12 +429,10 @@ namespace Avalonia.Media.TextFormatting private TextLine[] CreateTextLines() { var objectPool = FormattingObjectPool.Instance; - var fontManager = FontManager.Current; if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) { - var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties, - fontManager); + var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties); Bounds = new Rect(0, 0, 0, textLine.Height); @@ -461,7 +461,7 @@ namespace Avalonia.Media.TextFormatting if (previousLine != null && previousLine.NewLineLength > 0) { var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, - _paragraphProperties, fontManager); + _paragraphProperties); textLines.Add(emptyTextLine); @@ -504,7 +504,7 @@ namespace Avalonia.Media.TextFormatting //Fulfill max lines constraint if (MaxLines > 0 && textLines.Count >= MaxLines) { - if (textLine.TextLineBreak?.RemainingRuns is not null) + if (textLine.TextLineBreak is { IsSplit: true }) { textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width)); } @@ -521,8 +521,7 @@ namespace Avalonia.Media.TextFormatting //Make sure the TextLayout always contains at least on empty line if (textLines.Count == 0) { - var textLine = - TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, fontManager); + var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties); textLines.Add(textLine); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs index bf26ac5df4..3b3464b46e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs @@ -1,15 +1,13 @@ -using System.Collections.Generic; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { public class TextLineBreak { - public TextLineBreak(TextEndOfLine? textEndOfLine = null, FlowDirection flowDirection = FlowDirection.LeftToRight, - IReadOnlyList? remainingRuns = null) + public TextLineBreak(TextEndOfLine? textEndOfLine = null, + FlowDirection flowDirection = FlowDirection.LeftToRight, bool isSplit = false) { TextEndOfLine = textEndOfLine; FlowDirection = flowDirection; - RemainingRuns = remainingRuns; + IsSplit = isSplit; } /// @@ -23,8 +21,9 @@ namespace Avalonia.Media.TextFormatting public FlowDirection FlowDirection { get; } /// - /// Get the remaining runs that were split up by the during the formatting process. + /// Gets whether there were remaining runs after this line break, + /// that were split up by the during the formatting process. /// - public IReadOnlyList? RemainingRuns { get; } + public bool IsSplit { get; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index ad3244a3a5..4a7916275d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1285,13 +1285,11 @@ namespace Avalonia.Media.TextFormatting { case ShapedTextRun textRun: { - var properties = textRun.Properties; - var textMetrics = - new TextMetrics(properties.CachedGlyphTypeface, properties.FontRenderingEmSize); + var textMetrics = textRun.TextMetrics; - if (fontRenderingEmSize < properties.FontRenderingEmSize) + if (fontRenderingEmSize < textMetrics.FontRenderingEmSize) { - fontRenderingEmSize = properties.FontRenderingEmSize; + fontRenderingEmSize = textMetrics.FontRenderingEmSize; if (ascent > textMetrics.Ascent) { diff --git a/src/Avalonia.Base/Media/TextFormatting/WrappingTextLineBreak.cs b/src/Avalonia.Base/Media/TextFormatting/WrappingTextLineBreak.cs new file mode 100644 index 0000000000..dacff9e589 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/WrappingTextLineBreak.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Diagnostics; + +namespace Avalonia.Media.TextFormatting +{ + /// Represents a line break that occurred due to wrapping. + internal sealed class WrappingTextLineBreak : TextLineBreak + { + private List? _remainingRuns; + + public WrappingTextLineBreak(TextEndOfLine? textEndOfLine, FlowDirection flowDirection, + List remainingRuns) + : base(textEndOfLine, flowDirection, isSplit: true) + { + Debug.Assert(remainingRuns.Count > 0); + _remainingRuns = remainingRuns; + } + + /// + /// Gets the remaining runs from this line break, and clears them from this line break. + /// + /// A list of text runs. + public List? AcquireRemainingRuns() + { + var remainingRuns = _remainingRuns; + _remainingRuns = null; + return remainingRuns; + } + } +} diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs index 4dad8442de..03b85840a7 100644 --- a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs +++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs @@ -15,7 +15,7 @@ namespace Avalonia.Benchmarks.Text; public class HugeTextLayout : IDisposable { private static readonly Random s_rand = new(); - private static readonly bool s_useSkia = true; + private static readonly bool s_useSkia = false; private readonly IDisposable _app; private readonly string[] _manySmallStrings; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 7822d6624b..e8b87ebe00 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -558,7 +558,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = formatter.FormatLine(textSource, 0, 33, paragraphProperties); - Assert.NotNull(textLine.TextLineBreak?.RemainingRuns); + var remainingRunsLineBreak = Assert.IsType(textLine.TextLineBreak); + var remainingRuns = remainingRunsLineBreak.AcquireRemainingRuns(); + Assert.NotNull(remainingRuns); + Assert.NotEmpty(remainingRuns); } } From 9365bbefa359aa9415060cf0a63d255b426ceaa2 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 27 Jan 2023 15:52:41 -0500 Subject: [PATCH 011/185] Make AnonymousObserver public and LightweightObservableBase internal --- src/Avalonia.Base/Reactive/AnonymousObserver.cs | 7 ++++++- src/Avalonia.Base/Reactive/LightweightObservableBase.cs | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Reactive/AnonymousObserver.cs b/src/Avalonia.Base/Reactive/AnonymousObserver.cs index c2e02ae879..6c458713dc 100644 --- a/src/Avalonia.Base/Reactive/AnonymousObserver.cs +++ b/src/Avalonia.Base/Reactive/AnonymousObserver.cs @@ -1,9 +1,14 @@ using System; +using System.Runtime.CompilerServices; using System.Threading.Tasks; namespace Avalonia.Reactive; -internal class AnonymousObserver : IObserver +/// +/// Class to create an instance from delegate-based implementations of the On* methods. +/// +/// The type of the elements in the sequence. +public class AnonymousObserver : IObserver { private static readonly Action ThrowsOnError = ex => throw ex; private static readonly Action NoOpCompleted = () => { }; diff --git a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs index 263109972f..cf20f20172 100644 --- a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs +++ b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs @@ -14,7 +14,7 @@ namespace Avalonia.Reactive /// usage. This class provides a more lightweight base for some internal observable types /// in the Avalonia framework. /// - public abstract class LightweightObservableBase : IObservable + internal abstract class LightweightObservableBase : IObservable { private Exception? _error; private List>? _observers = new List>(); From 0065a962acf3fa8738b53f8426fad9d0c3bb762e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 28 Jan 2023 13:08:24 +0100 Subject: [PATCH 012/185] Move ItemsRepeater to separate assembly. --- Avalonia.Desktop.slnf | 1 + Avalonia.sln | 13 +++++++++++ samples/ControlCatalog/ControlCatalog.csproj | 1 + .../Avalonia.Controls.ItemsRepeater.csproj | 20 +++++++++++++++++ .../Controls}/ElementFactory.cs | 0 .../Controls}/IElementFactory.cs | 0 .../Controls}/ItemTemplateWrapper.cs | 0 .../Controls}/ItemsRepeater.cs | 0 .../ItemsRepeaterElementClearingEventArgs.cs | 0 ...emsRepeaterElementIndexChangedEventArgs.cs | 0 .../ItemsRepeaterElementPreparedEventArgs.cs | 0 .../Controls}/RecyclePool.cs | 0 .../Controls}/RecyclingElementFactory.cs | 0 .../Controls}/RepeaterLayoutContext.cs | 0 .../Controls}/UniqueIdElementPool.cs | 0 .../Controls}/ViewManager.cs | 10 ++++----- .../Controls}/ViewportManager.cs | 10 ++++----- .../Controls}/VirtualizationInfo.cs | 0 .../Layout/AttachedLayout.cs | 0 .../Layout/ElementManager.cs | 0 .../Layout/FlowLayoutAlgorithm.cs | 0 .../Layout/IFlowLayoutAlgorithmDelegates.cs | 0 .../Layout/LayoutContext.cs | 0 .../Layout/LayoutContextAdapter.cs | 0 .../Layout/NonVirtualizingLayout.cs | 0 .../Layout/NonVirtualizingLayoutContext.cs | 0 .../Layout/NonVirtualizingStackLayout.cs | 0 .../Layout/OrientationBasedMeasures.cs | 0 .../Layout/StackLayout.cs | 5 +++-- .../Layout/StackLayoutState.cs | 0 .../Layout/UniformGridLayout.cs | 0 .../Layout/UniformGridLayoutState.cs | 0 .../Layout/Utils/ListUtils.cs | 0 .../Layout}/UvBounds.cs | 0 .../Layout}/UvMeasure.cs | 0 .../Layout/VirtualLayoutContextAdapter.cs | 0 .../Layout/VirtualizingLayout.cs | 0 .../Layout/VirtualizingLayoutContext.cs | 0 .../Layout}/WrapItem.cs | 0 .../Layout}/WrapLayout.cs | 0 .../Layout}/WrapLayoutState.cs | 0 .../Properties/AssemblyInfo.cs | 4 ++++ .../Avalonia.Controls.csproj | 1 + src/Avalonia.Controls/StackPanel.cs | 4 ++-- .../VirtualizingStackPanel.cs | 2 +- ...valoniaXamlIlDataContextTypeTransformer.cs | 3 +-- .../AvaloniaXamlIlWellKnownTypes.cs | 2 -- ...ia.Controls.ItemsRepeater.UnitTests.csproj | 22 +++++++++++++++++++ .../ItemsRepeaterTests.cs | 0 .../NonVirtualizingStackLayoutTests.cs | 0 .../Avalonia.LeakTests.csproj | 1 + .../Avalonia.Markup.Xaml.UnitTests.csproj | 1 + 52 files changed, 81 insertions(+), 19 deletions(-) create mode 100644 src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj rename src/{Avalonia.Controls/Repeater => Avalonia.Controls.ItemsRepeater/Controls}/ElementFactory.cs (100%) rename src/{Avalonia.Controls/Repeater => Avalonia.Controls.ItemsRepeater/Controls}/IElementFactory.cs (100%) rename src/{Avalonia.Controls/Repeater => Avalonia.Controls.ItemsRepeater/Controls}/ItemTemplateWrapper.cs (100%) rename src/{Avalonia.Controls/Repeater => Avalonia.Controls.ItemsRepeater/Controls}/ItemsRepeater.cs (100%) rename src/{Avalonia.Controls/Repeater => Avalonia.Controls.ItemsRepeater/Controls}/ItemsRepeaterElementClearingEventArgs.cs (100%) rename src/{Avalonia.Controls/Repeater => Avalonia.Controls.ItemsRepeater/Controls}/ItemsRepeaterElementIndexChangedEventArgs.cs (100%) rename src/{Avalonia.Controls/Repeater => Avalonia.Controls.ItemsRepeater/Controls}/ItemsRepeaterElementPreparedEventArgs.cs (100%) rename src/{Avalonia.Controls/Repeater => Avalonia.Controls.ItemsRepeater/Controls}/RecyclePool.cs (100%) rename src/{Avalonia.Controls/Repeater => Avalonia.Controls.ItemsRepeater/Controls}/RecyclingElementFactory.cs (100%) rename src/{Avalonia.Controls/Repeater => Avalonia.Controls.ItemsRepeater/Controls}/RepeaterLayoutContext.cs (100%) rename src/{Avalonia.Controls/Repeater => Avalonia.Controls.ItemsRepeater/Controls}/UniqueIdElementPool.cs (100%) rename src/{Avalonia.Controls/Repeater => Avalonia.Controls.ItemsRepeater/Controls}/ViewManager.cs (99%) rename src/{Avalonia.Controls/Repeater => Avalonia.Controls.ItemsRepeater/Controls}/ViewportManager.cs (98%) rename src/{Avalonia.Controls/Repeater => Avalonia.Controls.ItemsRepeater/Controls}/VirtualizationInfo.cs (100%) rename src/{Avalonia.Base => Avalonia.Controls.ItemsRepeater}/Layout/AttachedLayout.cs (100%) rename src/{Avalonia.Base => Avalonia.Controls.ItemsRepeater}/Layout/ElementManager.cs (100%) rename src/{Avalonia.Base => Avalonia.Controls.ItemsRepeater}/Layout/FlowLayoutAlgorithm.cs (100%) rename src/{Avalonia.Base => Avalonia.Controls.ItemsRepeater}/Layout/IFlowLayoutAlgorithmDelegates.cs (100%) rename src/{Avalonia.Base => Avalonia.Controls.ItemsRepeater}/Layout/LayoutContext.cs (100%) rename src/{Avalonia.Base => Avalonia.Controls.ItemsRepeater}/Layout/LayoutContextAdapter.cs (100%) rename src/{Avalonia.Base => Avalonia.Controls.ItemsRepeater}/Layout/NonVirtualizingLayout.cs (100%) rename src/{Avalonia.Base => Avalonia.Controls.ItemsRepeater}/Layout/NonVirtualizingLayoutContext.cs (100%) rename src/{Avalonia.Base => Avalonia.Controls.ItemsRepeater}/Layout/NonVirtualizingStackLayout.cs (100%) rename src/{Avalonia.Base => Avalonia.Controls.ItemsRepeater}/Layout/OrientationBasedMeasures.cs (100%) rename src/{Avalonia.Base => Avalonia.Controls.ItemsRepeater}/Layout/StackLayout.cs (98%) rename src/{Avalonia.Base => Avalonia.Controls.ItemsRepeater}/Layout/StackLayoutState.cs (100%) rename src/{Avalonia.Base => Avalonia.Controls.ItemsRepeater}/Layout/UniformGridLayout.cs (100%) rename src/{Avalonia.Base => Avalonia.Controls.ItemsRepeater}/Layout/UniformGridLayoutState.cs (100%) rename src/{Avalonia.Base => Avalonia.Controls.ItemsRepeater}/Layout/Utils/ListUtils.cs (100%) rename src/{Avalonia.Base/Layout/WrapLayout => Avalonia.Controls.ItemsRepeater/Layout}/UvBounds.cs (100%) rename src/{Avalonia.Base/Layout/WrapLayout => Avalonia.Controls.ItemsRepeater/Layout}/UvMeasure.cs (100%) rename src/{Avalonia.Base => Avalonia.Controls.ItemsRepeater}/Layout/VirtualLayoutContextAdapter.cs (100%) rename src/{Avalonia.Base => Avalonia.Controls.ItemsRepeater}/Layout/VirtualizingLayout.cs (100%) rename src/{Avalonia.Base => Avalonia.Controls.ItemsRepeater}/Layout/VirtualizingLayoutContext.cs (100%) rename src/{Avalonia.Base/Layout/WrapLayout => Avalonia.Controls.ItemsRepeater/Layout}/WrapItem.cs (100%) rename src/{Avalonia.Base/Layout/WrapLayout => Avalonia.Controls.ItemsRepeater/Layout}/WrapLayout.cs (100%) rename src/{Avalonia.Base/Layout/WrapLayout => Avalonia.Controls.ItemsRepeater/Layout}/WrapLayoutState.cs (100%) create mode 100644 src/Avalonia.Controls.ItemsRepeater/Properties/AssemblyInfo.cs create mode 100644 tests/Avalonia.Controls.ItemsRepeater.UnitTests/Avalonia.Controls.ItemsRepeater.UnitTests.csproj rename tests/{Avalonia.Controls.UnitTests => Avalonia.Controls.ItemsRepeater.UnitTests}/ItemsRepeaterTests.cs (100%) rename tests/{Avalonia.Base.UnitTests/Layout => Avalonia.Controls.ItemsRepeater.UnitTests}/NonVirtualizingStackLayoutTests.cs (100%) diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 2f034bd083..3acd4bf9f2 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -15,6 +15,7 @@ "src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj", "src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj", "src\\Avalonia.Controls.DataGrid\\Avalonia.Controls.DataGrid.csproj", + "src\\Avalonia.Controls.ItemsRepeater\\Avalonia.Controls.ItemsRepeater.csproj", "src\\Avalonia.Controls\\Avalonia.Controls.csproj", "src\\Avalonia.DesignerSupport\\Avalonia.DesignerSupport.csproj", "src\\Avalonia.Desktop\\Avalonia.Desktop.csproj", diff --git a/Avalonia.sln b/Avalonia.sln index ce9a37a3ce..525e01c891 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -233,6 +233,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\R EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater", "src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj", "{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater.UnitTests", "tests\Avalonia.Controls.ItemsRepeater.UnitTests\Avalonia.Controls.ItemsRepeater.UnitTests.csproj", "{F4E36AA8-814E-4704-BC07-291F70F45193}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -548,6 +552,14 @@ Global {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.Build.0 = Release|Any CPU + {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Release|Any CPU.Build.0 = Release|Any CPU + {F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -613,6 +625,7 @@ Global {90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098} {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 18f0dd16ba..c223bfe1a9 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -26,6 +26,7 @@ + diff --git a/src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj b/src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj new file mode 100644 index 0000000000..1ec0ee33a7 --- /dev/null +++ b/src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj @@ -0,0 +1,20 @@ + + + net6.0;netstandard2.0 + Avalonia.Controls.ItemsRepeater + Avalonia + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls/Repeater/ElementFactory.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ElementFactory.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/ElementFactory.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ElementFactory.cs diff --git a/src/Avalonia.Controls/Repeater/IElementFactory.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/IElementFactory.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/IElementFactory.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/IElementFactory.cs diff --git a/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemTemplateWrapper.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ItemTemplateWrapper.cs diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/ItemsRepeater.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementClearingEventArgs.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementClearingEventArgs.cs diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementIndexChangedEventArgs.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementIndexChangedEventArgs.cs diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementPreparedEventArgs.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementPreparedEventArgs.cs diff --git a/src/Avalonia.Controls/Repeater/RecyclePool.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/RecyclePool.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/RecyclePool.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/RecyclePool.cs diff --git a/src/Avalonia.Controls/Repeater/RecyclingElementFactory.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/RecyclingElementFactory.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/RecyclingElementFactory.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/RecyclingElementFactory.cs diff --git a/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/RepeaterLayoutContext.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/RepeaterLayoutContext.cs diff --git a/src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/UniqueIdElementPool.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/UniqueIdElementPool.cs diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ViewManager.cs similarity index 99% rename from src/Avalonia.Controls/Repeater/ViewManager.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ViewManager.cs index 2dff18cd04..2d302a95dd 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls.ItemsRepeater/Controls/ViewManager.cs @@ -256,7 +256,7 @@ namespace Avalonia.Controls public void UpdatePin(Control element, bool addPin) { - var parent = element.VisualParent; + var parent = element.GetVisualParent(); var child = (Visual)element; while (parent != null) @@ -283,7 +283,7 @@ namespace Avalonia.Controls } child = parent; - parent = child.VisualParent; + parent = child.GetVisualParent(); } } @@ -656,7 +656,7 @@ namespace Avalonia.Controls // that handlers can walk up the tree in case they want to find their IndexPath in the // nested case. var children = repeater.Children; - if (element.VisualParent != repeater) + if (element.GetVisualParent() != repeater) { children.Add(element); } @@ -701,7 +701,7 @@ namespace Avalonia.Controls if (FocusManager.Instance?.Current is Visual child) { - var parent = child.VisualParent; + var parent = child.GetVisualParent(); var owner = _owner; // Find out if the focused element belongs to one of our direct @@ -722,7 +722,7 @@ namespace Avalonia.Controls } child = parent; - parent = child?.VisualParent; + parent = child?.GetVisualParent(); } } diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ViewportManager.cs similarity index 98% rename from src/Avalonia.Controls/Repeater/ViewportManager.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ViewportManager.cs index 56e0cda8fe..336fb2d228 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls.ItemsRepeater/Controls/ViewportManager.cs @@ -67,7 +67,7 @@ namespace Avalonia.Controls // be a direct child of ours, or even an indirect child. We need to walk up the tree starting // from anchorElement to figure out what child of ours (if any) to use as the suggested element. var child = anchorElement; - var parent = child.VisualParent as Control; + var parent = child.GetVisualParent() as Control; while (parent != null) { @@ -78,7 +78,7 @@ namespace Avalonia.Controls } child = parent; - parent = parent.VisualParent as Control; + parent = parent.GetVisualParent() as Control; } } } @@ -369,11 +369,11 @@ namespace Avalonia.Controls private Control? GetImmediateChildOfRepeater(Control descendant) { var targetChild = descendant; - var parent = (Control?)descendant.VisualParent; + var parent = (Control?)descendant.GetVisualParent(); while (parent != null && parent != _owner) { targetChild = parent; - parent = (Control?)parent.VisualParent; + parent = (Control?)parent.GetVisualParent(); } if (parent == null) @@ -471,7 +471,7 @@ namespace Avalonia.Controls break; } - parent = parent.VisualParent; + parent = parent.GetVisualParent(); } if (!_managingViewportDisabled) diff --git a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/VirtualizationInfo.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/VirtualizationInfo.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/VirtualizationInfo.cs diff --git a/src/Avalonia.Base/Layout/AttachedLayout.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/AttachedLayout.cs similarity index 100% rename from src/Avalonia.Base/Layout/AttachedLayout.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/AttachedLayout.cs diff --git a/src/Avalonia.Base/Layout/ElementManager.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/ElementManager.cs similarity index 100% rename from src/Avalonia.Base/Layout/ElementManager.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/ElementManager.cs diff --git a/src/Avalonia.Base/Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/FlowLayoutAlgorithm.cs similarity index 100% rename from src/Avalonia.Base/Layout/FlowLayoutAlgorithm.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/FlowLayoutAlgorithm.cs diff --git a/src/Avalonia.Base/Layout/IFlowLayoutAlgorithmDelegates.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/IFlowLayoutAlgorithmDelegates.cs similarity index 100% rename from src/Avalonia.Base/Layout/IFlowLayoutAlgorithmDelegates.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/IFlowLayoutAlgorithmDelegates.cs diff --git a/src/Avalonia.Base/Layout/LayoutContext.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/LayoutContext.cs similarity index 100% rename from src/Avalonia.Base/Layout/LayoutContext.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/LayoutContext.cs diff --git a/src/Avalonia.Base/Layout/LayoutContextAdapter.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/LayoutContextAdapter.cs similarity index 100% rename from src/Avalonia.Base/Layout/LayoutContextAdapter.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/LayoutContextAdapter.cs diff --git a/src/Avalonia.Base/Layout/NonVirtualizingLayout.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingLayout.cs similarity index 100% rename from src/Avalonia.Base/Layout/NonVirtualizingLayout.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingLayout.cs diff --git a/src/Avalonia.Base/Layout/NonVirtualizingLayoutContext.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingLayoutContext.cs similarity index 100% rename from src/Avalonia.Base/Layout/NonVirtualizingLayoutContext.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingLayoutContext.cs diff --git a/src/Avalonia.Base/Layout/NonVirtualizingStackLayout.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingStackLayout.cs similarity index 100% rename from src/Avalonia.Base/Layout/NonVirtualizingStackLayout.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingStackLayout.cs diff --git a/src/Avalonia.Base/Layout/OrientationBasedMeasures.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/OrientationBasedMeasures.cs similarity index 100% rename from src/Avalonia.Base/Layout/OrientationBasedMeasures.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/OrientationBasedMeasures.cs diff --git a/src/Avalonia.Base/Layout/StackLayout.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/StackLayout.cs similarity index 98% rename from src/Avalonia.Base/Layout/StackLayout.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/StackLayout.cs index e9093cc146..5e2b2b8574 100644 --- a/src/Avalonia.Base/Layout/StackLayout.cs +++ b/src/Avalonia.Controls.ItemsRepeater/Layout/StackLayout.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Specialized; +using Avalonia.Controls; using Avalonia.Data; using Avalonia.Logging; @@ -25,13 +26,13 @@ namespace Avalonia.Layout /// Defines the property. /// public static readonly StyledProperty OrientationProperty = - AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical); + StackPanel.OrientationProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty SpacingProperty = - AvaloniaProperty.Register(nameof(Spacing)); + StackPanel.SpacingProperty.AddOwner(); private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures(); diff --git a/src/Avalonia.Base/Layout/StackLayoutState.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/StackLayoutState.cs similarity index 100% rename from src/Avalonia.Base/Layout/StackLayoutState.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/StackLayoutState.cs diff --git a/src/Avalonia.Base/Layout/UniformGridLayout.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/UniformGridLayout.cs similarity index 100% rename from src/Avalonia.Base/Layout/UniformGridLayout.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/UniformGridLayout.cs diff --git a/src/Avalonia.Base/Layout/UniformGridLayoutState.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/UniformGridLayoutState.cs similarity index 100% rename from src/Avalonia.Base/Layout/UniformGridLayoutState.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/UniformGridLayoutState.cs diff --git a/src/Avalonia.Base/Layout/Utils/ListUtils.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/Utils/ListUtils.cs similarity index 100% rename from src/Avalonia.Base/Layout/Utils/ListUtils.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/Utils/ListUtils.cs diff --git a/src/Avalonia.Base/Layout/WrapLayout/UvBounds.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/UvBounds.cs similarity index 100% rename from src/Avalonia.Base/Layout/WrapLayout/UvBounds.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/UvBounds.cs diff --git a/src/Avalonia.Base/Layout/WrapLayout/UvMeasure.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/UvMeasure.cs similarity index 100% rename from src/Avalonia.Base/Layout/WrapLayout/UvMeasure.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/UvMeasure.cs diff --git a/src/Avalonia.Base/Layout/VirtualLayoutContextAdapter.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/VirtualLayoutContextAdapter.cs similarity index 100% rename from src/Avalonia.Base/Layout/VirtualLayoutContextAdapter.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/VirtualLayoutContextAdapter.cs diff --git a/src/Avalonia.Base/Layout/VirtualizingLayout.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/VirtualizingLayout.cs similarity index 100% rename from src/Avalonia.Base/Layout/VirtualizingLayout.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/VirtualizingLayout.cs diff --git a/src/Avalonia.Base/Layout/VirtualizingLayoutContext.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/VirtualizingLayoutContext.cs similarity index 100% rename from src/Avalonia.Base/Layout/VirtualizingLayoutContext.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/VirtualizingLayoutContext.cs diff --git a/src/Avalonia.Base/Layout/WrapLayout/WrapItem.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/WrapItem.cs similarity index 100% rename from src/Avalonia.Base/Layout/WrapLayout/WrapItem.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/WrapItem.cs diff --git a/src/Avalonia.Base/Layout/WrapLayout/WrapLayout.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/WrapLayout.cs similarity index 100% rename from src/Avalonia.Base/Layout/WrapLayout/WrapLayout.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/WrapLayout.cs diff --git a/src/Avalonia.Base/Layout/WrapLayout/WrapLayoutState.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/WrapLayoutState.cs similarity index 100% rename from src/Avalonia.Base/Layout/WrapLayout/WrapLayoutState.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/WrapLayoutState.cs diff --git a/src/Avalonia.Controls.ItemsRepeater/Properties/AssemblyInfo.cs b/src/Avalonia.Controls.ItemsRepeater/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..d8023b0853 --- /dev/null +++ b/src/Avalonia.Controls.ItemsRepeater/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using Avalonia.Metadata; + +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Layout")] diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 42c577041a..3195c38eef 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index aa63ac975e..9362dab553 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -22,13 +22,13 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty SpacingProperty = - StackLayout.SpacingProperty.AddOwner(); + AvaloniaProperty.Register(nameof(Spacing)); /// /// Defines the property. /// public static readonly StyledProperty OrientationProperty = - StackLayout.OrientationProperty.AddOwner(); + AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical); /// /// Defines the property. diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index c5276741b6..634efbd699 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty OrientationProperty = - StackLayout.OrientationProperty.AddOwner(); + StackPanel.OrientationProperty.AddOwner(); /// /// Defines the property. diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs index 574d46e737..f7c3089214 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs @@ -79,8 +79,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { var parentType = parentObject.Type.GetClrType(); - if (context.GetAvaloniaTypes().ItemsControl.IsDirectlyAssignableFrom(parentType) - || context.GetAvaloniaTypes().ItemsRepeater.IsDirectlyAssignableFrom(parentType)) + if (context.GetAvaloniaTypes().ItemsControl.IsDirectlyAssignableFrom(parentType)) { inferredDataContextTypeNode = InferDataContextOfPresentedItem(context, on, parentObject); } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 0b61316603..8f487b5552 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -55,7 +55,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType DataTemplate { get; } public IXamlType IDataTemplate { get; } public IXamlType ItemsControl { get; } - public IXamlType ItemsRepeater { get; } public IXamlType ReflectionBindingExtension { get; } public IXamlType RelativeSource { get; } @@ -179,7 +178,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers DataTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.DataTemplate"); IDataTemplate = cfg.TypeSystem.GetType("Avalonia.Controls.Templates.IDataTemplate"); ItemsControl = cfg.TypeSystem.GetType("Avalonia.Controls.ItemsControl"); - ItemsRepeater = cfg.TypeSystem.GetType("Avalonia.Controls.ItemsRepeater"); ReflectionBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension"); RelativeSource = cfg.TypeSystem.GetType("Avalonia.Data.RelativeSource"); UInt = cfg.TypeSystem.GetType("System.UInt32"); diff --git a/tests/Avalonia.Controls.ItemsRepeater.UnitTests/Avalonia.Controls.ItemsRepeater.UnitTests.csproj b/tests/Avalonia.Controls.ItemsRepeater.UnitTests/Avalonia.Controls.ItemsRepeater.UnitTests.csproj new file mode 100644 index 0000000000..6f9815757e --- /dev/null +++ b/tests/Avalonia.Controls.ItemsRepeater.UnitTests/Avalonia.Controls.ItemsRepeater.UnitTests.csproj @@ -0,0 +1,22 @@ + + + net6.0 + Library + true + + + + + + + + + + + + + + + + + diff --git a/tests/Avalonia.Controls.UnitTests/ItemsRepeaterTests.cs b/tests/Avalonia.Controls.ItemsRepeater.UnitTests/ItemsRepeaterTests.cs similarity index 100% rename from tests/Avalonia.Controls.UnitTests/ItemsRepeaterTests.cs rename to tests/Avalonia.Controls.ItemsRepeater.UnitTests/ItemsRepeaterTests.cs diff --git a/tests/Avalonia.Base.UnitTests/Layout/NonVirtualizingStackLayoutTests.cs b/tests/Avalonia.Controls.ItemsRepeater.UnitTests/NonVirtualizingStackLayoutTests.cs similarity index 100% rename from tests/Avalonia.Base.UnitTests/Layout/NonVirtualizingStackLayoutTests.cs rename to tests/Avalonia.Controls.ItemsRepeater.UnitTests/NonVirtualizingStackLayoutTests.cs diff --git a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj index 4572f7ae7c..15815c81b6 100644 --- a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj +++ b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj @@ -11,6 +11,7 @@ + diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj index fa4957c24c..ade6010bae 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -17,6 +17,7 @@ + From 58c23a7efcfb7ddcf4e63b3a8e475be5115528cf Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 30 Jan 2023 09:57:55 +0100 Subject: [PATCH 013/185] fix: misc compilation errors after merge PRs #10078 #10120 --- .../Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj | 3 --- src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs | 6 +++--- .../Avalonia.Win32.Interop/Wpf/WritableBitmapSurface.cs | 2 +- src/Windows/Avalonia.Win32/Avalonia.Win32.csproj | 3 +++ 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj b/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj index cc8a40e2d4..60bb75a342 100644 --- a/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj +++ b/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj @@ -13,9 +13,6 @@ - - - diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index 76fc4fa21d..bfc50db874 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -12,6 +12,7 @@ using Avalonia.Input.Raw; using Avalonia.Layout; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Key = Avalonia.Input.Key; using KeyEventArgs = System.Windows.Input.KeyEventArgs; using MouseButton = System.Windows.Input.MouseButton; @@ -90,8 +91,7 @@ namespace Avalonia.Win32.Interop.Wpf public IRenderer CreateRenderer(IRenderRoot root) { - var mgr = new PlatformRenderInterfaceContextManager(null); - return new ImmediateRenderer((Visual)root, () => mgr.CreateRenderTarget(_surfaces), mgr); + return new CompositingRenderer(root, Win32Platform.Compositor, () => _surfaces); } public void Dispose() @@ -134,7 +134,7 @@ namespace Avalonia.Win32.Interop.Wpf drawingContext.DrawImage(ImageSource, new System.Windows.Rect(0, 0, ActualWidth, ActualHeight)); } - void ITopLevelImpl.Invalidate(Rect rect) => InvalidateVisual(); + void ITopLevelImpl.SetInputRoot(IInputRoot inputRoot) => _inputRoot = inputRoot; diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WritableBitmapSurface.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WritableBitmapSurface.cs index 05fa9d9426..04b4a53580 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WritableBitmapSurface.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WritableBitmapSurface.cs @@ -25,7 +25,7 @@ namespace Avalonia.Win32.Interop.Wpf if (_bitmap == null || _bitmap.PixelWidth != (int) size.Width || _bitmap.PixelHeight != (int) size.Height) { _bitmap = new WriteableBitmap((int) size.Width, (int) size.Height, dpi.X, dpi.Y, - PixelFormats.Bgra32, null); + System.Windows.Media.PixelFormats.Bgra32, null); } return new LockedFramebuffer(_impl, _bitmap, dpi); } diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index eb51b7fd07..b7dca78845 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -27,4 +27,7 @@ $(NoWarn);CA1416 + + + From 04ed8e9eef3be1603d7476e4a25da647d80a4ad7 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 30 Jan 2023 10:11:49 +0100 Subject: [PATCH 014/185] fix: xml comment --- src/Avalonia.Base/Data/InstancedBinding.cs | 2 +- .../Metadata/InheritDataTypeFromItemsAttribute.cs | 4 ++-- src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs | 2 +- src/Avalonia.Controls/TreeViewItem.cs | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Data/InstancedBinding.cs b/src/Avalonia.Base/Data/InstancedBinding.cs index 00e5c3d8e6..c09c31632e 100644 --- a/src/Avalonia.Base/Data/InstancedBinding.cs +++ b/src/Avalonia.Base/Data/InstancedBinding.cs @@ -23,7 +23,7 @@ namespace Avalonia.Data /// The priority of the binding. /// /// This constructor can be used to create any type of binding and as such requires an - /// as the binding source because this is the only binding + /// as the binding source because this is the only binding /// source which can be used for all binding modes. If you wish to create an instance with /// something other than a subject, use one of the static creation methods on this class. /// diff --git a/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs b/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs index 6bb820d214..fac8cd8737 100644 --- a/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs +++ b/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs @@ -25,9 +25,9 @@ public sealed class InheritDataTypeFromItemsAttribute : Attribute /// The name of the property whose item type should be used on the target property. /// public string AncestorItemsProperty { get; } - + /// - /// The ancestor type to be used in a lookup for the . + /// The ancestor type to be used in a lookup for the . /// If null, the declaring type of the target property is used. /// public Type? AncestorType { get; set; } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs index 6d30358119..112dd436de 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// The point in global coordinates. /// True if the point hits the node's geometry; otherwise false. /// - /// This method does not recurse to child s, if you want + /// This method does not recurse to child s, if you want /// to hit test children they must be hit tested manually. /// bool HitTest(Point p); diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 022e1a74b1..e6b1016696 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -264,9 +264,9 @@ namespace Avalonia.Controls Dispatcher.UIThread.Post(this.BringIntoView); // must use the Dispatcher, otherwise the TreeView doesn't scroll } } - + /// - /// Invoked when the event occurs in the header. + /// Invoked when the event occurs in the header. /// protected virtual void OnHeaderDoubleTapped(TappedEventArgs e) { From a2c7f67c418fe93308ba4b4250de3e71a8cd79f9 Mon Sep 17 00:00:00 2001 From: Dmitry Zhelnin Date: Sat, 28 Jan 2023 15:45:08 +0300 Subject: [PATCH 015/185] throw XamlParseException if there are duplicate property setters in style or control theme --- .../AvaloniaXamlIlCompiler.cs | 1 + .../AvaloniaXamlIlDuplicateSettersChecker.cs | 44 +++++++++++++++++++ .../Xaml/XamlIlTests.cs | 41 +++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDuplicateSettersChecker.cs diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index aaaee39b0d..197815f9a0 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -49,6 +49,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions InsertBefore( new AvaloniaXamlIlControlThemeTransformer(), new AvaloniaXamlIlSelectorTransformer(), + new AvaloniaXamlIlDuplicateSettersChecker(), new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), new AvaloniaXamlIlBindingPathParser(), new AvaloniaXamlIlPropertyPathTransformer(), diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDuplicateSettersChecker.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDuplicateSettersChecker.cs new file mode 100644 index 0000000000..4f92fde3f0 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDuplicateSettersChecker.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Linq; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; + +class AvaloniaXamlIlDuplicateSettersChecker : IXamlAstTransformer +{ + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (node is not XamlAstObjectNode objectNode) + { + return node; + } + + var fullName = objectNode.Type.GetClrType().FullName; + if (fullName is not ("Avalonia.Styling.Style" or "Avalonia.Styling.ControlTheme")) + { + return node; + } + + var properties = objectNode.Children + .OfType() + .Where(n => n.Type.GetClrType().Name == "Setter") + .SelectMany(setter => + setter.Children.OfType() + .Where(c => c.Property.GetClrProperty().Name == "Property")) + .Select(p => p.Values[0]) + .OfType() + .Select(x => x.Text); + var index = new HashSet(); + foreach (var property in properties) + { + if (!index.Add(property)) + { + throw new XamlParseException($"Duplicate setter encountered for property '{property}'", node); + } + } + + return node; + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs index f42f787117..be2cae8ec4 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs @@ -334,6 +334,47 @@ namespace Avalonia.Markup.Xaml.UnitTests var parsed = (Button)AvaloniaRuntimeXamlLoader.Load(document); Assert.Equal(Colors.Blue, ((ISolidColorBrush)parsed.Background!).Color); } + + [Fact] + public void Style_Parser_Throws_For_Duplicate_Setter() + { + var xaml = @" + + + + + +"; + AssertThrows(() => AvaloniaRuntimeXamlLoader.Load(xaml, typeof(XamlIlTests).Assembly, designMode: true), + e => e.Message.StartsWith("Duplicate setter encountered for property 'Height'")); + } + + [Fact] + public void Control_Theme_Parser_Throws_For_Duplicate_Setter() + { + var xaml = @" + + + + + + + + + + +"; + AssertThrows(() => AvaloniaRuntimeXamlLoader.Load(xaml, typeof(XamlIlTests).Assembly, designMode: true), + e => e.Message.StartsWith("Duplicate setter encountered for property 'Height'")); + } } public class XamlIlBugTestsEventHandlerCodeBehind : Window From 0b726e1eff92cfbf37ed57cda1f42d7951cfce54 Mon Sep 17 00:00:00 2001 From: Dmitry Zhelnin Date: Mon, 30 Jan 2023 11:10:53 +0300 Subject: [PATCH 016/185] add Style and ControlTheme to WellKnownTypes --- .../Transformers/AvaloniaXamlIlControlThemeTransformer.cs | 3 ++- .../Transformers/AvaloniaXamlIlDuplicateSettersChecker.cs | 5 +++-- .../Transformers/AvaloniaXamlIlSelectorTransformer.cs | 3 ++- .../Transformers/AvaloniaXamlIlWellKnownTypes.cs | 4 ++++ 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs index 1338dc7248..51fe58d1c9 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs @@ -11,7 +11,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (!(node is XamlAstObjectNode on && on.Type.GetClrType().FullName == "Avalonia.Styling.ControlTheme")) + if (node is not XamlAstObjectNode on || + !context.GetAvaloniaTypes().ControlTheme.IsAssignableFrom(on.Type.GetClrType())) return node; // Check if we've already transformed this node. diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDuplicateSettersChecker.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDuplicateSettersChecker.cs index 4f92fde3f0..4ab9594cd8 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDuplicateSettersChecker.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDuplicateSettersChecker.cs @@ -15,8 +15,9 @@ class AvaloniaXamlIlDuplicateSettersChecker : IXamlAstTransformer return node; } - var fullName = objectNode.Type.GetClrType().FullName; - if (fullName is not ("Avalonia.Styling.Style" or "Avalonia.Styling.ControlTheme")) + var nodeType = objectNode.Type.GetClrType(); + if (!context.GetAvaloniaTypes().Style.IsAssignableFrom(nodeType) && + !context.GetAvaloniaTypes().ControlTheme.IsAssignableFrom(nodeType)) { return node; } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs index 078e23bc02..edddc5424a 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs @@ -19,7 +19,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (!(node is XamlAstObjectNode on && on.Type.GetClrType().FullName == "Avalonia.Styling.Style")) + if (node is not XamlAstObjectNode on || + !context.GetAvaloniaTypes().Style.IsAssignableFrom(on.Type.GetClrType())) return node; var pn = on.Children.OfType() diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 0aa3dda693..c55e116cb3 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -111,6 +111,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlMethod ResourceDictionaryDeferredAdd { get; } public IXamlType UriKind { get; } public IXamlConstructor UriConstructor { get; } + public IXamlType Style { get; } + public IXamlType ControlTheme { get; } public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg) { @@ -248,6 +250,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers XamlIlTypes.Object)); UriKind = cfg.TypeSystem.GetType("System.UriKind"); UriConstructor = Uri.GetConstructor(new List() { cfg.WellKnownTypes.String, UriKind }); + Style = cfg.TypeSystem.GetType("Avalonia.Styling.Style"); + ControlTheme = cfg.TypeSystem.GetType("Avalonia.Styling.ControlTheme"); } } From 281d30747dfe36de94a0ddec2ebcd2d8455663c8 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 30 Jan 2023 22:45:22 -0500 Subject: [PATCH 017/185] Switch TreeViewItem.IsExpanded to a StyledProperty Includes other small fixes and updates --- src/Avalonia.Controls/TreeViewItem.cs | 29 ++++++++++----------------- 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 022e1a74b1..5674874c01 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; -using Avalonia.Controls.Generators; using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Threading; @@ -22,11 +22,10 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty IsExpandedProperty = - AvaloniaProperty.RegisterDirect( + public static readonly StyledProperty IsExpandedProperty = + AvaloniaProperty.Register( nameof(IsExpanded), - o => o.IsExpanded, - (o, v) => o.IsExpanded = v); + defaultBindingMode: BindingMode.TwoWay); /// /// Defines the property. @@ -46,7 +45,6 @@ namespace Avalonia.Controls private TreeView? _treeView; private Control? _header; - private bool _isExpanded; private int _level; private bool _templateApplied; private bool _deferredBringIntoViewFlag; @@ -68,8 +66,8 @@ namespace Avalonia.Controls /// public bool IsExpanded { - get { return _isExpanded; } - set { SetAndRaise(IsExpandedProperty, ref _isExpanded, value); } + get => GetValue(IsExpandedProperty); + set => SetValue(IsExpandedProperty, value); } /// @@ -77,8 +75,8 @@ namespace Avalonia.Controls /// public bool IsSelected { - get { return GetValue(IsSelectedProperty); } - set { SetValue(IsSelectedProperty, value); } + get => GetValue(IsSelectedProperty); + set => SetValue(IsSelectedProperty, value); } /// @@ -86,8 +84,8 @@ namespace Avalonia.Controls /// public int Level { - get { return _level; } - private set { SetAndRaise(LevelProperty, ref _level, value); } + get => _level; + private set => SetAndRaise(LevelProperty, ref _level, value); } internal TreeView? TreeViewOwner => _treeView; @@ -115,11 +113,6 @@ namespace Avalonia.Controls } } - protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) - { - base.OnDetachedFromLogicalTree(e); - } - protected virtual void OnRequestBringIntoView(RequestBringIntoViewEventArgs e) { if (e.TargetObject == this) @@ -266,7 +259,7 @@ namespace Avalonia.Controls } /// - /// Invoked when the event occurs in the header. + /// Invoked when the event occurs in the header. /// protected virtual void OnHeaderDoubleTapped(TappedEventArgs e) { From 3f6cb8224120e3fb6d2940e35b8c2e43b237bd92 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 31 Jan 2023 10:45:44 +0100 Subject: [PATCH 018/185] Add failing test --- .../Media/TextFormatting/TextFormatterImpl.cs | 101 +++++++++--------- .../Media/TextFormatting/TextLineImpl.cs | 14 ++- .../TextFormatting/SingleBufferTextSource.cs | 8 +- .../TextFormatting/TextFormatterTests.cs | 11 +- 4 files changed, 79 insertions(+), 55 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 7505b9ccdd..c8db70dbfe 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -60,25 +60,25 @@ namespace Avalonia.Media.TextFormatting switch (textWrapping) { case TextWrapping.NoWrap: - { - // perf note: if textRuns comes from remainingRuns above, it's very likely coming from this class - // which already uses an array: ToArray() won't ever be called in this case - var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray(); + { + // perf note: if textRuns comes from remainingRuns above, it's very likely coming from this class + // which already uses an array: ToArray() won't ever be called in this case + var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray(); - textLine = new TextLineImpl(textRunArray, firstTextSourceIndex, textSourceLength, - paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); + textLine = new TextLineImpl(textRunArray, firstTextSourceIndex, textSourceLength, + paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); - textLine.FinalizeLine(); + textLine.FinalizeLine(); - break; - } + break; + } case TextWrapping.WrapWithOverflow: case TextWrapping.Wrap: - { - textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, - paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager); - break; - } + { + textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, + paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager); + break; + } default: throw new ArgumentOutOfRangeException(nameof(textWrapping)); } @@ -249,49 +249,49 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { case UnshapedTextRun shapeableRun: - { - groupedRuns.Clear(); - groupedRuns.Add(shapeableRun); + { + groupedRuns.Clear(); + groupedRuns.Add(shapeableRun); - var text = shapeableRun.Text; - var properties = shapeableRun.Properties; + var text = shapeableRun.Text; + var properties = shapeableRun.Properties; - while (index + 1 < processedRuns.Count) - { - if (processedRuns[index + 1] is not UnshapedTextRun nextRun) + while (index + 1 < processedRuns.Count) { + if (processedRuns[index + 1] is not UnshapedTextRun nextRun) + { + break; + } + + if (shapeableRun.BidiLevel == nextRun.BidiLevel + && TryJoinContiguousMemories(text, nextRun.Text, out var joinedText) + && CanShapeTogether(properties, nextRun.Properties)) + { + groupedRuns.Add(nextRun); + index++; + shapeableRun = nextRun; + text = joinedText; + continue; + } + break; } - if (shapeableRun.BidiLevel == nextRun.BidiLevel - && TryJoinContiguousMemories(text, nextRun.Text, out var joinedText) - && CanShapeTogether(properties, nextRun.Properties)) - { - groupedRuns.Add(nextRun); - index++; - shapeableRun = nextRun; - text = joinedText; - continue; - } + var shaperOptions = new TextShaperOptions( + properties.CachedGlyphTypeface, + properties.FontRenderingEmSize, shapeableRun.BidiLevel, properties.CultureInfo, + paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); + + ShapeTogether(groupedRuns, text, shaperOptions, textShaper, shapedRuns); break; } - - var shaperOptions = new TextShaperOptions( - properties.CachedGlyphTypeface, - properties.FontRenderingEmSize, shapeableRun.BidiLevel, properties.CultureInfo, - paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); - - ShapeTogether(groupedRuns, text, shaperOptions, textShaper, shapedRuns); - - break; - } default: - { - shapedRuns.Add(currentRun); + { + shapedRuns.Add(currentRun); - break; - } + break; + } } } } @@ -712,7 +712,7 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { case ShapedTextRun: - { + { var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); while (lineBreaker.MoveNext(out var lineBreak)) @@ -754,7 +754,7 @@ namespace Avalonia.Media.TextFormatting break; } - while (lineBreaker.MoveNext(out lineBreak) && index < textRuns.Count) + while (lineBreaker.MoveNext(out lineBreak)) { currentPosition += lineBreak.PositionWrap; @@ -780,6 +780,11 @@ namespace Avalonia.Media.TextFormatting currentPosition = currentLength + lineBreak.PositionWrap; } + //if (currentPosition == 0 && measuredLength > 0) + //{ + // currentPosition = measuredLength; + //} + breakFound = true; break; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index ad3244a3a5..e97f431412 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -172,9 +172,21 @@ namespace Avalonia.Media.TextFormatting distance -= Start; + var firstRunIndex = 0; + + if (_textRuns[firstRunIndex] is TextEndOfLine) + { + firstRunIndex++; + } + + if(firstRunIndex >= _textRuns.Length) + { + return new CharacterHit(FirstTextSourceIndex); + } + if (distance <= 0) { - var firstRun = _textRuns[0]; + var firstRun = _textRuns[firstRunIndex]; return GetRunCharacterHit(firstRun, FirstTextSourceIndex, 0); } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs index f963277397..7837749adf 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs @@ -7,25 +7,27 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { private readonly string _text; private readonly GenericTextRunProperties _defaultGenericPropertiesRunProperties; + private readonly bool _addEndOfParagraph; - public SingleBufferTextSource(string text, GenericTextRunProperties defaultProperties) + public SingleBufferTextSource(string text, GenericTextRunProperties defaultProperties, bool addEndOfParagraph = false) { _text = text; _defaultGenericPropertiesRunProperties = defaultProperties; + _addEndOfParagraph = addEndOfParagraph; } public TextRun GetTextRun(int textSourceIndex) { if (textSourceIndex >= _text.Length) { - return null; + return _addEndOfParagraph ? new TextEndOfParagraph() : null; } var runText = _text.AsMemory(textSourceIndex); if (runText.IsEmpty) { - return null; + return _addEndOfParagraph ? new TextEndOfParagraph() : null; } return new TextCharacters(runText, _defaultGenericPropertiesRunProperties); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 7822d6624b..dd04d26bb5 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -242,10 +242,16 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var defaultProperties = new GenericTextRunProperties(Typeface.Default); - var textSource = new SingleBufferTextSource(text, defaultProperties); + var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.WrapWithOverflow); + + var textSource = new SingleBufferTextSource("ABCDEFHFFHFJHKHFK", defaultProperties, true); var formatter = new TextFormatterImpl(); + var line = formatter.FormatLine(textSource, 0, 33, paragraphProperties); + + textSource = new SingleBufferTextSource(text, defaultProperties); + var numberOfLines = 0; var currentPosition = 0; @@ -253,8 +259,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting while (currentPosition < text.Length) { var textLine = - formatter.FormatLine(textSource, currentPosition, 1, - new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.WrapWithOverflow)); + formatter.FormatLine(textSource, currentPosition, 1, paragraphProperties); if (text.Length - currentPosition > expectedCharactersPerLine) { From 743e1fa04060a168b7ec779b78f6003c2e908dd3 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 31 Jan 2023 10:46:51 +0100 Subject: [PATCH 019/185] Fix WrapWithOverflow handling for textlines that contain TextEndOfParagraph runs --- .../Media/TextFormatting/TextFormatterImpl.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index c8db70dbfe..2e20d6844e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -780,10 +780,10 @@ namespace Avalonia.Media.TextFormatting currentPosition = currentLength + lineBreak.PositionWrap; } - //if (currentPosition == 0 && measuredLength > 0) - //{ - // currentPosition = measuredLength; - //} + if (currentPosition == 0 && measuredLength > 0) + { + currentPosition = measuredLength; + } breakFound = true; From 5e13c5b59aabd279e398f5a35514fdbe9a74726e Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Sun, 29 Jan 2023 17:09:37 +0100 Subject: [PATCH 020/185] Implemented layout and render time graph overlays --- samples/GpuInterop/MainWindow.axaml.cs | 5 +- samples/RenderDemo/MainWindow.xaml | 14 ++ samples/RenderDemo/MainWindow.xaml.cs | 23 ++- .../ViewModels/MainWindowViewModel.cs | 45 +++-- src/Avalonia.Base/Layout/LayoutManager.cs | 20 +- .../Composition/CompositingRenderer.cs | 31 +-- .../Server/DiagnosticTextRenderer.cs | 66 +++++++ .../Composition/Server/FpsCounter.cs | 58 +++--- .../Composition/Server/FrameTimeGraph.cs | 176 ++++++++++++++++++ .../Server/ServerCompositionTarget.cs | 131 ++++++++++--- src/Avalonia.Base/Rendering/IRenderer.cs | 11 +- .../Rendering/LayoutPassTiming.cs | 11 ++ .../Rendering/RendererDebugOverlays.cs | 35 ++++ .../Rendering/RendererDiagnostics.cs | 57 ++++++ .../Utilities/StopwatchHelper.cs | 19 ++ src/Avalonia.Base/composition-schema.xml | 4 +- src/Avalonia.Controls/TopLevel.cs | 64 ++++++- .../Diagnostics/ViewModels/MainViewModel.cs | 109 ++++++----- .../Diagnostics/ViewModels/ViewModelBase.cs | 4 +- .../Diagnostics/Views/MainView.xaml | 24 ++- .../Input/MouseDeviceTests.cs | 14 +- .../Input/PointerOverTests.cs | 22 +-- tests/Avalonia.Base.UnitTests/VisualTests.cs | 8 +- tests/Avalonia.Benchmarks/NullRenderer.cs | 50 ----- .../ButtonTests.cs | 20 +- .../ContextMenuTests.cs | 2 +- .../DesktopStyleApplicationLifetimeTests.cs | 3 + .../FlyoutTests.cs | 2 +- .../Primitives/PopupTests.cs | 2 +- .../WindowBaseTests.cs | 21 ++- .../WindowTests.cs | 12 +- tests/Avalonia.LeakTests/ControlTests.cs | 43 +---- .../MockWindowingPlatform.cs | 5 +- tests/Avalonia.UnitTests/NullRenderer.cs | 57 ++++++ tests/Avalonia.UnitTests/RendererMocks.cs | 15 ++ tests/Avalonia.UnitTests/TestRoot.cs | 4 +- 36 files changed, 867 insertions(+), 320 deletions(-) create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs create mode 100644 src/Avalonia.Base/Rendering/LayoutPassTiming.cs create mode 100644 src/Avalonia.Base/Rendering/RendererDebugOverlays.cs create mode 100644 src/Avalonia.Base/Rendering/RendererDiagnostics.cs create mode 100644 src/Avalonia.Base/Utilities/StopwatchHelper.cs delete mode 100644 tests/Avalonia.Benchmarks/NullRenderer.cs create mode 100644 tests/Avalonia.UnitTests/NullRenderer.cs create mode 100644 tests/Avalonia.UnitTests/RendererMocks.cs diff --git a/samples/GpuInterop/MainWindow.axaml.cs b/samples/GpuInterop/MainWindow.axaml.cs index 8fc8926783..7cf0bc00e2 100644 --- a/samples/GpuInterop/MainWindow.axaml.cs +++ b/samples/GpuInterop/MainWindow.axaml.cs @@ -1,6 +1,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.Rendering; namespace GpuInterop { @@ -8,9 +9,9 @@ namespace GpuInterop { public MainWindow() { - this.InitializeComponent(); + InitializeComponent(); this.AttachDevTools(); - this.Renderer.DrawFps = true; + Renderer.Diagnostics.DebugOverlays = RendererDebugOverlays.Fps; } private void InitializeComponent() diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index 20cc6c43e1..e1dbd20b07 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -26,6 +26,20 @@ IsHitTestVisible="False" /> + + + + + + + + + + diff --git a/samples/RenderDemo/MainWindow.xaml.cs b/samples/RenderDemo/MainWindow.xaml.cs index 877eb8016a..d85f3b6051 100644 --- a/samples/RenderDemo/MainWindow.xaml.cs +++ b/samples/RenderDemo/MainWindow.xaml.cs @@ -1,7 +1,9 @@ using System; +using System.Linq.Expressions; using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.Rendering; using RenderDemo.ViewModels; using MiniMvvm; @@ -11,13 +13,26 @@ namespace RenderDemo { public MainWindow() { - this.InitializeComponent(); + InitializeComponent(); this.AttachDevTools(); var vm = new MainWindowViewModel(); - vm.WhenAnyValue(x => x.DrawDirtyRects).Subscribe(x => Renderer.DrawDirtyRects = x); - vm.WhenAnyValue(x => x.DrawFps).Subscribe(x => Renderer.DrawFps = x); - this.DataContext = vm; + + void BindOverlay(Expression> expr, RendererDebugOverlays overlay) + => vm.WhenAnyValue(expr).Subscribe(x => + { + var diagnostics = Renderer.Diagnostics; + diagnostics.DebugOverlays = x ? + diagnostics.DebugOverlays | overlay : + diagnostics.DebugOverlays & ~overlay; + }); + + BindOverlay(x => x.DrawDirtyRects, RendererDebugOverlays.DirtyRects); + BindOverlay(x => x.DrawFps, RendererDebugOverlays.Fps); + BindOverlay(x => x.DrawLayoutTimeGraph, RendererDebugOverlays.LayoutTimeGraph); + BindOverlay(x => x.DrawRenderTimeGraph, RendererDebugOverlays.RenderTimeGraph); + + DataContext = vm; } private void InitializeComponent() diff --git a/samples/RenderDemo/ViewModels/MainWindowViewModel.cs b/samples/RenderDemo/ViewModels/MainWindowViewModel.cs index 19917c20df..caaa899b56 100644 --- a/samples/RenderDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/RenderDemo/ViewModels/MainWindowViewModel.cs @@ -1,49 +1,66 @@ -using System.Reactive; -using System.Threading.Tasks; +using System.Threading.Tasks; using MiniMvvm; namespace RenderDemo.ViewModels { public class MainWindowViewModel : ViewModelBase { - private bool drawDirtyRects = false; - private bool drawFps = true; - private double width = 800; - private double height = 600; + private bool _drawDirtyRects; + private bool _drawFps = true; + private bool _drawLayoutTimeGraph; + private bool _drawRenderTimeGraph; + private double _width = 800; + private double _height = 600; public MainWindowViewModel() { ToggleDrawDirtyRects = MiniCommand.Create(() => DrawDirtyRects = !DrawDirtyRects); ToggleDrawFps = MiniCommand.Create(() => DrawFps = !DrawFps); + ToggleDrawLayoutTimeGraph = MiniCommand.Create(() => DrawLayoutTimeGraph = !DrawLayoutTimeGraph); + ToggleDrawRenderTimeGraph = MiniCommand.Create(() => DrawRenderTimeGraph = !DrawRenderTimeGraph); ResizeWindow = MiniCommand.CreateFromTask(ResizeWindowAsync); } public bool DrawDirtyRects { - get => drawDirtyRects; - set => this.RaiseAndSetIfChanged(ref drawDirtyRects, value); + get => _drawDirtyRects; + set => RaiseAndSetIfChanged(ref _drawDirtyRects, value); } public bool DrawFps { - get => drawFps; - set => this.RaiseAndSetIfChanged(ref drawFps, value); + get => _drawFps; + set => RaiseAndSetIfChanged(ref _drawFps, value); + } + + public bool DrawLayoutTimeGraph + { + get => _drawLayoutTimeGraph; + set => RaiseAndSetIfChanged(ref _drawLayoutTimeGraph, value); + } + + public bool DrawRenderTimeGraph + { + get => _drawRenderTimeGraph; + set => RaiseAndSetIfChanged(ref _drawRenderTimeGraph, value); } public double Width { - get => width; - set => this.RaiseAndSetIfChanged(ref width, value); + get => _width; + set => RaiseAndSetIfChanged(ref _width, value); } public double Height { - get => height; - set => this.RaiseAndSetIfChanged(ref height, value); + get => _height; + set => RaiseAndSetIfChanged(ref _height, value); } public MiniCommand ToggleDrawDirtyRects { get; } public MiniCommand ToggleDrawFps { get; } + public MiniCommand ToggleDrawLayoutTimeGraph { get; } + public MiniCommand ToggleDrawRenderTimeGraph { get; } public MiniCommand ResizeWindow { get; } private async Task ResizeWindowAsync() diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs index 242d70821a..c4742bcba4 100644 --- a/src/Avalonia.Base/Layout/LayoutManager.cs +++ b/src/Avalonia.Base/Layout/LayoutManager.cs @@ -3,8 +3,9 @@ using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using Avalonia.Logging; +using Avalonia.Rendering; using Avalonia.Threading; -using Avalonia.VisualTree; +using Avalonia.Utilities; #nullable enable @@ -24,6 +25,7 @@ namespace Avalonia.Layout private bool _disposed; private bool _queued; private bool _running; + private int _totalPassCount; public LayoutManager(ILayoutRoot owner) { @@ -33,6 +35,8 @@ namespace Avalonia.Layout public virtual event EventHandler? LayoutUpdated; + internal Action? LayoutPassTimed { get; set; } + /// public virtual void InvalidateMeasure(Layoutable control) { @@ -116,10 +120,9 @@ namespace Avalonia.Layout if (!_running) { - Stopwatch? stopwatch = null; - const LogEventLevel timingLogLevel = LogEventLevel.Information; - bool captureTiming = Logger.IsEnabled(timingLogLevel, LogArea.Layout); + var captureTiming = LayoutPassTimed is not null || Logger.IsEnabled(timingLogLevel, LogArea.Layout); + var startingTimestamp = 0L; if (captureTiming) { @@ -129,8 +132,7 @@ namespace Avalonia.Layout _toMeasure.Count, _toArrange.Count); - stopwatch = new Stopwatch(); - stopwatch.Start(); + startingTimestamp = Stopwatch.GetTimestamp(); } _toMeasure.BeginLoop(MaxPasses); @@ -139,6 +141,7 @@ namespace Avalonia.Layout try { _running = true; + ++_totalPassCount; for (var pass = 0; pass < MaxPasses; ++pass) { @@ -160,9 +163,10 @@ namespace Avalonia.Layout if (captureTiming) { - stopwatch!.Stop(); + var elapsed = StopwatchHelper.GetElapsedTime(startingTimestamp); + LayoutPassTimed?.Invoke(new LayoutPassTiming(_totalPassCount, elapsed)); - Logger.TryGet(timingLogLevel, LogArea.Layout)?.Log(this, "Layout pass finished in {Time}", stopwatch.Elapsed); + Logger.TryGet(timingLogLevel, LogArea.Layout)?.Log(this, "Layout pass finished in {Time}", elapsed); } } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index c5d7ec61e0..668d650ffd 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -1,15 +1,12 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Numerics; -using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Collections.Pooled; using Avalonia.Media; -using Avalonia.Rendering.Composition.Drawing; -using Avalonia.Rendering.Composition.Server; -using Avalonia.Threading; using Avalonia.VisualTree; // Special license applies License.md @@ -38,6 +35,9 @@ public class CompositingRenderer : IRendererWithCompositor /// public bool RenderOnlyOnRenderThread { get; set; } = true; + /// + public RendererDiagnostics Diagnostics { get; } + public CompositingRenderer(IRenderRoot root, Compositor compositor, Func> surfaces) { _root = root; @@ -46,20 +46,21 @@ public class CompositingRenderer : IRendererWithCompositor CompositionTarget = compositor.CreateCompositionTarget(surfaces); CompositionTarget.Root = ((Visual)root).AttachToCompositor(compositor); _update = Update; + Diagnostics = new RendererDiagnostics(); + Diagnostics.PropertyChanged += OnDiagnosticsPropertyChanged; } - /// - public bool DrawFps + private void OnDiagnosticsPropertyChanged(object? sender, PropertyChangedEventArgs e) { - get => CompositionTarget.DrawFps; - set => CompositionTarget.DrawFps = value; - } - - /// - public bool DrawDirtyRects - { - get => CompositionTarget.DrawDirtyRects; - set => CompositionTarget.DrawDirtyRects = value; + switch (e.PropertyName) + { + case nameof(RendererDiagnostics.DebugOverlays): + CompositionTarget.DebugOverlays = Diagnostics.DebugOverlays; + break; + case nameof(RendererDiagnostics.LastLayoutPassTiming): + CompositionTarget.LastLayoutPassTiming = Diagnostics.LastLayoutPassTiming; + break; + } } /// diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs new file mode 100644 index 0000000000..6e97f00681 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs @@ -0,0 +1,66 @@ +using System; +using Avalonia.Media; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server +{ + /// + /// A class used to render diagnostic strings (only!), with caching of ASCII glyph runs. + /// + internal sealed class DiagnosticTextRenderer + { + private const char FirstChar = (char)32; + private const char LastChar = (char)126; + + private readonly GlyphRun[] _runs = new GlyphRun[LastChar - FirstChar + 1]; + + public double MaxHeight { get; } + + public DiagnosticTextRenderer(IGlyphTypeface typeface, double fontRenderingEmSize) + { + var chars = new char[LastChar - FirstChar + 1]; + for (var c = FirstChar; c <= LastChar; c++) + { + var index = c - FirstChar; + chars[index] = c; + var glyph = typeface.GetGlyph(c); + var run = new GlyphRun(typeface, fontRenderingEmSize, chars.AsMemory(index, 1), new[] { glyph }); + _runs[index] = run; + MaxHeight = Math.Max(run.Size.Height, MaxHeight); + } + } + + public Size MeasureAsciiText(ReadOnlySpan text) + { + var width = 0.0; + var height = 0.0; + + foreach (var c in text) + { + var effectiveChar = c is >= FirstChar and <= LastChar ? c : ' '; + var run = _runs[effectiveChar - FirstChar]; + width += run.Size.Width; + height = Math.Max(height, run.Size.Height); + } + + return new Size(width, height); + } + + public void DrawAsciiText(IDrawingContextImpl context, ReadOnlySpan text, IBrush foreground) + { + var offset = 0.0; + var originalTransform = context.Transform; + + foreach (var c in text) + { + var effectiveChar = c is >= FirstChar and <= LastChar ? c : ' '; + var run = _runs[effectiveChar - FirstChar]; + context.Transform = originalTransform * Matrix.CreateTranslation(offset, 0.0); + context.DrawGlyphRun(foreground, run.PlatformImpl); + offset += run.Size.Width; + } + + context.Transform = originalTransform; + } + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs index ebab39cee8..03bd965fa8 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -1,11 +1,8 @@ using System; using System.Diagnostics; using System.Globalization; -using System.Linq; using Avalonia.Media; -using Avalonia.Media.TextFormatting; using Avalonia.Platform; -using Avalonia.Utilities; // Special license applies License.md @@ -17,26 +14,18 @@ namespace Avalonia.Rendering.Composition.Server; internal class FpsCounter { private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); + private readonly DiagnosticTextRenderer _textRenderer; + private int _framesThisSecond; private int _totalFrames; private int _fps; private TimeSpan _lastFpsUpdate; - const int FirstChar = 32; - const int LastChar = 126; - // ASCII chars - private GlyphRun[] _runs = new GlyphRun[LastChar - FirstChar + 1]; - - public FpsCounter(IGlyphTypeface typeface) - { - for (var c = FirstChar; c <= LastChar; c++) - { - var s = new string((char)c, 1); - var glyph = typeface.GetGlyph((uint)(s[0])); - _runs[c - FirstChar] = new GlyphRun(typeface, 18, s.AsMemory(), new ushort[] { glyph }); - } - } - public void FpsTick() => _framesThisSecond++; + public FpsCounter(DiagnosticTextRenderer textRenderer) + => _textRenderer = textRenderer; + + public void FpsTick() + => _framesThisSecond++; public void RenderFps(IDrawingContextImpl context, string aux) { @@ -53,27 +42,24 @@ internal class FpsCounter _lastFpsUpdate = now; } - var fpsLine = FormattableString.Invariant($"Frame #{_totalFrames:00000000} FPS: {_fps:000} ") + aux; - double width = 0; - double height = 0; - foreach (var ch in fpsLine) - { - var run = _runs[ch - FirstChar]; - width += run.Size.Width; - height = Math.Max(height, run.Size.Height); - } +#if NET6_0_OR_GREATER + var fpsLine = string.Create(CultureInfo.InvariantCulture, $"Frame #{_totalFrames:00000000} FPS: {_fps:000} {aux}"); +#else + var fpsLine = FormattableString.Invariant($"Frame #{_totalFrames:00000000} FPS: {_fps:000} {aux}"); +#endif - var rect = new Rect(0, 0, width + 3, height + 3); + var size = _textRenderer.MeasureAsciiText(fpsLine.AsSpan()); + var rect = new Rect(0.0, 0.0, size.Width + 3.0, size.Height + 3.0); context.DrawRectangle(Brushes.Black, null, rect); - double offset = 0; - foreach (var ch in fpsLine) - { - var run = _runs[ch - FirstChar]; - context.Transform = Matrix.CreateTranslation(offset, 0); - context.DrawGlyphRun(Brushes.White, run.PlatformImpl); - offset += run.Size.Width; - } + _textRenderer.DrawAsciiText(context, fpsLine.AsSpan(), Brushes.White); + } + + public void Reset() + { + _framesThisSecond = 0; + _totalFrames = 0; + _fps = 0; } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs b/src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs new file mode 100644 index 0000000000..c926c75c52 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs @@ -0,0 +1,176 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Runtime.CompilerServices; +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server; + +/// +/// Represents a simple time graph for diagnostics purpose, used to show layout and render times. +/// +internal sealed class FrameTimeGraph +{ + private const double HeaderPadding = 2.0; + + private readonly IPlatformRenderInterface _renderInterface; + private readonly ImmutableSolidColorBrush _borderBrush; + private readonly ImmutablePen _graphPen; + private readonly double[] _frameValues; + private readonly Size _size; + private readonly Size _headerSize; + private readonly Size _graphSize; + private readonly double _defaultMaxY; + private readonly string _title; + private readonly DiagnosticTextRenderer _textRenderer; + + private int _startFrameIndex; + private int _frameCount; + + public Size Size + => _size; + + public FrameTimeGraph(int maxFrames, Size size, double defaultMaxY, string title, + DiagnosticTextRenderer textRenderer) + { + Debug.Assert(maxFrames >= 1); + Debug.Assert(size.Width > 0.0); + Debug.Assert(size.Height > 0.0); + + _renderInterface = AvaloniaLocator.Current.GetRequiredService(); + _borderBrush = new ImmutableSolidColorBrush(0x80808080); + _graphPen = new ImmutablePen(Brushes.Blue); + _frameValues = new double[maxFrames]; + _size = size; + _headerSize = new Size(size.Width, textRenderer.MaxHeight + HeaderPadding * 2.0); + _graphSize = new Size(size.Width, size.Height - _headerSize.Height); + _defaultMaxY = defaultMaxY; + _title = title; + _textRenderer = textRenderer; + } + + public void AddFrameValue(double value) + { + if (_frameCount < _frameValues.Length) + { + _frameValues[_startFrameIndex + _frameCount] = value; + ++_frameCount; + } + else + { + // overwrite oldest value + _frameValues[_startFrameIndex] = value; + if (++_startFrameIndex == _frameValues.Length) + { + _startFrameIndex = 0; + } + } + } + + public void Reset() + { + _startFrameIndex = 0; + _frameCount = 0; + } + + public void Render(IDrawingContextImpl context) + { + var originalTransform = context.Transform; + context.PushClip(new Rect(_size)); + + context.DrawRectangle(_borderBrush, null, new RoundedRect(new Rect(_size))); + context.DrawRectangle(_borderBrush, null, new RoundedRect(new Rect(_headerSize))); + + context.Transform = originalTransform * Matrix.CreateTranslation(HeaderPadding, HeaderPadding); + _textRenderer.DrawAsciiText(context, _title.AsSpan(), Brushes.Black); + + if (_frameCount > 0) + { + var (min, avg, max) = GetYValues(); + + DrawLabelledValue(context, "Min", min, originalTransform, _headerSize.Width * 0.19); + DrawLabelledValue(context, "Avg", avg, originalTransform, _headerSize.Width * 0.46); + DrawLabelledValue(context, "Max", max, originalTransform, _headerSize.Width * 0.73); + + context.Transform = originalTransform * Matrix.CreateTranslation(0.0, _headerSize.Height); + context.DrawGeometry(null, _graphPen, BuildGraphGeometry(Math.Max(max, _defaultMaxY))); + } + + context.Transform = originalTransform; + context.PopClip(); + } + + private void DrawLabelledValue(IDrawingContextImpl context, string label, double value, in Matrix originalTransform, + double left) + { + context.Transform = originalTransform * Matrix.CreateTranslation(left + HeaderPadding, HeaderPadding); + + var brush = value <= _defaultMaxY ? Brushes.Black : Brushes.Red; + +#if NET6_0_OR_GREATER + Span buffer = stackalloc char[24]; + buffer.TryWrite(CultureInfo.InvariantCulture, $"{label}: {value,5:F2}ms", out var charsWritten); + _textRenderer.DrawAsciiText(context, buffer.Slice(0, charsWritten), brush); +#else + var text = FormattableString.Invariant($"{label}: {value,5:F2}ms"); + _textRenderer.DrawAsciiText(context, text.AsSpan(), brush); +#endif + } + + private IStreamGeometryImpl BuildGraphGeometry(double maxY) + { + Debug.Assert(_frameCount > 0); + + var graphGeometry = _renderInterface.CreateStreamGeometry(); + using var geometryContext = graphGeometry.Open(); + + var xRatio = _graphSize.Width / _frameValues.Length; + var yRatio = _graphSize.Height / maxY; + + geometryContext.BeginFigure(new Point(0.0, _graphSize.Height - GetFrameValue(0) * yRatio), false); + + for (var i = 1; i < _frameCount; ++i) + { + var x = Math.Round(i * xRatio); + var y = _graphSize.Height - GetFrameValue(i) * yRatio; + geometryContext.LineTo(new Point(x, y)); + } + + geometryContext.EndFigure(false); + return graphGeometry; + } + + private (double Min, double Average, double Max) GetYValues() + { + Debug.Assert(_frameCount > 0); + + var min = double.MaxValue; + var max = double.MinValue; + var total = 0.0; + + for (var i = 0; i < _frameCount; ++i) + { + var y = GetFrameValue(i); + + total += y; + + if (y < min) + { + min = y; + } + + if (y > max) + { + max = y; + } + } + + return (min, total / _frameCount, max); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double GetFrameValue(int frameOffset) + => _frameValues[(_startFrameIndex + frameOffset) % _frameValues.Length]; +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index b172430fbb..8d959a9765 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Numerics; +using System.Diagnostics; using System.Threading; using Avalonia.Media; using Avalonia.Media.Imaging; @@ -22,10 +22,11 @@ namespace Avalonia.Rendering.Composition.Server private readonly ServerCompositor _compositor; private readonly Func> _surfaces; private static long s_nextId = 1; - public long Id { get; } - public ulong Revision { get; private set; } private IRenderTarget? _renderTarget; - private FpsCounter _fpsCounter = new FpsCounter(Typeface.Default.GlyphTypeface); + private DiagnosticTextRenderer? _diagnosticTextRenderer; + private FpsCounter? _fpsCounter; + private FrameTimeGraph? _renderTimeGraph; + private FrameTimeGraph? _layoutTimeGraph; private Rect _dirtyRect; private Random _random = new(); private Size _layerSize; @@ -35,10 +36,24 @@ namespace Avalonia.Rendering.Composition.Server private HashSet _attachedVisuals = new(); private Queue _adornerUpdateQueue = new(); + public long Id { get; } + public ulong Revision { get; private set; } public ICompositionTargetDebugEvents? DebugEvents { get; set; } public ReadbackIndices Readback { get; } = new(); public int RenderedVisuals { get; set; } + private DiagnosticTextRenderer DiagnosticTextRenderer + => _diagnosticTextRenderer ??= new DiagnosticTextRenderer(Typeface.Default.GlyphTypeface, 12.0); + + private FpsCounter FpsCounter + => _fpsCounter ??= new FpsCounter(DiagnosticTextRenderer); + + private FrameTimeGraph LayoutTimeGraph + => _layoutTimeGraph ??= CreateTimeGraph("Layout"); + + private FrameTimeGraph RenderTimeGraph + => _renderTimeGraph ??= CreateTimeGraph("Render"); + public ServerCompositionTarget(ServerCompositor compositor, Func> surfaces) : base(compositor) { @@ -47,6 +62,9 @@ namespace Avalonia.Rendering.Composition.Server Id = Interlocked.Increment(ref s_nextId); } + private FrameTimeGraph CreateTimeGraph(string title) + => new(360, new Size(360.0, 64.0), 1000.0 / 60.0, title, DiagnosticTextRenderer); + partial void OnIsEnabledChanged() { if (IsEnabled) @@ -62,7 +80,33 @@ namespace Avalonia.Rendering.Composition.Server v.Deactivate(); } } - + + partial void OnDebugOverlaysChanged() + { + if ((DebugOverlays & RendererDebugOverlays.Fps) == 0) + { + _fpsCounter?.Reset(); + } + + if ((DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) == 0) + { + _layoutTimeGraph?.Reset(); + } + + if ((DebugOverlays & RendererDebugOverlays.RenderTimeGraph) == 0) + { + _renderTimeGraph?.Reset(); + } + } + + partial void OnLastLayoutPassTimingChanged() + { + if ((DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) != 0) + { + LayoutTimeGraph.AddFrameValue(LastLayoutPassTiming.Elapsed.TotalMilliseconds); + } + } + partial void DeserializeChangesExtra(BatchStreamReader c) { _redrawRequested = true; @@ -92,7 +136,10 @@ namespace Avalonia.Rendering.Composition.Server return; Revision++; - + + var captureTiming = (DebugOverlays & RendererDebugOverlays.RenderTimeGraph) != 0; + var startingTimestamp = captureTiming ? Stopwatch.GetTimestamp() : 0L; + // Update happens in a separate phase to extend dirty rect if needed Root.Update(this); @@ -137,33 +184,69 @@ namespace Avalonia.Rendering.Composition.Server targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1, new Rect(_layerSize), new Rect(Size), BitmapInterpolationMode.LowQuality); - - - if (DrawDirtyRects) - { - targetContext.DrawRectangle(new ImmutableSolidColorBrush( - new Color(30, (byte)_random.Next(255), (byte)_random.Next(255), - (byte)_random.Next(255))) - , null, _dirtyRect); - } - if (DrawFps) + if (DebugOverlays != RendererDebugOverlays.None) { - var nativeMem = ByteSizeHelper.ToString((ulong)( - (Compositor.BatchMemoryPool.CurrentUsage + Compositor.BatchMemoryPool.CurrentPool) * - Compositor.BatchMemoryPool.BufferSize), false); - var managedMem = ByteSizeHelper.ToString((ulong)( - (Compositor.BatchObjectPool.CurrentUsage + Compositor.BatchObjectPool.CurrentPool) * - Compositor.BatchObjectPool.ArraySize * - IntPtr.Size), false); - _fpsCounter.RenderFps(targetContext, FormattableString.Invariant($"M:{managedMem} / N:{nativeMem} R:{RenderedVisuals:0000}")); + if (captureTiming) + { + var elapsed = StopwatchHelper.GetElapsedTime(startingTimestamp); + RenderTimeGraph.AddFrameValue(elapsed.TotalMilliseconds); + } + + DrawOverlays(targetContext, layerSize); } + RenderedVisuals = 0; _dirtyRect = default; } } + private void DrawOverlays(IDrawingContextImpl targetContext, Size layerSize) + { + if ((DebugOverlays & RendererDebugOverlays.DirtyRects) != 0) + { + targetContext.DrawRectangle( + new ImmutableSolidColorBrush( + new Color(30, (byte)_random.Next(255), (byte)_random.Next(255), (byte)_random.Next(255))), + null, + _dirtyRect); + } + + if ((DebugOverlays & RendererDebugOverlays.Fps) != 0) + { + var nativeMem = ByteSizeHelper.ToString((ulong) ( + (Compositor.BatchMemoryPool.CurrentUsage + Compositor.BatchMemoryPool.CurrentPool) * + Compositor.BatchMemoryPool.BufferSize), false); + var managedMem = ByteSizeHelper.ToString((ulong) ( + (Compositor.BatchObjectPool.CurrentUsage + Compositor.BatchObjectPool.CurrentPool) * + Compositor.BatchObjectPool.ArraySize * + IntPtr.Size), false); + FpsCounter.RenderFps(targetContext, + FormattableString.Invariant($"M:{managedMem} / N:{nativeMem} R:{RenderedVisuals:0000}")); + } + + var top = 0.0; + + void DrawTimeGraph(FrameTimeGraph graph) + { + top += 8.0; + targetContext.Transform = Matrix.CreateTranslation(layerSize.Width - graph.Size.Width - 8.0, top); + graph.Render(targetContext); + top += graph.Size.Height; + } + + if ((DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) != 0) + { + DrawTimeGraph(LayoutTimeGraph); + } + + if ((DebugOverlays & RendererDebugOverlays.RenderTimeGraph) != 0) + { + DrawTimeGraph(RenderTimeGraph); + } + } + public Rect SnapToDevicePixels(Rect rect) => SnapToDevicePixels(rect, Scaling); private static Rect SnapToDevicePixels(Rect rect, double scale) diff --git a/src/Avalonia.Base/Rendering/IRenderer.cs b/src/Avalonia.Base/Rendering/IRenderer.cs index f3f5b5e99b..ba960ff5f3 100644 --- a/src/Avalonia.Base/Rendering/IRenderer.cs +++ b/src/Avalonia.Base/Rendering/IRenderer.cs @@ -1,5 +1,4 @@ using System; -using Avalonia.VisualTree; using System.Collections.Generic; using System.Threading.Tasks; using Avalonia.Rendering.Composition; @@ -12,15 +11,9 @@ namespace Avalonia.Rendering public interface IRenderer : IDisposable { /// - /// Gets or sets a value indicating whether the renderer should draw an FPS counter. + /// Gets a value indicating whether the renderer should draw specific diagnostics. /// - bool DrawFps { get; set; } - - /// - /// Gets or sets a value indicating whether the renderer should draw a visual representation - /// of its dirty rectangles. - /// - bool DrawDirtyRects { get; set; } + RendererDiagnostics Diagnostics { get; } /// /// Raised when a portion of the scene has been invalidated. diff --git a/src/Avalonia.Base/Rendering/LayoutPassTiming.cs b/src/Avalonia.Base/Rendering/LayoutPassTiming.cs new file mode 100644 index 0000000000..05dfdb7c42 --- /dev/null +++ b/src/Avalonia.Base/Rendering/LayoutPassTiming.cs @@ -0,0 +1,11 @@ +using System; + +namespace Avalonia.Rendering +{ + /// + /// Represents a single layout pass timing. + /// + /// The number of the layout pass. + /// The elapsed time during the layout pass. + public readonly record struct LayoutPassTiming(int PassCounter, TimeSpan Elapsed); +} diff --git a/src/Avalonia.Base/Rendering/RendererDebugOverlays.cs b/src/Avalonia.Base/Rendering/RendererDebugOverlays.cs new file mode 100644 index 0000000000..85932f1568 --- /dev/null +++ b/src/Avalonia.Base/Rendering/RendererDebugOverlays.cs @@ -0,0 +1,35 @@ +using System; + +namespace Avalonia.Rendering; + +/// +/// Represents the various types of overlays that can be drawn by a renderer. +/// +[Flags] +public enum RendererDebugOverlays +{ + /// + /// Do not draw any overlay. + /// + None = 0, + + /// + /// Draw a FPS counter. + /// + Fps = 1 << 0, + + /// + /// Draw invalidated rectangles each frame. + /// + DirtyRects = 1 << 1, + + /// + /// Draw a graph of past layout times. + /// + LayoutTimeGraph = 1 << 2, + + /// + /// Draw a graph of past render times. + /// + RenderTimeGraph = 1 << 3 +} diff --git a/src/Avalonia.Base/Rendering/RendererDiagnostics.cs b/src/Avalonia.Base/Rendering/RendererDiagnostics.cs new file mode 100644 index 0000000000..60165fcbe0 --- /dev/null +++ b/src/Avalonia.Base/Rendering/RendererDiagnostics.cs @@ -0,0 +1,57 @@ +using System.ComponentModel; + +namespace Avalonia.Rendering +{ + /// + /// Manages configurable diagnostics that can be displayed by a renderer. + /// + public class RendererDiagnostics : INotifyPropertyChanged + { + private RendererDebugOverlays _debugOverlays; + private LayoutPassTiming _lastLayoutPassTiming; + private PropertyChangedEventArgs? _debugOverlaysChangedEventArgs; + private PropertyChangedEventArgs? _lastLayoutPassTimingChangedEventArgs; + + /// + /// Gets or sets which debug overlays are displayed by the renderer. + /// + public RendererDebugOverlays DebugOverlays + { + get => _debugOverlays; + set + { + if (_debugOverlays != value) + { + _debugOverlays = value; + OnPropertyChanged(_debugOverlaysChangedEventArgs ??= new(nameof(DebugOverlays))); + } + } + } + + /// + /// Gets or sets the last layout pass timing that the renderer may display. + /// + public LayoutPassTiming LastLayoutPassTiming + { + get => _lastLayoutPassTiming; + set + { + if (!_lastLayoutPassTiming.Equals(value)) + { + _lastLayoutPassTiming = value; + OnPropertyChanged(_lastLayoutPassTimingChangedEventArgs ??= new(nameof(LastLayoutPassTiming))); + } + } + } + + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + /// Called when a property changes on the object. + /// + /// The property change details. + protected virtual void OnPropertyChanged(PropertyChangedEventArgs args) + => PropertyChanged?.Invoke(this, args); + } +} diff --git a/src/Avalonia.Base/Utilities/StopwatchHelper.cs b/src/Avalonia.Base/Utilities/StopwatchHelper.cs new file mode 100644 index 0000000000..4719226ea4 --- /dev/null +++ b/src/Avalonia.Base/Utilities/StopwatchHelper.cs @@ -0,0 +1,19 @@ +using System; +using System.Diagnostics; + +namespace Avalonia.Utilities; + +/// +/// Allows using as timestamps without allocating. +/// +/// Equivalent to Stopwatch.GetElapsedTime in .NET 7. +internal static class StopwatchHelper +{ + private static readonly double s_timestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; + + public static TimeSpan GetElapsedTime(long startingTimestamp) + => GetElapsedTime(startingTimestamp, Stopwatch.GetTimestamp()); + + public static TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) + => new((long)((endingTimestamp - startingTimestamp) * s_timestampToTicks)); +} diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index a0dbf238dc..b1a1be1973 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -39,8 +39,8 @@ - - + + diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 06a829c418..cb2b8cfd1c 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -1,7 +1,7 @@ using System; +using System.ComponentModel; using Avalonia.Reactive; using Avalonia.Controls.Metadata; -using Avalonia.Controls.Notifications; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -17,7 +17,6 @@ using Avalonia.Platform.Storage; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Utilities; -using Avalonia.VisualTree; using Avalonia.Input.Platform; using System.Linq; @@ -106,6 +105,7 @@ namespace Avalonia.Controls private Border? _transparencyFallbackBorder; private TargetWeakEventSubscriber? _resourcesChangesSubscriber; private IStorageProvider? _storageProvider; + private LayoutDiagnosticBridge? _layoutDiagnosticBridge; /// /// Initializes static members of the class. @@ -194,7 +194,7 @@ namespace Avalonia.Controls ClientSize = impl.ClientSize; FrameSize = impl.FrameSize; - + this.GetObservable(PointerOverElementProperty) .Select( x => (x as InputElement)?.GetObservable(CursorProperty) ?? Observable.Empty()) @@ -328,8 +328,17 @@ namespace Avalonia.Controls { get { - if (_layoutManager == null) + if (_layoutManager is null) + { _layoutManager = CreateLayoutManager(); + + if (_layoutManager is LayoutManager typedLayoutManager && Renderer is not null) + { + _layoutDiagnosticBridge = new LayoutDiagnosticBridge(Renderer.Diagnostics, typedLayoutManager); + _layoutDiagnosticBridge.SetupBridge(); + } + } + return _layoutManager; } } @@ -435,6 +444,9 @@ namespace Avalonia.Controls Renderer?.Dispose(); Renderer = null!; + _layoutDiagnosticBridge?.Dispose(); + _layoutDiagnosticBridge = null; + _pointerOverPreProcessor?.OnCompleted(); _pointerOverPreProcessorSubscription?.Dispose(); _backGestureSubscription?.Dispose(); @@ -617,5 +629,49 @@ namespace Avalonia.Controls } ITextInputMethodImpl? ITextInputMethodRoot.InputMethod => PlatformImpl?.TryGetFeature(); + + /// + /// Provides layout pass timing from the layout manager to the renderer, for diagnostics purposes. + /// + private sealed class LayoutDiagnosticBridge : IDisposable + { + private readonly RendererDiagnostics _diagnostics; + private readonly LayoutManager _layoutManager; + private bool _isHandling; + + public LayoutDiagnosticBridge(RendererDiagnostics diagnostics, LayoutManager layoutManager) + { + _diagnostics = diagnostics; + _layoutManager = layoutManager; + + diagnostics.PropertyChanged += OnDiagnosticsPropertyChanged; + } + + public void SetupBridge() + { + var needsHandling = (_diagnostics.DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) != 0; + if (needsHandling != _isHandling) + { + _isHandling = needsHandling; + _layoutManager.LayoutPassTimed = needsHandling + ? timing => _diagnostics.LastLayoutPassTiming = timing + : null; + } + } + + private void OnDiagnosticsPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(RendererDiagnostics.DebugOverlays)) + { + SetupBridge(); + } + } + + public void Dispose() + { + _diagnostics.PropertyChanged -= OnDiagnosticsPropertyChanged; + _layoutManager.LayoutPassTimed = null; + } + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 3870cad7c5..3adad38ac6 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -1,11 +1,13 @@ using System; using System.ComponentModel; +using System.Runtime.CompilerServices; using Avalonia.Controls; using Avalonia.Diagnostics.Models; using Avalonia.Input; using Avalonia.Metadata; using Avalonia.Threading; using Avalonia.Reactive; +using Avalonia.Rendering; namespace Avalonia.Diagnostics.ViewModels { @@ -21,8 +23,6 @@ namespace Avalonia.Diagnostics.ViewModels private string? _focusedControl; private IInputElement? _pointerOverElement; private bool _shouldVisualizeMarginPadding = true; - private bool _shouldVisualizeDirtyRects; - private bool _showFpsOverlay; private bool _freezePopups; private string? _pointerOverElementName; private IInputRoot? _pointerOverRoot; @@ -75,69 +75,76 @@ namespace Avalonia.Diagnostics.ViewModels set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value); } - public bool ShouldVisualizeDirtyRects + public void ToggleVisualizeMarginPadding() + => ShouldVisualizeMarginPadding = !ShouldVisualizeMarginPadding; + + private IRenderer? TryGetRenderer() + => _root switch + { + TopLevel topLevel => topLevel.Renderer, + Controls.Application app => app.RendererRoot, + _ => null + }; + + private bool GetDebugOverlay(RendererDebugOverlays overlay) + => ((TryGetRenderer()?.Diagnostics.DebugOverlays ?? RendererDebugOverlays.None) & overlay) != 0; + + private void SetDebugOverlay(RendererDebugOverlays overlay, bool enable, + [CallerMemberName] string? propertyName = null) { - get => _shouldVisualizeDirtyRects; - set + if (TryGetRenderer() is not { } renderer) { - var changed = true; - if (_root is TopLevel topLevel && topLevel.Renderer is { }) - { - topLevel.Renderer.DrawDirtyRects = value; - } - else if (_root is Controls.Application app && app.RendererRoot is { }) - { - app.RendererRoot.DrawDirtyRects = value; - } - else - { - changed = false; - } - if (changed) - { - RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value); - } + return; } - } - public void ToggleVisualizeDirtyRects() - { - ShouldVisualizeDirtyRects = !ShouldVisualizeDirtyRects; + var oldValue = renderer.Diagnostics.DebugOverlays; + var newValue = enable ? oldValue | overlay : oldValue & ~overlay; + + if (oldValue == newValue) + { + return; + } + + renderer.Diagnostics.DebugOverlays = newValue; + RaisePropertyChanged(propertyName); } - public void ToggleVisualizeMarginPadding() + public bool ShowDirtyRectsOverlay { - ShouldVisualizeMarginPadding = !ShouldVisualizeMarginPadding; + get => GetDebugOverlay(RendererDebugOverlays.DirtyRects); + set => SetDebugOverlay(RendererDebugOverlays.DirtyRects, value); } + public void ToggleDirtyRectsOverlay() + => ShowDirtyRectsOverlay = !ShowDirtyRectsOverlay; + public bool ShowFpsOverlay { - get => _showFpsOverlay; - set - { - var changed = true; - if (_root is TopLevel topLevel && topLevel.Renderer is { }) - { - topLevel.Renderer.DrawFps = value; - } - else if (_root is Controls.Application app && app.RendererRoot is { }) - { - app.RendererRoot.DrawFps = value; - } - else - { - changed = false; - } - if(changed) - RaiseAndSetIfChanged(ref _showFpsOverlay, value); - } + get => GetDebugOverlay(RendererDebugOverlays.Fps); + set => SetDebugOverlay(RendererDebugOverlays.Fps, value); } public void ToggleFpsOverlay() + => ShowFpsOverlay = !ShowFpsOverlay; + + public bool ShowLayoutTimeGraphOverlay { - ShowFpsOverlay = !ShowFpsOverlay; + get => GetDebugOverlay(RendererDebugOverlays.LayoutTimeGraph); + set => SetDebugOverlay(RendererDebugOverlays.LayoutTimeGraph, value); } + public void ToggleLayoutTimeGraphOverlay() + => ShowLayoutTimeGraphOverlay = !ShowLayoutTimeGraphOverlay; + + public bool ShowRenderTimeGraphOverlay + { + get => GetDebugOverlay(RendererDebugOverlays.RenderTimeGraph); + set => SetDebugOverlay(RendererDebugOverlays.RenderTimeGraph, value); + } + + public void ToggleRenderTimeGraphOverlay() + => ShowRenderTimeGraphOverlay = !ShowRenderTimeGraphOverlay; + public ConsoleViewModel Console { get; } public ViewModelBase? Content @@ -254,10 +261,10 @@ namespace Avalonia.Diagnostics.ViewModels _pointerOverSubscription.Dispose(); _logicalTree.Dispose(); _visualTree.Dispose(); - if (_root is TopLevel top) + + if (TryGetRenderer() is { } renderer) { - top.Renderer.DrawDirtyRects = false; - top.Renderer.DrawFps = false; + renderer.Diagnostics.DebugOverlays = RendererDebugOverlays.None; } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs index a2ee37c625..ec88db6664 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs @@ -20,7 +20,7 @@ namespace Avalonia.Diagnostics.ViewModels { } - protected bool RaiseAndSetIfChanged([NotNullIfNotNull("value")] ref T field, T value, [CallerMemberName] string propertyName = null!) + protected bool RaiseAndSetIfChanged([NotNullIfNotNull("value")] ref T field, T value, [CallerMemberName] string? propertyName = null) { if (!EqualityComparer.Default.Equals(field, value)) { @@ -32,7 +32,7 @@ namespace Avalonia.Diagnostics.ViewModels return false; } - protected void RaisePropertyChanged([CallerMemberName] string propertyName = null!) + protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null) { var e = new PropertyChangedEventArgs(propertyName); OnPropertyChanged(e); diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml index 97e21079c1..eac807a5bc 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml @@ -65,28 +65,42 @@ - - + + - + - + + + + + + + + + + + diff --git a/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs b/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs index 466aba43ee..3d7dc66cc4 100644 --- a/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs @@ -1,15 +1,7 @@ -using System; -using System.Collections.Generic; -using Avalonia.Controls; -using Avalonia.Controls.Presenters; -using Avalonia.Controls.Templates; +using Avalonia.Controls; using Avalonia.Input; -using Avalonia.Input.Raw; using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Rendering; using Avalonia.UnitTests; -using Moq; using Xunit; namespace Avalonia.Base.UnitTests.Input @@ -21,7 +13,7 @@ namespace Avalonia.Base.UnitTests.Input { using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var device = new MouseDevice(); var impl = CreateTopLevelImplMock(renderer.Object); @@ -59,7 +51,7 @@ namespace Avalonia.Base.UnitTests.Input { using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var device = new MouseDevice(); var impl = CreateTopLevelImplMock(renderer.Object); diff --git a/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs b/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs index 1ac50446c0..629188800a 100644 --- a/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs @@ -22,7 +22,7 @@ namespace Avalonia.Base.UnitTests.Input { using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var device = CreatePointerDeviceMock().Object; var impl = CreateTopLevelImplMock(renderer.Object); @@ -50,7 +50,7 @@ namespace Avalonia.Base.UnitTests.Input { using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var device = CreatePointerDeviceMock().Object; var impl = CreateTopLevelImplMock(renderer.Object); @@ -93,7 +93,7 @@ namespace Avalonia.Base.UnitTests.Input { using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var device = CreatePointerDeviceMock(pointerType: PointerType.Touch).Object; var impl = CreateTopLevelImplMock(renderer.Object); @@ -119,7 +119,7 @@ namespace Avalonia.Base.UnitTests.Input { using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var pointer = new Mock(); var device = CreatePointerDeviceMock(pointer.Object).Object; var impl = CreateTopLevelImplMock(renderer.Object); @@ -155,7 +155,7 @@ namespace Avalonia.Base.UnitTests.Input { using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var device = CreatePointerDeviceMock().Object; var impl = CreateTopLevelImplMock(renderer.Object); @@ -201,7 +201,7 @@ namespace Avalonia.Base.UnitTests.Input { using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var deviceMock = CreatePointerDeviceMock(); var impl = CreateTopLevelImplMock(renderer.Object); var result = new List<(object?, string)>(); @@ -256,7 +256,7 @@ namespace Avalonia.Base.UnitTests.Input { using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var deviceMock = CreatePointerDeviceMock(); var impl = CreateTopLevelImplMock(renderer.Object); var result = new List<(object?, string)>(); @@ -307,7 +307,7 @@ namespace Avalonia.Base.UnitTests.Input using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); var expectedPosition = new Point(15, 15); - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var deviceMock = CreatePointerDeviceMock(); var impl = CreateTopLevelImplMock(renderer.Object); var result = new List<(object?, string, Point)>(); @@ -351,7 +351,7 @@ namespace Avalonia.Base.UnitTests.Input { using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var deviceMock = CreatePointerDeviceMock(); var impl = CreateTopLevelImplMock(renderer.Object); @@ -405,7 +405,7 @@ namespace Avalonia.Base.UnitTests.Input { using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var deviceMock = CreatePointerDeviceMock(); var impl = CreateTopLevelImplMock(renderer.Object); @@ -442,7 +442,7 @@ namespace Avalonia.Base.UnitTests.Input { using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var deviceMock = CreatePointerDeviceMock(); var impl = CreateTopLevelImplMock(renderer.Object); diff --git a/tests/Avalonia.Base.UnitTests/VisualTests.cs b/tests/Avalonia.Base.UnitTests/VisualTests.cs index fb214a6b34..11bdc4bc68 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTests.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTests.cs @@ -150,7 +150,7 @@ namespace Avalonia.Base.UnitTests [Fact] public void Attaching_To_Visual_Tree_Should_Invalidate_Visual() { - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var child = new Decorator(); var root = new TestRoot { @@ -165,7 +165,7 @@ namespace Avalonia.Base.UnitTests [Fact] public void Detaching_From_Visual_Tree_Should_Invalidate_Visual() { - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var child = new Decorator(); var root = new TestRoot { @@ -307,7 +307,7 @@ namespace Avalonia.Base.UnitTests public void Changing_ZIndex_Should_InvalidateVisual() { Canvas canvas1; - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var root = new TestRoot { Child = new StackPanel @@ -331,7 +331,7 @@ namespace Avalonia.Base.UnitTests { Canvas canvas1; StackPanel stackPanel; - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var root = new TestRoot { Child = stackPanel = new StackPanel diff --git a/tests/Avalonia.Benchmarks/NullRenderer.cs b/tests/Avalonia.Benchmarks/NullRenderer.cs deleted file mode 100644 index feb325f630..0000000000 --- a/tests/Avalonia.Benchmarks/NullRenderer.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Avalonia.Rendering; -using Avalonia.VisualTree; - -namespace Avalonia.Benchmarks -{ - internal class NullRenderer : IRenderer - { - public bool DrawFps { get; set; } - public bool DrawDirtyRects { get; set; } -#pragma warning disable CS0067 - public event EventHandler SceneInvalidated; -#pragma warning restore CS0067 - public void AddDirty(Visual visual) - { - } - - public void Dispose() - { - } - - public IEnumerable HitTest(Point p, Visual root, Func filter) => null; - - public Visual HitTestFirst(Point p, Visual root, Func filter) => null; - - public void Paint(Rect rect) - { - } - - public void RecalculateChildren(Visual visual) - { - } - - public void Resized(Size size) - { - } - - public void Start() - { - } - - public void Stop() - { - } - - public ValueTask TryGetRenderInterfaceFeature(Type featureType) => new(0); - } -} diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs index 8bd51ec500..4ff98bdedd 100644 --- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs @@ -134,16 +134,16 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Button_Raises_Click() { - var renderer = Mock.Of(); + var renderer = RendererMocks.CreateRenderer(); var pt = new Point(50, 50); - Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) + renderer.Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) .Returns>((p, r, f) => r.Bounds.Contains(p) ? new Visual[] { r } : new Visual[0]); var target = new TestButton() { Bounds = new Rect(0, 0, 100, 100), - Renderer = renderer + Renderer = renderer.Object }; bool clicked = false; @@ -166,16 +166,16 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Button_Does_Not_Raise_Click_When_PointerReleased_Outside() { - var renderer = Mock.Of(); - - Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) + var renderer = RendererMocks.CreateRenderer(); + + renderer.Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) .Returns>((p, r, f) => r.Bounds.Contains(p) ? new Visual[] { r } : new Visual[0]); var target = new TestButton() { Bounds = new Rect(0, 0, 100, 100), - Renderer = renderer + Renderer = renderer.Object }; bool clicked = false; @@ -199,9 +199,9 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Button_With_RenderTransform_Raises_Click() { - var renderer = Mock.Of(); + var renderer = RendererMocks.CreateRenderer(); var pt = new Point(150, 50); - Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) + renderer.Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) .Returns>((p, r, f) => r.Bounds.Contains(p.Transform(r.RenderTransform.Value.Invert())) ? new Visual[] { r } : new Visual[0]); @@ -210,7 +210,7 @@ namespace Avalonia.Controls.UnitTests { Bounds = new Rect(0, 0, 100, 100), RenderTransform = new TranslateTransform { X = 100, Y = 0 }, - Renderer = renderer + Renderer = renderer.Object }; //actual bounds of button should be 100,0,100,100 x -> translated 100 pixels diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index baf933bd66..d99c90cb77 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -595,7 +595,7 @@ namespace Avalonia.Controls.UnitTests private static Window PreparedWindow(object content = null) { - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var platform = AvaloniaLocator.Current.GetRequiredService(); var windowImpl = Mock.Get(platform.CreateWindow()); windowImpl.Setup(x => x.CreateRenderer(It.IsAny())).Returns(renderer.Object); diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs index 3a2e1c08bd..8cd5816984 100644 --- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform; +using Avalonia.Rendering; using Avalonia.UnitTests; using Moq; using Xunit; @@ -189,6 +190,8 @@ namespace Avalonia.Controls.UnitTests public void Impl_Closing_Should_Remove_Window_From_OpenWindows() { var windowImpl = new Mock(); + windowImpl.Setup(x => x.CreateRenderer(It.IsAny())) + .Returns(() => RendererMocks.CreateRenderer().Object); windowImpl.SetupProperty(x => x.Closed); windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1); diff --git a/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs b/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs index 02767a21eb..7767de11c7 100644 --- a/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs +++ b/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs @@ -569,7 +569,7 @@ namespace Avalonia.Controls.UnitTests private static Window PreparedWindow(object content = null) { - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var platform = AvaloniaLocator.Current.GetRequiredService(); var windowImpl = Mock.Get(platform.CreateWindow()); windowImpl.Setup(x => x.CreateRenderer(It.IsAny())).Returns(renderer.Object); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index f4206959a9..4804b29fee 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -563,7 +563,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (CreateServices()) { - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var platform = AvaloniaLocator.Current.GetRequiredService(); var windowImpl = Mock.Get(platform.CreateWindow()); windowImpl.Setup(x => x.CreateRenderer(It.IsAny())).Returns(renderer.Object); diff --git a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs index 8f9af52ed8..a10b1324d6 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs @@ -110,6 +110,8 @@ namespace Avalonia.Controls.UnitTests public void IsVisible_Should_Be_False_Atfer_Impl_Signals_Close() { var windowImpl = new Mock(); + windowImpl.Setup(x => x.CreateRenderer(It.IsAny())) + .Returns(() => RendererMocks.CreateRenderer().Object); windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1); windowImpl.SetupProperty(x => x.Closed); @@ -129,6 +131,8 @@ namespace Avalonia.Controls.UnitTests public void Setting_IsVisible_True_Shows_Window() { var windowImpl = new Mock(); + windowImpl.Setup(x => x.CreateRenderer(It.IsAny())) + .Returns(() => RendererMocks.CreateRenderer().Object); windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1); @@ -145,6 +149,8 @@ namespace Avalonia.Controls.UnitTests public void Setting_IsVisible_False_Hides_Window() { var windowImpl = new Mock(); + windowImpl.Setup(x => x.CreateRenderer(It.IsAny())) + .Returns(() => RendererMocks.CreateRenderer().Object); windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1); @@ -163,7 +169,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var target = new TestWindowBase(renderer.Object); target.Show(); @@ -194,7 +200,7 @@ namespace Avalonia.Controls.UnitTests using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var target = new TestWindowBase(renderer.Object); target.Show(); @@ -209,7 +215,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var windowImpl = new Mock(); windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1); @@ -240,12 +246,15 @@ namespace Avalonia.Controls.UnitTests public bool IsClosed { get; private set; } public TestWindowBase(IRenderer renderer = null) - : base(Mock.Of(x => - x.RenderScaling == 1 && - x.CreateRenderer(It.IsAny()) == renderer)) + : base(CreateWindowsBaseImplMock(renderer ?? RendererMocks.CreateRenderer().Object)) { } + private static IWindowBaseImpl CreateWindowsBaseImplMock(IRenderer renderer) + => Mock.Of(x => + x.RenderScaling == 1 && + x.CreateRenderer(It.IsAny()) == renderer); + public TestWindowBase(IWindowBaseImpl impl) : base(impl) { diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index ca245005c2..014174990e 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -98,6 +98,8 @@ namespace Avalonia.Controls.UnitTests public void IsVisible_Should_Be_False_After_Impl_Signals_Close() { var windowImpl = new Mock(); + windowImpl.Setup(x => x.CreateRenderer(It.IsAny())) + .Returns(() => RendererMocks.CreateRenderer().Object); windowImpl.SetupProperty(x => x.Closed); windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1); @@ -269,7 +271,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var target = new Window(CreateImpl(renderer)); target.Show(); @@ -284,7 +286,7 @@ namespace Avalonia.Controls.UnitTests using (UnitTestApplication.Start(TestServices.StyledWindow)) { var parent = new Window(); - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var target = new Window(CreateImpl(renderer)); parent.Show(); @@ -317,7 +319,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); var target = new Window(CreateImpl(renderer)); target.Show(); @@ -334,6 +336,8 @@ namespace Avalonia.Controls.UnitTests { var parent = new Window(); var windowImpl = new Mock(); + windowImpl.Setup(x => x.CreateRenderer(It.IsAny())) + .Returns(() => RendererMocks.CreateRenderer().Object); windowImpl.SetupProperty(x => x.Closed); windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1); @@ -375,6 +379,8 @@ namespace Avalonia.Controls.UnitTests { var parent = new Window(); var windowImpl = new Mock(); + windowImpl.Setup(x => x.CreateRenderer(It.IsAny())) + .Returns(() => RendererMocks.CreateRenderer().Object); windowImpl.SetupProperty(x => x.Closed); windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1); diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 678fb5c163..c9f79871c9 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -462,7 +462,7 @@ namespace Avalonia.LeakTests { using (Start()) { - var renderer = new Mock(); + var renderer = RendererMocks.CreateRenderer(); renderer.Setup(x => x.Dispose()); var impl = new Mock(); impl.Setup(r => r.TryGetFeature(It.IsAny())).Returns(null); @@ -1029,46 +1029,5 @@ namespace Avalonia.LeakTests public IEnumerable Children { get; set; } } - private class NullRenderer : IRenderer - { - public bool DrawFps { get; set; } - public bool DrawDirtyRects { get; set; } -#pragma warning disable CS0067 - public event EventHandler SceneInvalidated; -#pragma warning restore CS0067 - public void AddDirty(Visual visual) - { - } - - public void Dispose() - { - } - - public IEnumerable HitTest(Point p, Visual root, Func filter) => null; - - public Visual HitTestFirst(Point p, Visual root, Func filter) => null; - - public void Paint(Rect rect) - { - } - - public void RecalculateChildren(Visual visual) - { - } - - public void Resized(Size size) - { - } - - public void Start() - { - } - - public void Stop() - { - } - - public ValueTask TryGetRenderInterfaceFeature(Type featureType) => new(null); - } } } diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 7f28477d09..142a9cd8ee 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -1,6 +1,5 @@ using System; using Avalonia.Controls.Primitives.PopupPositioning; -using Avalonia.Input; using Moq; using Avalonia.Platform; using Avalonia.Rendering; @@ -28,6 +27,8 @@ namespace Avalonia.UnitTests var clientSize = new Size(initialWidth, initialHeight); windowImpl.SetupAllProperties(); + windowImpl.Setup(x => x.CreateRenderer(It.IsAny())) + .Returns(() => RendererMocks.CreateRenderer().Object); windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize); windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize); windowImpl.Setup(x => x.DesktopScaling).Returns(1); @@ -92,6 +93,8 @@ namespace Avalonia.UnitTests var positioner = new ManagedPopupPositioner(positionerHelper); popupImpl.SetupAllProperties(); + popupImpl.Setup(x => x.CreateRenderer(It.IsAny())) + .Returns(() => RendererMocks.CreateRenderer().Object); popupImpl.Setup(x => x.ClientSize).Returns(() => clientSize); popupImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize); popupImpl.Setup(x => x.RenderScaling).Returns(1); diff --git a/tests/Avalonia.UnitTests/NullRenderer.cs b/tests/Avalonia.UnitTests/NullRenderer.cs new file mode 100644 index 0000000000..1b59aa30eb --- /dev/null +++ b/tests/Avalonia.UnitTests/NullRenderer.cs @@ -0,0 +1,57 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Rendering; + +namespace Avalonia.UnitTests; + +public sealed class NullRenderer : IRenderer +{ + public RendererDiagnostics Diagnostics { get; } = new(); + + public event EventHandler? SceneInvalidated; + + public NullRenderer() + { + } + + public void AddDirty(Visual visual) + { + } + + public void Dispose() + { + } + + public IEnumerable HitTest(Point p, Visual root, Func filter) + => Enumerable.Empty(); + + public Visual? HitTestFirst(Point p, Visual root, Func filter) + => null; + + public void Paint(Rect rect) + { + } + + public void RecalculateChildren(Visual visual) + { + } + + public void Resized(Size size) + { + } + + public void Start() + { + } + + public void Stop() + { + } + + public ValueTask TryGetRenderInterfaceFeature(Type featureType) + => new((object?) null); +} diff --git a/tests/Avalonia.UnitTests/RendererMocks.cs b/tests/Avalonia.UnitTests/RendererMocks.cs new file mode 100644 index 0000000000..d4808a7556 --- /dev/null +++ b/tests/Avalonia.UnitTests/RendererMocks.cs @@ -0,0 +1,15 @@ +using Avalonia.Rendering; +using Moq; + +namespace Avalonia.UnitTests +{ + public static class RendererMocks + { + public static Mock CreateRenderer() + { + var renderer = new Mock(); + renderer.SetupGet(r => r.Diagnostics).Returns(new RendererDiagnostics()); + return renderer; + } + } +} diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index 93c04057ef..875c5eb944 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -1,9 +1,7 @@ -using System; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Layout; using Avalonia.LogicalTree; -using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Styling; @@ -18,7 +16,7 @@ namespace Avalonia.UnitTests public TestRoot() { - Renderer = Mock.Of(); + Renderer = RendererMocks.CreateRenderer().Object; LayoutManager = new LayoutManager(this); IsVisible = true; KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle); From f43fc66ae3386373b687a945449e5552e87177ed Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 31 Jan 2023 16:16:45 +0100 Subject: [PATCH 021/185] Fix failing integration tests on macOS. Looks like maybe macOS 13.1 changed behavior around fullscreen windows, breaking our integration tests. --- .../ElementExtensions.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs index e7837a6971..b9df420270 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs @@ -118,7 +118,16 @@ namespace Avalonia.IntegrationTests.Appium Thread.Sleep(1000); var newWindows = session.FindElements(By.XPath("/XCUIElementTypeApplication/XCUIElementTypeWindow")); - var newWindowTitles = newWindows.ToDictionary(x => x.Text); + + // Try to find the new window by looking for a window with a title that didn't exist before the button + // was clicked. Sometimes it seems that when a window becomes fullscreen, all other windows in the + // application lose their titles, so filter out windows with no title (this may have started happening + // with macOS 13.1?) + var newWindowTitles = newWindows + .Select(x => (x.Text, x)) + .Where(x => !string.IsNullOrEmpty(x.Text)) + .ToDictionary(x => x.Text, x => x.x); + var newWindowTitle = Assert.Single(newWindowTitles.Keys.Except(oldWindowTitles.Keys)); return Disposable.Create(() => From aeb4313487b5fac2fca655bb0f3842f0638a7b26 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Tue, 31 Jan 2023 23:23:29 +0800 Subject: [PATCH 022/185] feat: add SelectAll hotkey support. --- src/Avalonia.Controls/SelectableTextBlock.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/SelectableTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs index f4c4d54951..f8ce5d23f6 100644 --- a/src/Avalonia.Controls/SelectableTextBlock.cs +++ b/src/Avalonia.Controls/SelectableTextBlock.cs @@ -229,7 +229,11 @@ namespace Avalonia.Controls if (Match(keymap.Copy)) { Copy(); - + handled = true; + } + else if (Match(keymap.SelectAll)) + { + SelectAll(); handled = true; } From e97a8cfa3833c3ac15183d54c6c160853ebf2b7b Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Tue, 31 Jan 2023 18:18:42 +0100 Subject: [PATCH 023/185] Made LayoutPassTiming internal --- src/Avalonia.Base/Rendering/LayoutPassTiming.cs | 2 +- src/Avalonia.Base/Rendering/RendererDiagnostics.cs | 2 +- src/Avalonia.Base/composition-schema.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Rendering/LayoutPassTiming.cs b/src/Avalonia.Base/Rendering/LayoutPassTiming.cs index 05dfdb7c42..b4b6d1d4f1 100644 --- a/src/Avalonia.Base/Rendering/LayoutPassTiming.cs +++ b/src/Avalonia.Base/Rendering/LayoutPassTiming.cs @@ -7,5 +7,5 @@ namespace Avalonia.Rendering /// /// The number of the layout pass. /// The elapsed time during the layout pass. - public readonly record struct LayoutPassTiming(int PassCounter, TimeSpan Elapsed); + internal readonly record struct LayoutPassTiming(int PassCounter, TimeSpan Elapsed); } diff --git a/src/Avalonia.Base/Rendering/RendererDiagnostics.cs b/src/Avalonia.Base/Rendering/RendererDiagnostics.cs index 60165fcbe0..0897cac62e 100644 --- a/src/Avalonia.Base/Rendering/RendererDiagnostics.cs +++ b/src/Avalonia.Base/Rendering/RendererDiagnostics.cs @@ -31,7 +31,7 @@ namespace Avalonia.Rendering /// /// Gets or sets the last layout pass timing that the renderer may display. /// - public LayoutPassTiming LastLayoutPassTiming + internal LayoutPassTiming LastLayoutPassTiming { get => _lastLayoutPassTiming; set diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index b1a1be1973..36fd9fe709 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -40,7 +40,7 @@ - + From cba57d8008a8a4c3b383baa0bfd1dfb85cfb9d28 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Tue, 31 Jan 2023 18:21:32 +0100 Subject: [PATCH 024/185] Create DiagnosticTextRenderer on the UI thread --- .../Composition/Compositor.Factories.cs | 3 +-- .../Rendering/Composition/Compositor.cs | 5 ++++- .../Server/DiagnosticTextRenderer.cs | 20 +++++++++++++++---- .../Composition/Server/FrameTimeGraph.cs | 2 +- .../Server/ServerCompositionTarget.cs | 15 +++++++------- 5 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs index 6dba18704f..801dd32d59 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Avalonia.Platform; using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Server; @@ -15,7 +14,7 @@ public partial class Compositor /// public CompositionTarget CreateCompositionTarget(Func> surfaces) { - return new CompositionTarget(this, new ServerCompositionTarget(_server, surfaces)); + return new CompositionTarget(this, new ServerCompositionTarget(_server, surfaces, DiagnosticTextRenderer)); } public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server)); diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index aea4df525d..153b32c5f3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -35,11 +35,14 @@ namespace Avalonia.Rendering.Composition private Task? _pendingBatch; private readonly object _pendingBatchLock = new(); private List _pendingServerCompositorJobs = new(); + private DiagnosticTextRenderer? _diagnosticTextRenderer; internal IEasing DefaultEasing { get; } + private DiagnosticTextRenderer DiagnosticTextRenderer + => _diagnosticTextRenderer ??= new(Typeface.Default.GlyphTypeface, 12.0); + internal event Action? AfterCommit; - /// /// Creates a new compositor on a specified render loop that would use a particular GPU diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs index 6e97f00681..b01fb46aa3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs @@ -14,7 +14,21 @@ namespace Avalonia.Rendering.Composition.Server private readonly GlyphRun[] _runs = new GlyphRun[LastChar - FirstChar + 1]; - public double MaxHeight { get; } + public double GetMaxHeight() + { + var maxHeight = 0.0; + + for (var c = FirstChar; c <= LastChar; c++) + { + var height = _runs[c - FirstChar].Size.Height; + if (height > maxHeight) + { + maxHeight = height; + } + } + + return maxHeight; + } public DiagnosticTextRenderer(IGlyphTypeface typeface, double fontRenderingEmSize) { @@ -24,9 +38,7 @@ namespace Avalonia.Rendering.Composition.Server var index = c - FirstChar; chars[index] = c; var glyph = typeface.GetGlyph(c); - var run = new GlyphRun(typeface, fontRenderingEmSize, chars.AsMemory(index, 1), new[] { glyph }); - _runs[index] = run; - MaxHeight = Math.Max(run.Size.Height, MaxHeight); + _runs[index] = new GlyphRun(typeface, fontRenderingEmSize, chars.AsMemory(index, 1), new[] { glyph }); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs b/src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs index c926c75c52..d103b068a6 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs @@ -44,7 +44,7 @@ internal sealed class FrameTimeGraph _graphPen = new ImmutablePen(Brushes.Blue); _frameValues = new double[maxFrames]; _size = size; - _headerSize = new Size(size.Width, textRenderer.MaxHeight + HeaderPadding * 2.0); + _headerSize = new Size(size.Width, textRenderer.GetMaxHeight() + HeaderPadding * 2.0); _graphSize = new Size(size.Width, size.Height - _headerSize.Height); _defaultMaxY = defaultMaxY; _title = title; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 8d959a9765..e7db80f90c 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -21,9 +21,9 @@ namespace Avalonia.Rendering.Composition.Server { private readonly ServerCompositor _compositor; private readonly Func> _surfaces; + private readonly DiagnosticTextRenderer _diagnosticTextRenderer; private static long s_nextId = 1; private IRenderTarget? _renderTarget; - private DiagnosticTextRenderer? _diagnosticTextRenderer; private FpsCounter? _fpsCounter; private FrameTimeGraph? _renderTimeGraph; private FrameTimeGraph? _layoutTimeGraph; @@ -42,11 +42,8 @@ namespace Avalonia.Rendering.Composition.Server public ReadbackIndices Readback { get; } = new(); public int RenderedVisuals { get; set; } - private DiagnosticTextRenderer DiagnosticTextRenderer - => _diagnosticTextRenderer ??= new DiagnosticTextRenderer(Typeface.Default.GlyphTypeface, 12.0); - private FpsCounter FpsCounter - => _fpsCounter ??= new FpsCounter(DiagnosticTextRenderer); + => _fpsCounter ??= new FpsCounter(_diagnosticTextRenderer); private FrameTimeGraph LayoutTimeGraph => _layoutTimeGraph ??= CreateTimeGraph("Layout"); @@ -54,16 +51,18 @@ namespace Avalonia.Rendering.Composition.Server private FrameTimeGraph RenderTimeGraph => _renderTimeGraph ??= CreateTimeGraph("Render"); - public ServerCompositionTarget(ServerCompositor compositor, Func> surfaces) : - base(compositor) + public ServerCompositionTarget(ServerCompositor compositor, Func> surfaces, + DiagnosticTextRenderer diagnosticTextRenderer) + : base(compositor) { _compositor = compositor; _surfaces = surfaces; + _diagnosticTextRenderer = diagnosticTextRenderer; Id = Interlocked.Increment(ref s_nextId); } private FrameTimeGraph CreateTimeGraph(string title) - => new(360, new Size(360.0, 64.0), 1000.0 / 60.0, title, DiagnosticTextRenderer); + => new(360, new Size(360.0, 64.0), 1000.0 / 60.0, title, _diagnosticTextRenderer); partial void OnIsEnabledChanged() { From 0fa4263354742a317ea1f68cfb8aadeeb396138c Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Tue, 31 Jan 2023 19:24:49 +0100 Subject: [PATCH 025/185] Fixed overlay graph on high DPI screens --- .../Rendering/Composition/Server/ServerCompositionTarget.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index e7db80f90c..63ec8d756b 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -192,7 +192,7 @@ namespace Avalonia.Rendering.Composition.Server RenderTimeGraph.AddFrameValue(elapsed.TotalMilliseconds); } - DrawOverlays(targetContext, layerSize); + DrawOverlays(targetContext); } RenderedVisuals = 0; @@ -201,7 +201,7 @@ namespace Avalonia.Rendering.Composition.Server } } - private void DrawOverlays(IDrawingContextImpl targetContext, Size layerSize) + private void DrawOverlays(IDrawingContextImpl targetContext) { if ((DebugOverlays & RendererDebugOverlays.DirtyRects) != 0) { @@ -230,7 +230,7 @@ namespace Avalonia.Rendering.Composition.Server void DrawTimeGraph(FrameTimeGraph graph) { top += 8.0; - targetContext.Transform = Matrix.CreateTranslation(layerSize.Width - graph.Size.Width - 8.0, top); + targetContext.Transform = Matrix.CreateTranslation(Size.Width - graph.Size.Width - 8.0, top); graph.Render(targetContext); top += graph.Size.Height; } From 35662ad4cbe5b4805a57b398d4fe376190412c93 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 31 Jan 2023 15:59:28 -0500 Subject: [PATCH 026/185] Allow only well known ThemeVariant values in xaml compiler, add more documentation. --- .../ControlCatalog/Pages/ThemePage.axaml.cs | 2 +- src/Avalonia.Base/Styling/ThemeVariant.cs | 62 +++++++++++++++---- .../Styling/ThemeVariantTypeConverter.cs | 3 +- src/Avalonia.Controls/Application.cs | 2 +- src/Avalonia.Controls/TopLevel.cs | 2 +- .../AvaloniaXamlIlLanguageParseIntrinsics.cs | 7 --- .../AvaloniaXamlIlWellKnownTypes.cs | 2 - .../{ => Xaml}/ThemeDictionariesTests.cs | 13 ++-- 8 files changed, 64 insertions(+), 29 deletions(-) rename tests/Avalonia.Markup.Xaml.UnitTests/{ => Xaml}/ThemeDictionariesTests.cs (97%) diff --git a/samples/ControlCatalog/Pages/ThemePage.axaml.cs b/samples/ControlCatalog/Pages/ThemePage.axaml.cs index af7b2fe37d..f0ae1a722d 100644 --- a/samples/ControlCatalog/Pages/ThemePage.axaml.cs +++ b/samples/ControlCatalog/Pages/ThemePage.axaml.cs @@ -18,7 +18,7 @@ namespace ControlCatalog.Pages selector.Items = new[] { - new ThemeVariant("Default"), + ThemeVariant.Default, ThemeVariant.Dark, ThemeVariant.Light, Pink diff --git a/src/Avalonia.Base/Styling/ThemeVariant.cs b/src/Avalonia.Base/Styling/ThemeVariant.cs index d9cb123925..8218533f4f 100644 --- a/src/Avalonia.Base/Styling/ThemeVariant.cs +++ b/src/Avalonia.Base/Styling/ThemeVariant.cs @@ -5,20 +5,60 @@ using Avalonia.Platform; namespace Avalonia.Styling; +/// +/// Specifies a UI theme variant that should be used for the +/// [TypeConverter(typeof(ThemeVariantTypeConverter))] -public sealed record ThemeVariant(object Key) -{ +public sealed record ThemeVariant +{ + /// + /// Creates a new instance of the + /// + /// Key of the theme variant by which variants are compared. + /// Reference to a theme variant which should be used, if resource wasn't found for the requested variant. + /// Thrown if inheritVariant is a reference to the which is ambiguous value to inherit. + /// Thrown if key is null. public ThemeVariant(object key, ThemeVariant? inheritVariant) - : this(key) { + Key = key ?? throw new ArgumentNullException(nameof(key)); InheritVariant = inheritVariant; + + if (inheritVariant == Default) + { + throw new ArgumentException("Inheriting default theme variant is not supported.", nameof(inheritVariant)); + } } + private ThemeVariant(object key) + { + Key = key; + } + + /// + /// Key of the theme variant by which variants are compared. + /// + public object Key { get; } + + /// + /// Reference to a theme variant which should be used, if resource wasn't found for the requested variant. + /// + public ThemeVariant? InheritVariant { get; } + + /// + /// Inherit theme variant from the parent. If set on Application, system theme is inherited. + /// Using Default as the ResourceDictionary.Key marks this dictionary as a fallback in case the theme variant or resource key is not found in other theme dictionaries. + /// public static ThemeVariant Default { get; } = new(nameof(Default)); + + /// + /// Use the Light theme variant. + /// public static ThemeVariant Light { get; } = new(nameof(Light)); - public static ThemeVariant Dark { get; } = new(nameof(Dark)); - public ThemeVariant? InheritVariant { get; init; } + /// + /// Use the Dark theme variant. + /// + public static ThemeVariant Dark { get; } = new(nameof(Dark)); public override string ToString() { @@ -35,7 +75,7 @@ public sealed record ThemeVariant(object Key) return Key == other?.Key; } - public static ThemeVariant FromPlatformThemeVariant(PlatformThemeVariant themeVariant) + public static explicit operator ThemeVariant(PlatformThemeVariant themeVariant) { return themeVariant switch { @@ -45,19 +85,19 @@ public sealed record ThemeVariant(object Key) }; } - public PlatformThemeVariant? ToPlatformThemeVariant() + public static explicit operator PlatformThemeVariant?(ThemeVariant themeVariant) { - if (this == Light) + if (themeVariant == Light) { return PlatformThemeVariant.Light; } - else if (this == Dark) + else if (themeVariant == Dark) { return PlatformThemeVariant.Dark; } - else if (InheritVariant is { } inheritVariant) + else if (themeVariant.InheritVariant is { } inheritVariant) { - return inheritVariant.ToPlatformThemeVariant(); + return (PlatformThemeVariant?)inheritVariant; } return null; diff --git a/src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs b/src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs index 4da1b495f5..acb2d7651b 100644 --- a/src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs +++ b/src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs @@ -15,9 +15,10 @@ public class ThemeVariantTypeConverter : TypeConverter { return value switch { + nameof(ThemeVariant.Default) => ThemeVariant.Default, nameof(ThemeVariant.Light) => ThemeVariant.Light, nameof(ThemeVariant.Dark) => ThemeVariant.Dark, - _ => new ThemeVariant(value) + _ => throw new NotSupportedException("ThemeVariant type converter supports only build in variants. For custom variants please use x:Static markup extension.") }; } } diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 3dcba4ded9..6d3ba3cf8a 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -340,7 +340,7 @@ namespace Avalonia private void OnColorValuesChanged(object? sender, PlatformColorValues e) { - SetValue(ActualThemeVariantProperty, ThemeVariant.FromPlatformThemeVariant(e.ThemeVariant), BindingPriority.Template); + SetValue(ActualThemeVariantProperty, (ThemeVariant)e.ThemeVariant, BindingPriority.Template); } } } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 7fe82a452e..676fa1519a 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -435,7 +435,7 @@ namespace Avalonia.Controls } else if (change.Property == ActualThemeVariantProperty) { - PlatformImpl?.SetFrameThemeVariant(change.GetNewValue().ToPlatformThemeVariant() ?? PlatformThemeVariant.Light); + PlatformImpl?.SetFrameThemeVariant((PlatformThemeVariant?)change.GetNewValue() ?? PlatformThemeVariant.Light); } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index 365a07a7f6..4068caac21 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -302,13 +302,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions result = new XamlStaticExtensionNode(new XamlAstObjectNode(node, node.Type), themeVariantTypeRef, foundConstProperty.Name); return true; } - - result = new XamlAstNewClrObjectNode(node, themeVariantTypeRef, types.ThemeVariantConstructor, - new List() - { - new XamlConstantNode(node, context.Configuration.WellKnownTypes.String, variantText) - }); - return true; } result = null; diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index a4a3bcce94..16f6a32ae1 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -69,7 +69,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType Thickness { get; } public IXamlConstructor ThicknessFullConstructor { get; } public IXamlType ThemeVariant { get; } - public IXamlConstructor ThemeVariantConstructor { get; } public IXamlType Point { get; } public IXamlConstructor PointFullConstructor { get; } public IXamlType Vector { get; } @@ -193,7 +192,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers FontFamily = cfg.TypeSystem.GetType("Avalonia.Media.FontFamily"); FontFamilyConstructorUriName = FontFamily.GetConstructor(new List { Uri, XamlIlTypes.String }); ThemeVariant = cfg.TypeSystem.GetType("Avalonia.Styling.ThemeVariant"); - ThemeVariantConstructor = ThemeVariant.GetConstructor(new List { XamlIlTypes.String }); (IXamlType, IXamlConstructor) GetNumericTypeInfo(string name, IXamlType componentType, int componentCount) { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/ThemeDictionariesTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs similarity index 97% rename from tests/Avalonia.Markup.Xaml.UnitTests/ThemeDictionariesTests.cs rename to tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs index 56040c2186..c5b62cdff2 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/ThemeDictionariesTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs @@ -6,10 +6,12 @@ using Avalonia.Styling; using Moq; using Xunit; -namespace Avalonia.Markup.Xaml.UnitTests; +namespace Avalonia.Markup.Xaml.UnitTests.Xaml; public class ThemeDictionariesTests : XamlTestBase { + public static ThemeVariant Custom { get; } = new(nameof(Custom), ThemeVariant.Light); + [Fact] public void DynamicResource_Updated_When_Control_Theme_Changed() { @@ -353,13 +355,14 @@ public class ThemeDictionariesTests : XamlTestBase Assert.Equal(Colors.Red, ((ISolidColorBrush)border.Background)!.Color); } - + [Fact] public void Custom_Theme_Can_Be_Defined_In_ThemeDictionaries() { var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" @@ -370,7 +373,7 @@ public class ThemeDictionariesTests : XamlTestBase White - + Pink @@ -380,9 +383,9 @@ public class ThemeDictionariesTests : XamlTestBase "); var border = (Border)themeVariantScope.Child!; - - themeVariantScope.RequestedThemeVariant = new ThemeVariant("Custom"); + themeVariantScope.RequestedThemeVariant = Custom; + Assert.Equal(Colors.Pink, ((ISolidColorBrush)border.Background)!.Color); } From 6ded9c6e7fd091ca575569780c17969a67713a9d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 1 Feb 2023 14:43:23 +0100 Subject: [PATCH 027/185] Update actual window state after showing window. --- native/Avalonia.Native/src/OSX/WindowImpl.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index f345043f61..ce82f7d83f 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -66,7 +66,7 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) { _isModal = isDialog; WindowBaseImpl::Show(activate, isDialog); - + GetWindowState(&_actualWindowState); HideOrShowTrafficLights(); return SetWindowState(_lastWindowState); From ffcec80f80cd705b9300cdd7783937a89f312cf9 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 1 Feb 2023 16:31:26 +0100 Subject: [PATCH 028/185] fix(GpuInterop): CS0618 member is obsolete, due to the renaming of some members of the Silk.NET.Vulkan enums --- .../VulkanDemo/VulkanBufferHelper.cs | 6 +-- .../VulkanDemo/VulkanCommandBufferPool.cs | 7 ++- .../GpuInterop/VulkanDemo/VulkanContent.cs | 50 +++++++++---------- .../GpuInterop/VulkanDemo/VulkanContext.cs | 12 ++--- samples/GpuInterop/VulkanDemo/VulkanImage.cs | 18 +++---- .../VulkanDemo/VulkanMemoryHelper.cs | 8 +-- .../GpuInterop/VulkanDemo/VulkanSwapchain.cs | 5 +- 7 files changed, 51 insertions(+), 55 deletions(-) diff --git a/samples/GpuInterop/VulkanDemo/VulkanBufferHelper.cs b/samples/GpuInterop/VulkanDemo/VulkanBufferHelper.cs index 949a951a36..290eb06b1e 100644 --- a/samples/GpuInterop/VulkanDemo/VulkanBufferHelper.cs +++ b/samples/GpuInterop/VulkanDemo/VulkanBufferHelper.cs @@ -38,8 +38,8 @@ static class VulkanBufferHelper MemoryTypeIndex = (uint)FindSuitableMemoryTypeIndex(api, physicalDevice, memoryRequirements.MemoryTypeBits, - MemoryPropertyFlags.MemoryPropertyHostCoherentBit | - MemoryPropertyFlags.MemoryPropertyHostVisibleBit) + MemoryPropertyFlags.HostCoherentBit | + MemoryPropertyFlags.HostVisibleBit) }; api.AllocateMemory(device, memoryAllocateInfo, null, out memory).ThrowOnError(); @@ -77,4 +77,4 @@ static class VulkanBufferHelper return -1; } -} \ No newline at end of file +} diff --git a/samples/GpuInterop/VulkanDemo/VulkanCommandBufferPool.cs b/samples/GpuInterop/VulkanDemo/VulkanCommandBufferPool.cs index 65a05c5226..2f018171dc 100644 --- a/samples/GpuInterop/VulkanDemo/VulkanCommandBufferPool.cs +++ b/samples/GpuInterop/VulkanDemo/VulkanCommandBufferPool.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Avalonia.Input; using Silk.NET.Vulkan; using SilkNetDemo; @@ -25,7 +24,7 @@ namespace Avalonia.Vulkan var commandPoolCreateInfo = new CommandPoolCreateInfo { SType = StructureType.CommandPoolCreateInfo, - Flags = CommandPoolCreateFlags.CommandPoolCreateResetCommandBufferBit, + Flags = CommandPoolCreateFlags.ResetCommandBufferBit, QueueFamilyIndex = queueFamilyIndex }; @@ -109,7 +108,7 @@ namespace Avalonia.Vulkan var fenceCreateInfo = new FenceCreateInfo() { SType = StructureType.FenceCreateInfo, - Flags = FenceCreateFlags.FenceCreateSignaledBit + Flags = FenceCreateFlags.SignaledBit }; api.CreateFence(device, fenceCreateInfo, null, out _fence); @@ -134,7 +133,7 @@ namespace Avalonia.Vulkan var beginInfo = new CommandBufferBeginInfo { SType = StructureType.CommandBufferBeginInfo, - Flags = CommandBufferUsageFlags.CommandBufferUsageOneTimeSubmitBit + Flags = CommandBufferUsageFlags.OneTimeSubmitBit }; _api.BeginCommandBuffer(InternalHandle, beginInfo); diff --git a/samples/GpuInterop/VulkanDemo/VulkanContent.cs b/samples/GpuInterop/VulkanDemo/VulkanContent.cs index b16343190a..5805604f1b 100644 --- a/samples/GpuInterop/VulkanDemo/VulkanContent.cs +++ b/samples/GpuInterop/VulkanDemo/VulkanContent.cs @@ -208,7 +208,7 @@ unsafe class VulkanContent : IDisposable api.CmdBindDescriptorSets(commandBufferHandle, PipelineBindPoint.Graphics, _pipelineLayout,0,1, &dset, null); - api.CmdPushConstants(commandBufferHandle, _pipelineLayout, ShaderStageFlags.ShaderStageVertexBit | ShaderStageFlags.FragmentBit, 0, + api.CmdPushConstants(commandBufferHandle, _pipelineLayout, ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit, 0, (uint)Marshal.SizeOf(), &vertexConstant); api.CmdBindVertexBuffers(commandBufferHandle, 0, 1, _vertexBuffer, 0); api.CmdBindIndexBuffer(commandBufferHandle, _indexBuffer, 0, IndexType.Uint16); @@ -237,14 +237,14 @@ unsafe class VulkanContent : IDisposable SrcSubresource = new ImageSubresourceLayers { - AspectMask = ImageAspectFlags.ImageAspectColorBit, + AspectMask = ImageAspectFlags.ColorBit, BaseArrayLayer = 0, LayerCount = 1, MipLevel = 0 }, DstSubresource = new ImageSubresourceLayers { - AspectMask = ImageAspectFlags.ImageAspectColorBit, + AspectMask = ImageAspectFlags.ColorBit, BaseArrayLayer = 0, LayerCount = 1, MipLevel = 0 @@ -326,19 +326,19 @@ unsafe class VulkanContent : IDisposable var imageCreateInfo = new ImageCreateInfo { SType = StructureType.ImageCreateInfo, - ImageType = ImageType.ImageType2D, + ImageType = ImageType.Type2D, Format = Format.D32Sfloat, Extent = new Extent3D((uint?)size.Width, (uint?)size.Height, 1), MipLevels = 1, ArrayLayers = 1, - Samples = SampleCountFlags.SampleCount1Bit, + Samples = SampleCountFlags.Count1Bit, Tiling = ImageTiling.Optimal, - Usage = ImageUsageFlags.ImageUsageDepthStencilAttachmentBit, + Usage = ImageUsageFlags.DepthStencilAttachmentBit, SharingMode = SharingMode.Exclusive, InitialLayout = ImageLayout.Undefined, - Flags = ImageCreateFlags.ImageCreateMutableFormatBit + Flags = ImageCreateFlags.CreateMutableFormatBit }; var api = _context.Api; @@ -355,7 +355,7 @@ unsafe class VulkanContent : IDisposable AllocationSize = memoryRequirements.Size, MemoryTypeIndex = (uint)FindSuitableMemoryTypeIndex(api, _context.PhysicalDevice, - memoryRequirements.MemoryTypeBits, MemoryPropertyFlags.MemoryPropertyDeviceLocalBit) + memoryRequirements.MemoryTypeBits, MemoryPropertyFlags.DeviceLocalBit) }; api.AllocateMemory(device, memoryAllocateInfo, null, @@ -369,14 +369,14 @@ unsafe class VulkanContent : IDisposable ComponentSwizzle.B, ComponentSwizzle.A); - var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.ImageAspectDepthBit, + var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.DepthBit, 0, 1, 0, 1); var imageViewCreateInfo = new ImageViewCreateInfo { SType = StructureType.ImageViewCreateInfo, Image = _depthImage, - ViewType = ImageViewType.ImageViewType2D, + ViewType = ImageViewType.Type2D, Format = Format.D32Sfloat, Components = componentMapping, SubresourceRange = subresourceRange @@ -406,7 +406,7 @@ unsafe class VulkanContent : IDisposable var colorAttachment = new AttachmentDescription() { Format = Format.R8G8B8A8Unorm, - Samples = SampleCountFlags.SampleCount1Bit, + Samples = SampleCountFlags.Count1Bit, LoadOp = AttachmentLoadOp.Clear, StoreOp = AttachmentStoreOp.Store, InitialLayout = ImageLayout.Undefined, @@ -418,7 +418,7 @@ unsafe class VulkanContent : IDisposable var depthAttachment = new AttachmentDescription() { Format = Format.D32Sfloat, - Samples = SampleCountFlags.SampleCount1Bit, + Samples = SampleCountFlags.Count1Bit, LoadOp = AttachmentLoadOp.Clear, StoreOp = AttachmentStoreOp.DontCare, InitialLayout = ImageLayout.Undefined, @@ -431,10 +431,10 @@ unsafe class VulkanContent : IDisposable { SrcSubpass = Vk.SubpassExternal, DstSubpass = 0, - SrcStageMask = PipelineStageFlags.PipelineStageColorAttachmentOutputBit, + SrcStageMask = PipelineStageFlags.ColorAttachmentOutputBit, SrcAccessMask = 0, - DstStageMask = PipelineStageFlags.PipelineStageColorAttachmentOutputBit, - DstAccessMask = AccessFlags.AccessColorAttachmentWriteBit + DstStageMask = PipelineStageFlags.ColorAttachmentOutputBit, + DstAccessMask = AccessFlags.ColorAttachmentWriteBit }; var colorAttachmentReference = new AttachmentReference() @@ -498,14 +498,14 @@ unsafe class VulkanContent : IDisposable var vertShaderStageInfo = new PipelineShaderStageCreateInfo() { SType = StructureType.PipelineShaderStageCreateInfo, - Stage = ShaderStageFlags.ShaderStageVertexBit, + Stage = ShaderStageFlags.VertexBit, Module = _vertShader, PName = (byte*)pname, }; var fragShaderStageInfo = new PipelineShaderStageCreateInfo() { SType = StructureType.PipelineShaderStageCreateInfo, - Stage = ShaderStageFlags.ShaderStageFragmentBit, + Stage = ShaderStageFlags.FragmentBit, Module = _fragShader, PName = (byte*)pname, }; @@ -564,7 +564,7 @@ unsafe class VulkanContent : IDisposable RasterizerDiscardEnable = false, PolygonMode = PolygonMode.Fill, LineWidth = 1, - CullMode = CullModeFlags.CullModeNone, + CullMode = CullModeFlags.None, DepthBiasEnable = false }; @@ -572,7 +572,7 @@ unsafe class VulkanContent : IDisposable { SType = StructureType.PipelineMultisampleStateCreateInfo, SampleShadingEnable = false, - RasterizationSamples = SampleCountFlags.SampleCount1Bit + RasterizationSamples = SampleCountFlags.Count1Bit }; var depthStencilCreateInfo = new PipelineDepthStencilStateCreateInfo() @@ -587,10 +587,10 @@ unsafe class VulkanContent : IDisposable var colorBlendAttachmentState = new PipelineColorBlendAttachmentState() { - ColorWriteMask = ColorComponentFlags.ColorComponentABit | - ColorComponentFlags.ColorComponentRBit | - ColorComponentFlags.ColorComponentGBit | - ColorComponentFlags.ColorComponentBBit, + ColorWriteMask = ColorComponentFlags.ABit | + ColorComponentFlags.RBit | + ColorComponentFlags.GBit | + ColorComponentFlags.BBit, BlendEnable = false }; @@ -617,14 +617,14 @@ unsafe class VulkanContent : IDisposable { Offset = 0, Size = (uint)Marshal.SizeOf(), - StageFlags = ShaderStageFlags.ShaderStageVertexBit + StageFlags = ShaderStageFlags.VertexBit }; var fragPushConstantRange = new PushConstantRange() { //Offset = vertexPushConstantRange.Size, Size = (uint)Marshal.SizeOf(), - StageFlags = ShaderStageFlags.ShaderStageFragmentBit + StageFlags = ShaderStageFlags.FragmentBit }; var layoutBindingInfo = new DescriptorSetLayoutBinding diff --git a/samples/GpuInterop/VulkanDemo/VulkanContext.cs b/samples/GpuInterop/VulkanDemo/VulkanContext.cs index 56041d6965..3fdd9695f2 100644 --- a/samples/GpuInterop/VulkanDemo/VulkanContext.cs +++ b/samples/GpuInterop/VulkanDemo/VulkanContext.cs @@ -86,12 +86,12 @@ public unsafe class VulkanContext : IDisposable var debugCreateInfo = new DebugUtilsMessengerCreateInfoEXT { SType = StructureType.DebugUtilsMessengerCreateInfoExt, - MessageSeverity = DebugUtilsMessageSeverityFlagsEXT.DebugUtilsMessageSeverityVerboseBitExt | - DebugUtilsMessageSeverityFlagsEXT.DebugUtilsMessageSeverityWarningBitExt | - DebugUtilsMessageSeverityFlagsEXT.DebugUtilsMessageSeverityErrorBitExt, - MessageType = DebugUtilsMessageTypeFlagsEXT.DebugUtilsMessageTypeGeneralBitExt | - DebugUtilsMessageTypeFlagsEXT.DebugUtilsMessageTypeValidationBitExt | - DebugUtilsMessageTypeFlagsEXT.DebugUtilsMessageTypePerformanceBitExt, + MessageSeverity = DebugUtilsMessageSeverityFlagsEXT.VerboseBitExt | + DebugUtilsMessageSeverityFlagsEXT.WarningBitExt | + DebugUtilsMessageSeverityFlagsEXT.ErrorBitExt, + MessageType = DebugUtilsMessageTypeFlagsEXT.GeneralBitExt | + DebugUtilsMessageTypeFlagsEXT.ValidationBitExt | + DebugUtilsMessageTypeFlagsEXT.PerformanceBitExt, PfnUserCallback = new PfnDebugUtilsMessengerCallbackEXT(LogCallback), }; diff --git a/samples/GpuInterop/VulkanDemo/VulkanImage.cs b/samples/GpuInterop/VulkanDemo/VulkanImage.cs index e8854bfeb2..59b2ef7e30 100644 --- a/samples/GpuInterop/VulkanDemo/VulkanImage.cs +++ b/samples/GpuInterop/VulkanDemo/VulkanImage.cs @@ -54,8 +54,8 @@ public unsafe class VulkanImage : IDisposable Size = size; MipLevels = 1;//mipLevels; _imageUsageFlags = - ImageUsageFlags.ImageUsageColorAttachmentBit | ImageUsageFlags.ImageUsageTransferDstBit | - ImageUsageFlags.ImageUsageTransferSrcBit | ImageUsageFlags.ImageUsageSampledBit; + ImageUsageFlags.ColorAttachmentBit | ImageUsageFlags.TransferDstBit | + ImageUsageFlags.TransferSrcBit | ImageUsageFlags.SampledBit; //MipLevels = MipLevels != 0 ? MipLevels : (uint)Math.Floor(Math.Log(Math.Max(Size.Width, Size.Height), 2)); @@ -72,19 +72,19 @@ public unsafe class VulkanImage : IDisposable { PNext = exportable ? &externalMemoryCreateInfo : null, SType = StructureType.ImageCreateInfo, - ImageType = ImageType.ImageType2D, + ImageType = ImageType.Type2D, Format = Format, Extent = new Extent3D((uint?)Size.Width, (uint?)Size.Height, 1), MipLevels = MipLevels, ArrayLayers = 1, - Samples = SampleCountFlags.SampleCount1Bit, + Samples = SampleCountFlags.Count1Bit, Tiling = Tiling, Usage = _imageUsageFlags, SharingMode = SharingMode.Exclusive, InitialLayout = ImageLayout.Undefined, - Flags = ImageCreateFlags.ImageCreateMutableFormatBit + Flags = ImageCreateFlags.CreateMutableFormatBit }; Api @@ -128,7 +128,7 @@ public unsafe class VulkanImage : IDisposable MemoryTypeIndex = (uint)VulkanMemoryHelper.FindSuitableMemoryTypeIndex( Api, _physicalDevice, - memoryRequirements.MemoryTypeBits, MemoryPropertyFlags.MemoryPropertyDeviceLocalBit) + memoryRequirements.MemoryTypeBits, MemoryPropertyFlags.DeviceLocalBit) }; Api.AllocateMemory(_device, memoryAllocateInfo, null, @@ -146,7 +146,7 @@ public unsafe class VulkanImage : IDisposable ComponentSwizzle.Identity, ComponentSwizzle.Identity); - AspectFlags = ImageAspectFlags.ImageAspectColorBit; + AspectFlags = ImageAspectFlags.ColorBit; var subresourceRange = new ImageSubresourceRange(AspectFlags, 0, MipLevels, 0, 1); @@ -154,7 +154,7 @@ public unsafe class VulkanImage : IDisposable { SType = StructureType.ImageViewCreateInfo, Image = InternalHandle.Value, - ViewType = ImageViewType.ImageViewType2D, + ViewType = ImageViewType.Type2D, Format = Format, Components = componentMapping, SubresourceRange = subresourceRange @@ -168,7 +168,7 @@ public unsafe class VulkanImage : IDisposable _currentLayout = ImageLayout.Undefined; - TransitionLayout(ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessNoneKhr); + TransitionLayout(ImageLayout.ColorAttachmentOptimal, AccessFlags.NoneKhr); } public int ExportFd() diff --git a/samples/GpuInterop/VulkanDemo/VulkanMemoryHelper.cs b/samples/GpuInterop/VulkanDemo/VulkanMemoryHelper.cs index f6778610dc..b7c7b9cf44 100644 --- a/samples/GpuInterop/VulkanDemo/VulkanMemoryHelper.cs +++ b/samples/GpuInterop/VulkanDemo/VulkanMemoryHelper.cs @@ -29,7 +29,7 @@ internal static class VulkanMemoryHelper AccessFlags destinationAccessMask, uint mipLevels) { - var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.ImageAspectColorBit, 0, mipLevels, 0, 1); + var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.ColorBit, 0, mipLevels, 0, 1); var barrier = new ImageMemoryBarrier { @@ -46,8 +46,8 @@ internal static class VulkanMemoryHelper api.CmdPipelineBarrier( commandBuffer, - PipelineStageFlags.PipelineStageAllCommandsBit, - PipelineStageFlags.PipelineStageAllCommandsBit, + PipelineStageFlags.AllCommandsBit, + PipelineStageFlags.AllCommandsBit, 0, 0, null, @@ -56,4 +56,4 @@ internal static class VulkanMemoryHelper 1, barrier); } -} \ No newline at end of file +} diff --git a/samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs b/samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs index 325c815ccb..fc0e98b3e0 100644 --- a/samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs +++ b/samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia; @@ -7,9 +6,7 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Composition; using Avalonia.Vulkan; -using Metsys.Bson; using Silk.NET.Vulkan; -using SkiaSharp; namespace GpuInterop.VulkanDemo; @@ -84,7 +81,7 @@ class VulkanSwapchainImage : ISwapchainImage _image.TransitionLayout(buffer.InternalHandle, ImageLayout.Undefined, AccessFlags.None, - ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessColorAttachmentReadBit); + ImageLayout.ColorAttachmentOptimal, AccessFlags.ColorAttachmentReadBit); if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) buffer.Submit(null,null,null, null, new VulkanCommandBufferPool.VulkanCommandBuffer.KeyedMutexSubmitInfo From 86dc4042ab99b2340365fa57c629c2c2dad95e8f Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 1 Feb 2023 16:52:58 +0100 Subject: [PATCH 029/185] fix: misc warning - Warning CA1815 PlatformGraphicsExternalImageProperties should override Equals Avalonia.Base (net6.0) \src\Avalonia.Base\Platform\PlatformGraphicsExternalMemory.cs 7 N/A - Warning CA1823 Unused field 'ProximityPoints' Avalonia.Controls (net6.0) src\Avalonia.Controls\Presenters\ScrollContentPresenter.cs 18 N/A - Warning CS0169 The field 'VulkanDemoControl._vkInstance' is never used GpuInterop samples\GpuInterop\VulkanDemo\VulkanDemoControl.cs 19 N/A - Warning CS0169 The field 'VulkanDemoControl._api' is never used GpuInterop samples\GpuInterop\VulkanDemo\VulkanDemoControl.cs 20 N/A - Warning CS0219 The variable 'isZooming' is assigned but its value is never used ControlCatalog (netstandard2.0) samples\ControlCatalog\Pages\GesturePage.cs 73 N/A - Warning xUnit2004 Do not use Assert.Equal() to check for boolean conditions. Avalonia.IntegrationTests.Appium tests\Avalonia.IntegrationTests.Appium\WindowTests_MacOS.cs 267 N/A --- samples/ControlCatalog/Pages/GesturePage.cs | 1 - samples/GpuInterop/VulkanDemo/VulkanDemoControl.cs | 12 ------------ .../Platform/PlatformGraphicsExternalMemory.cs | 6 +----- .../Presenters/ScrollContentPresenter.cs | 1 - .../WindowTests_MacOS.cs | 2 +- 5 files changed, 2 insertions(+), 20 deletions(-) diff --git a/samples/ControlCatalog/Pages/GesturePage.cs b/samples/ControlCatalog/Pages/GesturePage.cs index 0bb8f38219..cc4429f414 100644 --- a/samples/ControlCatalog/Pages/GesturePage.cs +++ b/samples/ControlCatalog/Pages/GesturePage.cs @@ -70,7 +70,6 @@ namespace ControlCatalog.Pages _currentScale = 1; Vector3 currentOffset = default; - bool isZooming = false; CompositionVisual? compositionVisual = null; diff --git a/samples/GpuInterop/VulkanDemo/VulkanDemoControl.cs b/samples/GpuInterop/VulkanDemo/VulkanDemoControl.cs index 6a1cd641b3..962b4e433a 100644 --- a/samples/GpuInterop/VulkanDemo/VulkanDemoControl.cs +++ b/samples/GpuInterop/VulkanDemo/VulkanDemoControl.cs @@ -1,24 +1,12 @@ using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia; -using Avalonia.Platform; using Avalonia.Rendering.Composition; -using Silk.NET.Core; -using Silk.NET.Vulkan; -using Silk.NET.Vulkan.Extensions.KHR; -using SilkNetDemo; namespace GpuInterop.VulkanDemo; public class VulkanDemoControl : DrawingSurfaceDemoBase { - private Instance _vkInstance; - private Vk _api; - class VulkanResources : IAsyncDisposable { public VulkanContext Context { get; } diff --git a/src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs b/src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs index cad4ab2051..4b47c93eb5 100644 --- a/src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs +++ b/src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - namespace Avalonia.Platform; -public struct PlatformGraphicsExternalImageProperties +public record struct PlatformGraphicsExternalImageProperties { public int Width { get; set; } public int Height { get; set; } diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 762702efcc..454f7eac9d 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -15,7 +15,6 @@ namespace Avalonia.Controls.Presenters public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider { private const double EdgeDetectionTolerance = 0.1; - private const int ProximityPoints = 10; /// /// Defines the property. diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 6c61a85561..d9817ecdd1 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -264,7 +264,7 @@ namespace Avalonia.IntegrationTests.Appium var secondaryWindow = GetWindow("SecondaryWindow"); var (_, miniaturizeButton, _) = secondaryWindow.GetChromeButtons(); - Assert.Equal(false, miniaturizeButton.Enabled); + Assert.False(miniaturizeButton.Enabled); } } From 6d49ffc95550d29f5c64f874c9163d4c86559016 Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Wed, 1 Feb 2023 17:33:16 +0100 Subject: [PATCH 030/185] Added tests for binding value types to null --- .../Data/BindingTests.cs | 70 ++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index 3ba8e8354d..c312a71d44 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -648,16 +648,69 @@ namespace Avalonia.Markup.UnitTests.Data }; } + [Fact] + public void Binding_Producing_Default_Value_Should_Result_In_Correct_Priority() + { + var defaultValue = StyledPropertyClass.NullableDoubleProperty.GetDefaultValue(typeof(StyledPropertyClass)); + + var vm = new NullableValuesViewModel() { NullableDouble = defaultValue }; + var target = new StyledPropertyClass(); + + target.Bind(StyledPropertyClass.NullableDoubleProperty, new Binding(nameof(NullableValuesViewModel.NullableDouble)) { Source = vm }); + + Assert.Equal(BindingPriority.LocalValue, target.GetDiagnosticInternal(StyledPropertyClass.NullableDoubleProperty).Priority); + Assert.Equal(defaultValue, target.GetValue(StyledPropertyClass.NullableDoubleProperty)); + } + + [Fact] + public void Binding_Non_Nullable_ValueType_To_Null_Reverts_To_Default_Value() + { + var source = new NullableValuesViewModel { NullableDouble = 42 }; + var target = new StyledPropertyClass(); + var binding = new Binding(nameof(source.NullableDouble)) { Source = source }; + + target.Bind(StyledPropertyClass.DoubleValueProperty, binding); + Assert.Equal(42, target.DoubleValue); + + source.NullableDouble = null; + + Assert.Equal(12.3, target.DoubleValue); + } + + [Fact] + public void Binding_Nullable_ValueType_To_Null_Sets_Value_To_Null() + { + var source = new NullableValuesViewModel { NullableDouble = 42 }; + var target = new StyledPropertyClass(); + var binding = new Binding(nameof(source.NullableDouble)) { Source = source }; + + target.Bind(StyledPropertyClass.NullableDoubleProperty, binding); + Assert.Equal(42, target.NullableDouble); + + source.NullableDouble = null; + + Assert.Null(target.NullableDouble); + } + private class StyledPropertyClass : AvaloniaObject { public static readonly StyledProperty DoubleValueProperty = - AvaloniaProperty.Register(nameof(DoubleValue)); + AvaloniaProperty.Register(nameof(DoubleValue), 12.3); public double DoubleValue { get { return GetValue(DoubleValueProperty); } set { SetValue(DoubleValueProperty, value); } } + + public static StyledProperty NullableDoubleProperty = + AvaloniaProperty.Register(nameof(NullableDoubleProperty), -1); + + public double? NullableDouble + { + get => GetValue(NullableDoubleProperty); + set => SetValue(NullableDoubleProperty, value); + } } private class DirectPropertyClass : AvaloniaObject @@ -676,6 +729,21 @@ namespace Avalonia.Markup.UnitTests.Data } } + private class NullableValuesViewModel : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + private double? _nullableDouble; + public double? NullableDouble + { + get => _nullableDouble; set + { + _nullableDouble = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NullableDouble))); + } + } + } + private class TestStackOverflowViewModel : INotifyPropertyChanged { public int SetterInvokedCount { get; private set; } From 796722f31944df2f62c2f56bffeec5ad6e39d32a Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Sat, 21 Jan 2023 16:17:10 +0100 Subject: [PATCH 031/185] Never convert null to UnsetValue in bindings Revert ToggleButton.IsChecked hack --- .../Data/Converters/DefaultValueConverter.cs | 2 +- .../Primitives/ToggleButton.cs | 2 +- .../Data/Core/BindingExpressionTests.cs | 25 ------------------- 3 files changed, 2 insertions(+), 27 deletions(-) diff --git a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs index f5c135459d..aeb71d16ae 100644 --- a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs @@ -30,7 +30,7 @@ namespace Avalonia.Data.Converters { if (value == null) { - return targetType.IsValueType ? AvaloniaProperty.UnsetValue : null; + return null; } if (typeof(ICommand).IsAssignableFrom(targetType) && value is Delegate d && d.Method.GetParameters().Length <= 1) diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index dfb436a55e..158c5d875b 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -20,7 +20,7 @@ namespace Avalonia.Controls.Primitives nameof(IsChecked), o => o.IsChecked, (o, v) => o.IsChecked = v, - unsetValue: null, + unsetValue: false, defaultBindingMode: BindingMode.TwoWay); /// diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs index 339cf8a334..924e844ec5 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs @@ -78,18 +78,6 @@ namespace Avalonia.Base.UnitTests.Data.Core GC.KeepAlive(data); } - [Fact] - public async Task Should_Coerce_Get_Null_Double_String_To_UnsetValue() - { - var data = new Class1 { StringValue = null }; - var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double)); - var result = await target.Take(1); - - Assert.Equal(AvaloniaProperty.UnsetValue, result); - - GC.KeepAlive(data); - } - [Fact] public void Should_Convert_Set_String_To_Double() { @@ -249,19 +237,6 @@ namespace Avalonia.Base.UnitTests.Data.Core GC.KeepAlive(data); } - [Fact] - public void Should_Coerce_Setting_Null_Double_To_Default_Value() - { - var data = new Class1 { DoubleValue = 5.6 }; - var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string)); - - target.OnNext(null); - - Assert.Equal(0, data.DoubleValue); - - GC.KeepAlive(data); - } - [Fact] public void Should_Coerce_Setting_UnsetValue_Double_To_Default_Value() { From f2c3805d6a0086ce27ac9ecb55680def9f31b9ea Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 1 Feb 2023 18:08:13 +0100 Subject: [PATCH 032/185] Update ncrunch config. --- .ncrunch/Avalonia.UnitTests.v3.ncrunchproject | 5 +++++ .ncrunch/GpuInterop.v3.ncrunchproject | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .ncrunch/Avalonia.UnitTests.v3.ncrunchproject create mode 100644 .ncrunch/GpuInterop.v3.ncrunchproject diff --git a/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject b/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject new file mode 100644 index 0000000000..cff5044edf --- /dev/null +++ b/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + False + + \ No newline at end of file diff --git a/.ncrunch/GpuInterop.v3.ncrunchproject b/.ncrunch/GpuInterop.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/GpuInterop.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file From 1e926dac72b4c666d4c213cd72693d4cbf551e27 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 1 Feb 2023 18:19:40 +0100 Subject: [PATCH 033/185] Added failing tests for #10110. --- .../AvaloniaObjectTests_Binding.cs | 18 +++++++++++++ .../Xaml/ResourceDictionaryTests.cs | 25 ++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 030d6ba215..f0386615c5 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -1285,6 +1285,24 @@ namespace Avalonia.Base.UnitTests subscription.Dispose(); } + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + public void Binding_Producing_UnsetValue_Does_Not_Cause_Unsubscribe(BindingPriority priority) + { + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.FooProperty, source, priority); + + source.OnNext("foo"); + Assert.Equal("foo", target.GetValue(Class1.FooProperty)); + source.OnNext(BindingValue.Unset); + Assert.Equal("foodefault", target.GetValue(Class1.FooProperty)); + source.OnNext("bar"); + Assert.Equal("bar", target.GetValue(Class1.FooProperty)); + } + [Fact] public void Produces_Correct_Values_And_Base_Values_With_Multiple_Animation_Bindings() { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs index 6cab83751f..bea6bc4dc8 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs @@ -307,7 +307,30 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.True(buttonResources.ContainsDeferredKey("Red")); } } - + + [Fact] + public void Dynamically_Changing_Referenced_Resources_Works_With_DynamicResource() + { + var xaml = @" + + + Red + + +"; + + var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); + + Assert.Equal(Colors.Red, ((ISolidColorBrush)userControl.FindResource("brush")!).Color); + + userControl.Resources.Remove("color"); + Assert.Equal(default, ((ISolidColorBrush)userControl.FindResource("brush")!).Color); + + userControl.Resources.Add("color", Colors.Blue); + Assert.Equal(Colors.Blue, ((ISolidColorBrush)userControl.FindResource("brush")!).Color); + } + private IDisposable StyledWindow(params (string, string)[] assets) { var services = TestServices.StyledWindow.With( From a22249b898aa621679bc79ea7fb1406a74dd0942 Mon Sep 17 00:00:00 2001 From: workgroupengineering Date: Thu, 2 Feb 2023 09:45:36 +0100 Subject: [PATCH 034/185] fix: remarks --- src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs index 112dd436de..2bfd2080c3 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// The point in global coordinates. /// True if the point hits the node's geometry; otherwise false. /// - /// This method does not recurse to child s, if you want + /// This method does not recurse to childs, if you want /// to hit test children they must be hit tested manually. /// bool HitTest(Point p); From b0a2ae99e44151f58ef6e77ca731f78bd5b3d660 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 2 Feb 2023 10:37:20 +0100 Subject: [PATCH 035/185] Fix default GlyphRun.BaselineOrigin Add a unit test --- src/Avalonia.Base/Media/GlyphRun.cs | 2 +- .../Avalonia.Skia/PlatformRenderInterface.cs | 2 +- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 2 +- .../Controls/TextBlockTests.cs | 51 ++++++++++++++++++ .../Should_Draw_TextDecorations.expected.png | Bin 0 -> 1279 bytes .../Should_Draw_TextDecorations.expected.png | Bin 0 -> 1516 bytes 6 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 tests/TestFiles/Direct2D1/Controls/TextBlock/Should_Draw_TextDecorations.expected.png create mode 100644 tests/TestFiles/Skia/Controls/TextBlock/Should_Draw_TextDecorations.expected.png diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 0ec7152359..2966ceee8d 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -166,7 +166,7 @@ namespace Avalonia.Media /// public Point BaselineOrigin { - get => _baselineOrigin ?? default; + get => PlatformImpl.Item.BaselineOrigin; set => Set(ref _baselineOrigin, value); } diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index d12db39ad6..e795f3d304 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -86,7 +86,7 @@ namespace Avalonia.Skia SKPath path = new SKPath(); - var (currentX, currentY) = glyphRun.PlatformImpl.Item.BaselineOrigin; + var (currentX, currentY) = glyphRun.BaselineOrigin; for (var i = 0; i < glyphRun.GlyphInfos.Count; i++) { diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index eb3f9911df..99c01dd111 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -257,7 +257,7 @@ namespace Avalonia.Direct2D1 sink.Close(); } - var (baselineOriginX, baselineOriginY) = glyphRun.PlatformImpl.Item.BaselineOrigin; + var (baselineOriginX, baselineOriginY) = glyphRun.BaselineOrigin; var transformedGeometry = new SharpDX.Direct2D1.TransformedGeometry( Direct2D1Factory, diff --git a/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs b/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs index c11bd2b816..4210ee8238 100644 --- a/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs +++ b/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs @@ -1,3 +1,4 @@ +using System.Net; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Layout; @@ -17,6 +18,56 @@ namespace Avalonia.Direct2D1.RenderTests.Controls { } + [Win32Fact("Has text")] + public async Task Should_Draw_TextDecorations() + { + Border target = new Border + { + Padding = new Thickness(8), + Width = 200, + Height = 30, + Background = Brushes.White, + Child = new TextBlock + { + FontFamily = TestFontFamily, + FontSize = 12, + Foreground = Brushes.Black, + Text = "Neque porro quisquam est qui dolorem", + VerticalAlignment = VerticalAlignment.Top, + TextWrapping = TextWrapping.NoWrap, + TextDecorations = new TextDecorationCollection + { + new TextDecoration + { + Location = TextDecorationLocation.Overline, + StrokeThickness= 1.5, + StrokeThicknessUnit = TextDecorationUnit.Pixel, + Stroke = new SolidColorBrush(Colors.Red) + }, + new TextDecoration + { + Location = TextDecorationLocation.Baseline, + StrokeThickness= 1.5, + StrokeThicknessUnit = TextDecorationUnit.Pixel, + Stroke = new SolidColorBrush(Colors.Green) + }, + new TextDecoration + { + Location = TextDecorationLocation.Underline, + StrokeThickness= 1.5, + StrokeThicknessUnit = TextDecorationUnit.Pixel, + Stroke = new SolidColorBrush(Colors.Blue), + StrokeOffset = 2, + StrokeOffsetUnit = TextDecorationUnit.Pixel + } + } + } + }; + + await RenderToFile(target); + CompareImages(); + } + [Win32Fact("Has text")] public async Task Wrapping_NoWrap() { diff --git a/tests/TestFiles/Direct2D1/Controls/TextBlock/Should_Draw_TextDecorations.expected.png b/tests/TestFiles/Direct2D1/Controls/TextBlock/Should_Draw_TextDecorations.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..494c8a900252641099b21c89d680145adde9046f GIT binary patch literal 1279 zcmVPx#1ZP1_K>z@;j|==^1poj9sYygZRCr$Pnma6QK@^6ELPRvT5|Kza3WZ8UL_9*` z*;1fVNoa0IqtqPH+=fJ@aunp2UWIr>M3f2=uSTJ|2_h=4f9|Yp8T-thvmN(!)|X87 znpt~ZYu1{X?Z|t6K?#BoO1^mtf)IiPh#)wS01*TS5+H)$KmtS%97uo&f&&QMmwmhfq^qk-KRd$d&`nQI>xc)C`MbC#;9Y{Qc9eXt5oiN#Q$6aBh#D*Jm+K*O9Um(; z4k|~=CYe!LzMZJ2lmb*Vj?lkbK~b&>d-0oNhX zT+t`;=jZ1tkw|FSUh8!5b&SF*K+ew2)WE=inwy(5C9hym2p$~uNR^e9fsvu>?CcD5 z^Yin%JPH_=w+SOOH#Zw?G*?$wL0#-HpH> zd3o7r)8q0V3I^UUT_mHz`vd`)w7omR7;p%RNCXia6u?PZ zT3WOWMn*=AP3)5cAXvB!FjimS%(NzUV3n7b2R7igh{EMR2nLK7<@PBV7xE$B z@3xErq`$vkO-@dl+$$7~lzb!E1-^TDn@E`$K9|Ab!(MPcIG25Gi39EGv=(8TMO<9| zg8)GpHZIJ?cazK(83hO&6jy@v^>tICbbfJh(d6EO_i6VAe7DAq!;gO6Mtyy~?jx=- z>^^Ci##s0F_q7brC(1qooDruTA1ii2zDuVgu6=w#`*1G4UN}{_4fwSg@6+xLTvtAq zE)!h-!@+eIryXt=9|3a&z9&-T4~4z+y}Gf!zCNA5xw%oRtE;NDwN+JAROqtJ%}sTE zeXV8C)6=6$N=mf6va+J??(Vc)U0to84Gj(Y$#r#gb?Wr=^j$whSy`Ft=;$!H^q@_= zdvbE3^XQZM1AMG@o2Xk`TN})agL?%?1+_lrL84!$XlkB{|xVfL~~=RcR1muh=^ z+mwd;0QxyPI?^)c!q++-+_#>;yT3ys1LRB3HzR$V$HzzY^z@{Pii%80_PM>iRfUCx zs-U32l>9sV4*S?VJUpoU{CsV*T;#{h#;gzsh^u4Ek3jSWm^CM002ovPDHLkV1l~1QeprA literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Skia/Controls/TextBlock/Should_Draw_TextDecorations.expected.png b/tests/TestFiles/Skia/Controls/TextBlock/Should_Draw_TextDecorations.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..297bd592ff27631f9c0c5738da922f49e7f3dd25 GIT binary patch literal 1516 zcmVR9JKak1zRd8c^6@WOQo{X5km70J(RM=3R0%aLc%IoM2ZM99+FHZ z$z(FwCf#j$AM9cB&-?%X{qOz%dv9ho<@>&mLZOIc$5uO_P;6&Zf+!R*s02|cVo(X9 zP{g1TM4^a5C5S>1gGvyEB8F!M-f`r}V{k*E*dnf9j}@oXi$aB>KZCbebwNIr!jZub zhx74N9bG!AQYp3_ z(zV8_KVgDs%oTNn8=WE2Ns&r5|MG%*ADJTr`KG)s^5nxAC!Nl=&8=O~Her>JP97ng zME-QjBu>gf%23;oQR$>`q{In)a%k^5H|Oi4#HTLu&2|x6tS)~#``Oqc2W^ED^fT6G zx6V-irIW{T8vf*kXvQe{)-UP&>4R+T2p3t1`bE7%vz2u*0-uWLfv*Cy7WOsSa!ulntNESa=+ zVbaM^&|5LM2qIwxPMa+GZJkxy!U;NAMrs%FYb7Xn7`{)fMBjoMCT{*v7dWB#_`Xjt zBzh6wuTjc@UG42>ekP-OVr2qU$V=gsUS9iE=+o%}wO}dVu7bpbPqfvpZK2Nwb zEk{|nK^5yhPz9BuTc#V>WkG@>97$NO=atE6TBNUaS5UT%-c?~3dVH0*Ru0n`oXwk# zPg2q*OMVqQSS8bFI-wXeD){g=C8IeT$IqWIdNKL7E#9Ptjz!XkvIAcf&Wj_g+<@t$ z2iXnq@E$9|W#92ryuPn-&&KV`EZ=OmITR)P5$um_EU8gjGWo)t=jw&aw3;0<3^wld z+F5HUois38vIwSMm}dUXd8CuV#m4&7g4ckVc@_@u5AIv9EOP0m=EcjioHM7GXh^6m zR@igSWNM;U|9WMS%ARwWQNq7h5ubg>ug%Mu`2|8IV+s6`U@x@e^X1PUu>AQ~ed4ph z?aRyD?9eocVIJk+?uXo45x8q@Nq9VzSqjyW~KdSww$Tj22ibt>WkZdT+t--f# S6vVXv0000 Date: Thu, 2 Feb 2023 10:06:57 +0000 Subject: [PATCH 036/185] fix scroll snap points attached properties --- src/Avalonia.Controls/ScrollViewer.cs | 88 +++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 1c23919d0e..ab114da933 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -154,15 +154,15 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty HorizontalSnapPointsTypeProperty = - AvaloniaProperty.Register( + public static readonly AttachedProperty HorizontalSnapPointsTypeProperty = + AvaloniaProperty.RegisterAttached( nameof(HorizontalSnapPointsType)); /// /// Defines the property. /// - public static readonly StyledProperty VerticalSnapPointsTypeProperty = - AvaloniaProperty.Register( + public static readonly AttachedProperty VerticalSnapPointsTypeProperty = + AvaloniaProperty.RegisterAttached( nameof(VerticalSnapPointsType)); /// @@ -625,6 +625,86 @@ namespace Avalonia.Controls control.SetValue(HorizontalScrollBarVisibilityProperty, value); } + /// + /// Gets the value of the HorizontalSnapPointsType attached property. + /// + /// The control to read the value from. + /// The value of the property. + public static SnapPointsType GetHorizontalSnapPointsType(Control control) + { + return control.GetValue(HorizontalSnapPointsTypeProperty); + } + + /// + /// Gets the value of the HorizontalSnapPointsType attached property. + /// + /// The control to set the value on. + /// The value of the property. + public static void SetHorizontalSnapPointsType(Control control, SnapPointsType value) + { + control.SetValue(HorizontalSnapPointsTypeProperty, value); + } + + /// + /// Gets the value of the VerticalSnapPointsType attached property. + /// + /// The control to read the value from. + /// The value of the property. + public static SnapPointsType GetVerticalSnapPointsType(Control control) + { + return control.GetValue(VerticalSnapPointsTypeProperty); + } + + /// + /// Gets the value of the VerticalSnapPointsType attached property. + /// + /// The control to set the value on. + /// The value of the property. + public static void SetVerticalSnapPointsType(Control control, SnapPointsType value) + { + control.SetValue(VerticalSnapPointsTypeProperty, value); + } + + /// + /// Gets the value of the HorizontalSnapPointsAlignment attached property. + /// + /// The control to read the value from. + /// The value of the property. + public static SnapPointsAlignment GetHorizontalSnapPointsAlignment(Control control) + { + return control.GetValue(HorizontalSnapPointsAlignmentProperty); + } + + /// + /// Gets the value of the HorizontalSnapPointsAlignment attached property. + /// + /// The control to set the value on. + /// The value of the property. + public static void SetHorizontalSnapPointsAlignment(Control control, SnapPointsAlignment value) + { + control.SetValue(HorizontalSnapPointsAlignmentProperty, value); + } + + /// + /// Gets the value of the VerticalSnapPointsAlignment attached property. + /// + /// The control to read the value from. + /// The value of the property. + public static SnapPointsAlignment GetVerticalSnapPointsAlignment(Control control) + { + return control.GetValue(VerticalSnapPointsAlignmentProperty); + } + + /// + /// Gets the value of the VerticalSnapPointsAlignment attached property. + /// + /// The control to set the value on. + /// The value of the property. + public static void SetVerticalSnapPointsAlignment(Control control, SnapPointsAlignment value) + { + control.SetValue(VerticalSnapPointsAlignmentProperty, value); + } + /// /// Gets the value of the VerticalScrollBarVisibility attached property. /// From cd90a9c0c239c4a67ae8dd348358f52b56658084 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 2 Feb 2023 14:21:22 +0100 Subject: [PATCH 037/185] feat(HamburgerMenu): Auto close on SelectedItem Changed when HamburgerMenu is display as Overlay --- samples/ControlCatalog/MainView.xaml | 4 ++-- .../SampleControls/HamburgerMenu/HamburgerMenu.cs | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 0695d9d17a..3681298a72 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -14,8 +14,8 @@ - - + + diff --git a/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs b/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs index ab61dcde91..7ff8160720 100644 --- a/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs +++ b/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs @@ -52,6 +52,14 @@ namespace ControlSamples var (oldBounds, newBounds) = change.GetOldAndNewValue(); EnsureSplitViewMode(oldBounds, newBounds); } + + if (change.Property == SelectedItemProperty) + { + if (_splitView is not null && _splitView.DisplayMode == SplitViewDisplayMode.Overlay) + { + _splitView.SetValue(SplitView.IsPaneOpenProperty, false, Avalonia.Data.BindingPriority.Animation); + } + } } private void EnsureSplitViewMode(Rect oldBounds, Rect newBounds) @@ -60,12 +68,12 @@ namespace ControlSamples { var threshold = ExpandedModeThresholdWidth; - if (newBounds.Width >= threshold && oldBounds.Width < threshold) + if (newBounds.Width >= threshold) { _splitView.DisplayMode = SplitViewDisplayMode.Inline; _splitView.IsPaneOpen = true; } - else if (newBounds.Width < threshold && oldBounds.Width >= threshold) + else if (newBounds.Width < threshold) { _splitView.DisplayMode = SplitViewDisplayMode.Overlay; _splitView.IsPaneOpen = false; From 69b3db42a2f25e0dd878918911ec136565bbd7e2 Mon Sep 17 00:00:00 2001 From: Dmitry Zhelnin Date: Thu, 2 Feb 2023 13:20:17 +0300 Subject: [PATCH 038/185] Cleanup attributes Enable rules CA1018: Mark attributes with AttributeUsageAttribute; CA1813: Avoid unsealed attributes --- .editorconfig | 4 ++++ src/Avalonia.Base/Metadata/AmbientAttribute.cs | 4 ++-- src/Avalonia.Base/Metadata/ContentAttribute.cs | 2 +- src/Avalonia.Base/Metadata/DataTypeAttribute.cs | 4 ++-- src/Avalonia.Base/Metadata/DependsOnAttribute.cs | 2 +- .../Metadata/NotClientImplementableAttribute.cs | 2 +- src/Avalonia.Base/Metadata/TemplateContent.cs | 2 +- .../Metadata/TrimSurroundingWhitespaceAttribute.cs | 2 +- src/Avalonia.Base/Metadata/UnstableAttribute.cs | 3 ++- .../Metadata/UsableDuringInitializationAttribute.cs | 4 ++-- .../WhitespaceSignificantCollectionAttribute.cs | 2 +- .../Metadata/XmlnsDefinitionAttribute.cs | 2 +- .../Rendering/Composition/Expressions/Expression.cs | 3 ++- .../Platform/ExportAvaloniaModuleAttribute.cs | 2 +- src/Avalonia.Controls/ResolveByNameAttribute.cs | 3 ++- src/Avalonia.OpenGL/GlEntryPointAttribute.cs | 4 ++-- .../AvaloniaRemoteMessageGuidAttribute.cs | 2 +- src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs | 3 ++- src/Shared/ModuleInitializer.cs | 3 ++- src/Shared/SourceGeneratorAttributes.cs | 11 ++++++++--- 20 files changed, 39 insertions(+), 25 deletions(-) diff --git a/.editorconfig b/.editorconfig index eac5870f96..e6ae266849 100644 --- a/.editorconfig +++ b/.editorconfig @@ -145,10 +145,14 @@ dotnet_diagnostic.CS1591.severity = suggestion # CS0162: Remove unreachable code dotnet_diagnostic.CS0162.severity = error +# CA1018: Mark attributes with AttributeUsageAttribute +dotnet_diagnostic.CA1018.severity = error # CA1304: Specify CultureInfo dotnet_diagnostic.CA1304.severity = warning # CA1802: Use literals where appropriate dotnet_diagnostic.CA1802.severity = warning +# CA1813: Avoid unsealed attributes +dotnet_diagnostic.CA1813.severity = error # CA1815: Override equals and operator equals on value types dotnet_diagnostic.CA1815.severity = warning # CA1820: Test for empty strings using string length diff --git a/src/Avalonia.Base/Metadata/AmbientAttribute.cs b/src/Avalonia.Base/Metadata/AmbientAttribute.cs index 85ca6c4ec9..1c85a67641 100644 --- a/src/Avalonia.Base/Metadata/AmbientAttribute.cs +++ b/src/Avalonia.Base/Metadata/AmbientAttribute.cs @@ -3,10 +3,10 @@ using System; namespace Avalonia.Metadata { /// - /// Defines the ambient class/property + /// Defines the ambient class/property /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, Inherited = true)] - public class AmbientAttribute : Attribute + public sealed class AmbientAttribute : Attribute { } } diff --git a/src/Avalonia.Base/Metadata/ContentAttribute.cs b/src/Avalonia.Base/Metadata/ContentAttribute.cs index a0b2fa0e1d..f32c8e78f6 100644 --- a/src/Avalonia.Base/Metadata/ContentAttribute.cs +++ b/src/Avalonia.Base/Metadata/ContentAttribute.cs @@ -6,7 +6,7 @@ namespace Avalonia.Metadata /// Defines the property that contains the object's content in markup. /// [AttributeUsage(AttributeTargets.Property)] - public class ContentAttribute : Attribute + public sealed class ContentAttribute : Attribute { } } diff --git a/src/Avalonia.Base/Metadata/DataTypeAttribute.cs b/src/Avalonia.Base/Metadata/DataTypeAttribute.cs index ac46a0d30a..dd9603b4a9 100644 --- a/src/Avalonia.Base/Metadata/DataTypeAttribute.cs +++ b/src/Avalonia.Base/Metadata/DataTypeAttribute.cs @@ -9,7 +9,7 @@ namespace Avalonia.Metadata; /// Used on DataTemplate.DataType property so it can be inherited in compiled bindings inside of the template. /// [AttributeUsage(AttributeTargets.Property)] -public class DataTypeAttribute : Attribute +public sealed class DataTypeAttribute : Attribute { - + } diff --git a/src/Avalonia.Base/Metadata/DependsOnAttribute.cs b/src/Avalonia.Base/Metadata/DependsOnAttribute.cs index caee71ebfd..ca58a91eb9 100644 --- a/src/Avalonia.Base/Metadata/DependsOnAttribute.cs +++ b/src/Avalonia.Base/Metadata/DependsOnAttribute.cs @@ -6,7 +6,7 @@ namespace Avalonia.Metadata /// Indicates that the property depends on the value of another property in markup. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] - public class DependsOnAttribute : Attribute + public sealed class DependsOnAttribute : Attribute { /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs b/src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs index 348c983c03..75fe7b8031 100644 --- a/src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs +++ b/src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs @@ -11,7 +11,7 @@ namespace Avalonia.Metadata /// may be added to its API. /// [AttributeUsage(AttributeTargets.Interface)] - public class NotClientImplementableAttribute : Attribute + public sealed class NotClientImplementableAttribute : Attribute { } } diff --git a/src/Avalonia.Base/Metadata/TemplateContent.cs b/src/Avalonia.Base/Metadata/TemplateContent.cs index 258154aba4..78bcc2ff29 100644 --- a/src/Avalonia.Base/Metadata/TemplateContent.cs +++ b/src/Avalonia.Base/Metadata/TemplateContent.cs @@ -6,7 +6,7 @@ namespace Avalonia.Metadata /// Defines the property that contains the object's content in markup. /// [AttributeUsage(AttributeTargets.Property)] - public class TemplateContentAttribute : Attribute + public sealed class TemplateContentAttribute : Attribute { public Type? TemplateResultType { get; set; } } diff --git a/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs b/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs index c46891b3ad..a644c9afe6 100644 --- a/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs +++ b/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs @@ -3,7 +3,7 @@ namespace Avalonia.Metadata { [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] - public class TrimSurroundingWhitespaceAttribute : Attribute + public sealed class TrimSurroundingWhitespaceAttribute : Attribute { } diff --git a/src/Avalonia.Base/Metadata/UnstableAttribute.cs b/src/Avalonia.Base/Metadata/UnstableAttribute.cs index 3b6fa5168a..361f6d30fd 100644 --- a/src/Avalonia.Base/Metadata/UnstableAttribute.cs +++ b/src/Avalonia.Base/Metadata/UnstableAttribute.cs @@ -6,7 +6,8 @@ namespace Avalonia.Metadata /// This API is unstable and is not covered by API compatibility guarantees between minor and /// patch releases. /// - public class UnstableAttribute : Attribute + [AttributeUsage(AttributeTargets.All)] + public sealed class UnstableAttribute : Attribute { } } diff --git a/src/Avalonia.Base/Metadata/UsableDuringInitializationAttribute.cs b/src/Avalonia.Base/Metadata/UsableDuringInitializationAttribute.cs index 753a96b9ce..d2d163b368 100644 --- a/src/Avalonia.Base/Metadata/UsableDuringInitializationAttribute.cs +++ b/src/Avalonia.Base/Metadata/UsableDuringInitializationAttribute.cs @@ -3,8 +3,8 @@ using System; namespace Avalonia.Metadata { [AttributeUsage(AttributeTargets.Class)] - public class UsableDuringInitializationAttribute : Attribute + public sealed class UsableDuringInitializationAttribute : Attribute { - + } } diff --git a/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs b/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs index aeaa38dad9..2fd2b1da3b 100644 --- a/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs +++ b/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs @@ -6,7 +6,7 @@ namespace Avalonia.Metadata /// Indicates that a collection type should be processed as being whitespace significant by a XAML processor. /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] - public class WhitespaceSignificantCollectionAttribute : Attribute + public sealed class WhitespaceSignificantCollectionAttribute : Attribute { } } diff --git a/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs b/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs index d43fa55f5c..c6b79ba987 100644 --- a/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs +++ b/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs @@ -6,7 +6,7 @@ namespace Avalonia.Metadata /// Maps an XML namespace to a CLR namespace for use in XAML. /// [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public class XmlnsDefinitionAttribute : Attribute + public sealed class XmlnsDefinitionAttribute : Attribute { /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs index ff2069e71e..560ee05c10 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs @@ -39,7 +39,8 @@ namespace Avalonia.Rendering.Composition.Expressions } } - internal class PrettyPrintStringAttribute : Attribute + [AttributeUsage(AttributeTargets.Field)] + internal sealed class PrettyPrintStringAttribute : Attribute { public string Name { get; } diff --git a/src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs b/src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs index 5a34c5c0e1..f271abb59a 100644 --- a/src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs +++ b/src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs @@ -41,7 +41,7 @@ namespace Avalonia.Platform /// The fallback module will only be initialized if the Skia-specific module is not applicable. /// [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public class ExportAvaloniaModuleAttribute : Attribute + public sealed class ExportAvaloniaModuleAttribute : Attribute { public ExportAvaloniaModuleAttribute(string name, Type moduleType) { diff --git a/src/Avalonia.Controls/ResolveByNameAttribute.cs b/src/Avalonia.Controls/ResolveByNameAttribute.cs index a13b10d630..3c56c20db0 100644 --- a/src/Avalonia.Controls/ResolveByNameAttribute.cs +++ b/src/Avalonia.Controls/ResolveByNameAttribute.cs @@ -7,7 +7,8 @@ namespace Avalonia.Controls /// When applying this to attached properties, ensure to put on both /// the Getter and Setter methods. /// - public class ResolveByNameAttribute : Attribute + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] + public sealed class ResolveByNameAttribute : Attribute { } } diff --git a/src/Avalonia.OpenGL/GlEntryPointAttribute.cs b/src/Avalonia.OpenGL/GlEntryPointAttribute.cs index 3e31de6995..386db30f92 100644 --- a/src/Avalonia.OpenGL/GlEntryPointAttribute.cs +++ b/src/Avalonia.OpenGL/GlEntryPointAttribute.cs @@ -3,7 +3,7 @@ using System; namespace Avalonia.OpenGL { [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - class GlMinVersionEntryPoint : Attribute + sealed class GlMinVersionEntryPoint : Attribute { public GlMinVersionEntryPoint(string entry, int minVersionMajor, int minVersionMinor) { @@ -28,7 +28,7 @@ namespace Avalonia.OpenGL } [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - class GlExtensionEntryPoint : Attribute + sealed class GlExtensionEntryPoint : Attribute { public GlExtensionEntryPoint(string entry, string extension) { diff --git a/src/Avalonia.Remote.Protocol/AvaloniaRemoteMessageGuidAttribute.cs b/src/Avalonia.Remote.Protocol/AvaloniaRemoteMessageGuidAttribute.cs index 98a843bad1..44605a2ffb 100644 --- a/src/Avalonia.Remote.Protocol/AvaloniaRemoteMessageGuidAttribute.cs +++ b/src/Avalonia.Remote.Protocol/AvaloniaRemoteMessageGuidAttribute.cs @@ -3,7 +3,7 @@ namespace Avalonia.Remote.Protocol { [AttributeUsage(AttributeTargets.Class)] - public class AvaloniaRemoteMessageGuidAttribute : Attribute + public sealed class AvaloniaRemoteMessageGuidAttribute : Attribute { public Guid Guid { get; } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs index 8d6f8cdf3a..da4d7374d4 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs @@ -34,7 +34,8 @@ namespace Avalonia.Markup.Xaml } - public class ConstructorArgumentAttribute : Attribute + [AttributeUsage(AttributeTargets.Property)] + public sealed class ConstructorArgumentAttribute : Attribute { public ConstructorArgumentAttribute(string name) { diff --git a/src/Shared/ModuleInitializer.cs b/src/Shared/ModuleInitializer.cs index a72929e06f..e58b296474 100644 --- a/src/Shared/ModuleInitializer.cs +++ b/src/Shared/ModuleInitializer.cs @@ -1,7 +1,8 @@ namespace System.Runtime.CompilerServices { #if NETSTANDARD2_0 - internal class ModuleInitializerAttribute : Attribute + [AttributeUsage(AttributeTargets.Method)] + internal sealed class ModuleInitializerAttribute : Attribute { } diff --git a/src/Shared/SourceGeneratorAttributes.cs b/src/Shared/SourceGeneratorAttributes.cs index 3f00fbef57..bdd21d0426 100644 --- a/src/Shared/SourceGeneratorAttributes.cs +++ b/src/Shared/SourceGeneratorAttributes.cs @@ -16,7 +16,9 @@ namespace Avalonia.SourceGenerator } - internal class GetProcAddressAttribute : Attribute + + [AttributeUsage(AttributeTargets.Method)] + internal sealed class GetProcAddressAttribute : Attribute { public GetProcAddressAttribute(string proc) { @@ -39,11 +41,14 @@ namespace Avalonia.SourceGenerator } } - internal class GenerateEnumValueDictionaryAttribute : Attribute + [AttributeUsage(AttributeTargets.Method)] + internal sealed class GenerateEnumValueDictionaryAttribute : Attribute { } - internal class GenerateEnumValueListAttribute : Attribute + + [AttributeUsage(AttributeTargets.Method)] + internal sealed class GenerateEnumValueListAttribute : Attribute { } } From 936d4c3448108f793002adb87f09f1eede21dbb1 Mon Sep 17 00:00:00 2001 From: Dmitry Zhelnin Date: Thu, 2 Feb 2023 18:15:35 +0300 Subject: [PATCH 039/185] Animatable: handle transitions subscription when contol is added or removed from visual tree --- src/Avalonia.Base/Animation/Animatable.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Animation/Animatable.cs b/src/Avalonia.Base/Animation/Animatable.cs index edaa76233e..eddb89c3e8 100644 --- a/src/Avalonia.Base/Animation/Animatable.cs +++ b/src/Avalonia.Base/Animation/Animatable.cs @@ -27,6 +27,7 @@ namespace Avalonia.Animation AvaloniaProperty.Register(nameof(Transitions)); private bool _transitionsEnabled = true; + private bool _isSubscribedToTransitionsCollection = false; private Dictionary? _transitionState; /// @@ -62,6 +63,11 @@ namespace Avalonia.Animation if (Transitions is object) { + if (!_isSubscribedToTransitionsCollection) + { + _isSubscribedToTransitionsCollection = true; + Transitions.CollectionChanged += TransitionsCollectionChanged; + } AddTransitions(Transitions); } } @@ -72,7 +78,7 @@ namespace Avalonia.Animation /// /// /// This method should not be called from user code, it will be called automatically by the framework - /// when a control is added to the visual tree. + /// when a control is removed from the visual tree. /// protected void DisableTransitions() { @@ -82,6 +88,11 @@ namespace Avalonia.Animation if (Transitions is object) { + if (_isSubscribedToTransitionsCollection) + { + _isSubscribedToTransitionsCollection = false; + Transitions.CollectionChanged -= TransitionsCollectionChanged; + } RemoveTransitions(Transitions); } } @@ -110,6 +121,7 @@ namespace Avalonia.Animation } newTransitions.CollectionChanged += TransitionsCollectionChanged; + _isSubscribedToTransitionsCollection = true; AddTransitions(toAdd); } From 7b00ef69892358650c10e0f27fb0d5dd1477e3f5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Feb 2023 12:00:40 +0100 Subject: [PATCH 040/185] Make untyped bindings produce default value. This changes the behavior of bindings slightly, in that previously a binding which produced `UnsetValue` (or a binding error) caused the value of any lower priority value of value to take effect. After this change, the binding reverts to the property's default value. This behavior more closely matches WPF and fixes #10110. --- src/Avalonia.Base/AvaloniaObject.cs | 4 +- .../PropertyStore/BindingEntryBase.cs | 47 +++++++++---------- .../LocalValueBindingObserver.cs | 42 ++++++++++++----- .../LocalValueUntypedBindingObserver.cs | 25 +++++++--- .../SourceUntypedBindingEntry.cs | 2 + .../PropertyStore/TypedBindingEntry.cs | 2 + .../PropertyStore/UntypedBindingEntry.cs | 5 ++ .../AvaloniaObjectTests_Binding.cs | 18 ++++++- .../AvaloniaObjectTests_Validation.cs | 4 +- 9 files changed, 99 insertions(+), 50 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 1946d4ba5c..db6dbe98e0 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -270,8 +270,8 @@ namespace Avalonia /// The property. /// True if the property is set, otherwise false. /// - /// Checks whether a value is assigned to the property, or that there is a binding to the - /// property that is producing a value other than . + /// Returns true if is a styled property which has a value + /// assigned to it or a binding targeting it; otherwise false. /// public bool IsSet(AvaloniaProperty property) { diff --git a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs index 11dc80ef8f..e1ff0970c2 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs @@ -16,6 +16,8 @@ namespace Avalonia.PropertyStore private IDisposable? _subscription; private bool _hasValue; private TValue? _value; + private TValue? _defaultValue; + private bool _isDefaultValueInitialized; protected BindingEntryBase( ValueFrame frame, @@ -89,6 +91,7 @@ namespace Avalonia.PropertyStore protected abstract BindingValue ConvertAndValidate(TSource value); protected abstract BindingValue ConvertAndValidate(BindingValue value); + protected abstract TValue GetDefaultValue(Type ownerType); protected virtual void Start(bool produceValue) { @@ -104,17 +107,6 @@ namespace Avalonia.PropertyStore }; } - private void ClearValue() - { - if (_hasValue) - { - _hasValue = false; - _value = default; - if (_subscription is not null) - Frame.Owner?.OnBindingValueCleared(Property, Frame.Priority); - } - } - private void SetValue(BindingValue value) { static void Execute(BindingEntryBase instance, BindingValue value) @@ -124,24 +116,20 @@ namespace Avalonia.PropertyStore LoggingUtils.LogIfNecessary(instance.Frame.Owner.Owner, instance.Property, value); - if (value.HasValue) - { - if (!instance._hasValue || !EqualityComparer.Default.Equals(instance._value, value.Value)) - { - instance._value = value.Value; - instance._hasValue = true; - if (instance._subscription is not null && instance._subscription != s_creatingQuiet) - instance.Frame.Owner?.OnBindingValueChanged(instance, instance.Frame.Priority); - } - } - else if (value.Type != BindingValueType.DoNothing) + var effectiveValue = value.HasValue ? value.Value : instance.GetCachedDefaultValue(); + + if (!instance._hasValue || !EqualityComparer.Default.Equals(instance._value, effectiveValue)) { - instance.ClearValue(); + instance._value = effectiveValue; + instance._hasValue = true; if (instance._subscription is not null && instance._subscription != s_creatingQuiet) - instance.Frame.Owner?.OnBindingValueCleared(instance.Property, instance.Frame.Priority); + instance.Frame.Owner?.OnBindingValueChanged(instance, instance.Frame.Priority); } } + if (value.Type == BindingValueType.DoNothing) + return; + if (Dispatcher.UIThread.CheckAccess()) { Execute(this, value); @@ -161,5 +149,16 @@ namespace Avalonia.PropertyStore _subscription = null; Frame.OnBindingCompleted(this); } + + private TValue GetCachedDefaultValue() + { + if (!_isDefaultValueInitialized) + { + _defaultValue = GetDefaultValue(Frame.Owner!.Owner.GetType()); + _isDefaultValueInitialized = true; + } + + return _defaultValue!; + } } } diff --git a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs index f89cb029b6..0771ade647 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs @@ -10,6 +10,8 @@ namespace Avalonia.PropertyStore { private readonly ValueStore _owner; private IDisposable? _subscription; + private T? _defaultValue; + private bool _isDefaultValueInitialized; public LocalValueBindingObserver(ValueStore owner, StyledProperty property) { @@ -41,26 +43,28 @@ namespace Avalonia.PropertyStore public void OnNext(T value) { - static void Execute(ValueStore owner, StyledProperty property, T value) + static void Execute(LocalValueBindingObserver instance, T value) { - if (property.ValidateValue?.Invoke(value) != false) - owner.SetValue(property, value, BindingPriority.LocalValue); - else - owner.ClearLocalValue(property); + var owner = instance._owner; + var property = instance.Property; + + if (property.ValidateValue?.Invoke(value) == false) + value = instance.GetCachedDefaultValue(); + + owner.SetValue(property, value, BindingPriority.LocalValue); } if (Dispatcher.UIThread.CheckAccess()) { - Execute(_owner, Property, value); + Execute(this, value); } else { // To avoid allocating closure in the outer scope we need to capture variables // locally. This allows us to skip most of the allocations when on UI thread. - var instance = _owner; - var property = Property; + var instance = this; var newValue = value; - Dispatcher.UIThread.Post(() => Execute(instance, property, newValue)); + Dispatcher.UIThread.Post(() => Execute(instance, newValue)); } } @@ -73,12 +77,13 @@ namespace Avalonia.PropertyStore LoggingUtils.LogIfNecessary(owner.Owner, property, value); - if (value.HasValue) - owner.SetValue(property, value.Value, BindingPriority.LocalValue); - else if (value.Type != BindingValueType.DataValidationError) - owner.ClearLocalValue(property); + var effectiveValue = value.HasValue ? value.Value : instance.GetCachedDefaultValue(); + owner.SetValue(property, effectiveValue, BindingPriority.LocalValue); } + if (value.Type is BindingValueType.DoNothing or BindingValueType.DataValidationError) + return; + if (Dispatcher.UIThread.CheckAccess()) { Execute(this, value); @@ -92,5 +97,16 @@ namespace Avalonia.PropertyStore Dispatcher.UIThread.Post(() => Execute(instance, newValue)); } } + + private T GetCachedDefaultValue() + { + if (!_isDefaultValueInitialized) + { + _defaultValue = Property.GetDefaultValue(_owner.Owner.GetType()); + _isDefaultValueInitialized = true; + } + + return _defaultValue!; + } } } diff --git a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs index 2d157b2519..46e6ed810a 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs @@ -1,5 +1,4 @@ using System; -using System.Security.Cryptography; using Avalonia.Data; using Avalonia.Threading; @@ -10,6 +9,8 @@ namespace Avalonia.PropertyStore { private readonly ValueStore _owner; private IDisposable? _subscription; + private T? _defaultValue; + private bool _isDefaultValueInitialized; public LocalValueUntypedBindingObserver(ValueStore owner, StyledProperty property) { @@ -49,11 +50,7 @@ namespace Avalonia.PropertyStore if (value == AvaloniaProperty.UnsetValue) { - owner.ClearLocalValue(property); - } - else if (value == BindingOperations.DoNothing) - { - // Do nothing! + owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue); } else if (UntypedValueUtils.TryConvertAndValidate(property, value, out var typedValue)) { @@ -61,11 +58,14 @@ namespace Avalonia.PropertyStore } else { - owner.ClearLocalValue(property); + owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue); LoggingUtils.LogInvalidValue(owner.Owner, property, typeof(T), value); } } + if (value == BindingOperations.DoNothing) + return; + if (Dispatcher.UIThread.CheckAccess()) { Execute(this, value); @@ -79,5 +79,16 @@ namespace Avalonia.PropertyStore Dispatcher.UIThread.Post(() => Execute(instance, newValue)); } } + + private T GetCachedDefaultValue() + { + if (!_isDefaultValueInitialized) + { + _defaultValue = Property.GetDefaultValue(_owner.Owner.GetType()); + _isDefaultValueInitialized = true; + } + + return _defaultValue!; + } } } diff --git a/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs b/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs index b56d0d4529..b82714817b 100644 --- a/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs @@ -31,5 +31,7 @@ namespace Avalonia.PropertyStore { throw new NotSupportedException(); } + + protected override TTarget GetDefaultValue(Type ownerType) => Property.GetDefaultValue(ownerType); } } diff --git a/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs b/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs index 697725c87b..550f5c0001 100644 --- a/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs @@ -48,5 +48,7 @@ namespace Avalonia.PropertyStore return value; } + + protected override T GetDefaultValue(Type ownerType) => Property.GetDefaultValue(ownerType); } } diff --git a/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs b/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs index f8becb2e06..a77d7fddb6 100644 --- a/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs @@ -29,5 +29,10 @@ namespace Avalonia.PropertyStore { throw new NotSupportedException(); } + + protected override object? GetDefaultValue(Type ownerType) + { + return ((IStyledPropertyMetadata)Property.GetMetadata(ownerType)).DefaultValue; + } } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index f0386615c5..baaed5104d 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -372,6 +372,20 @@ namespace Avalonia.Base.UnitTests Assert.Null(target.GetValue(property)); } + [Fact] + public void LocalValue_Bind_Generic_To_ValueType_Accepts_UnsetValue() + { + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.QuxProperty, source); + source.OnNext(6.7); + source.OnNext(BindingValue.Unset); + + Assert.Equal(5.6, target.GetValue(Class1.QuxProperty)); + Assert.True(target.IsSet(Class1.QuxProperty)); + } + [Fact] public void LocalValue_Bind_NonGeneric_To_ValueType_Accepts_UnsetValue() { @@ -383,7 +397,7 @@ namespace Avalonia.Base.UnitTests source.OnNext(AvaloniaProperty.UnsetValue); Assert.Equal(5.6, target.GetValue(Class1.QuxProperty)); - Assert.False(target.IsSet(Class1.QuxProperty)); + Assert.True(target.IsSet(Class1.QuxProperty)); } [Fact] @@ -397,7 +411,7 @@ namespace Avalonia.Base.UnitTests source.OnNext(AvaloniaProperty.UnsetValue); Assert.Equal(5.6, target.GetValue(Class1.QuxProperty)); - Assert.False(target.IsSet(Class1.QuxProperty)); + Assert.True(target.IsSet(Class1.QuxProperty)); } [Fact] diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs index e8175cf477..3fc950573d 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs @@ -66,7 +66,7 @@ namespace Avalonia.Base.UnitTests } [Fact] - public void Reverts_To_Lower_Priority_If_Style_Binding_Fails_Validation() + public void Reverts_To_DefaultValue_If_Style_Binding_Fails_Validation_2() { var target = new Class1(); var source = new Subject(); @@ -75,7 +75,7 @@ namespace Avalonia.Base.UnitTests target.Bind(Class1.FooProperty, source, BindingPriority.StyleTrigger); source.OnNext(150); - Assert.Equal(10, target.GetValue(Class1.FooProperty)); + Assert.Equal(11, target.GetValue(Class1.FooProperty)); } [Fact] From bca57ea0e9c65c5b17422303261b1a7a058d842b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Feb 2023 16:27:48 +0100 Subject: [PATCH 041/185] Added additional failing test. --- .../AvaloniaObjectTests_Validation.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs index 3fc950573d..513aeb65ab 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs @@ -78,6 +78,20 @@ namespace Avalonia.Base.UnitTests Assert.Equal(11, target.GetValue(Class1.FooProperty)); } + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + public void Reverts_To_DefaultValue_If_Style_Binding_Fails_Validation_3(BindingPriority priority) + { + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.FooProperty, source, priority); + source.OnNext(150); + + Assert.Equal(11, target.GetValue(Class1.FooProperty)); + } + [Fact] public void Reverts_To_DefaultValue_Even_In_Presence_Of_Other_Bindings() { From b2c1d80f5cfdf3c0dccbed07cb908b69f7db3e2d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Feb 2023 16:32:02 +0100 Subject: [PATCH 042/185] Fix validation of local value bindings. Local value bindings with a type of `BindingValue` were not being validated. --- .../PropertyStore/LocalValueBindingObserver.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs index 0771ade647..5908d9e535 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs @@ -77,8 +77,17 @@ namespace Avalonia.PropertyStore LoggingUtils.LogIfNecessary(owner.Owner, property, value); - var effectiveValue = value.HasValue ? value.Value : instance.GetCachedDefaultValue(); - owner.SetValue(property, effectiveValue, BindingPriority.LocalValue); + if (value.HasValue) + { + var effectiveValue = value.Value; + if (property.ValidateValue?.Invoke(effectiveValue) == false) + effectiveValue = instance.GetCachedDefaultValue(); + owner.SetValue(property, effectiveValue, BindingPriority.LocalValue); + } + else + { + owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue); + } } if (value.Type is BindingValueType.DoNothing or BindingValueType.DataValidationError) From 96bda931196bab8368969d0a1e1151a9427d618d Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Sat, 21 Jan 2023 11:43:14 +0100 Subject: [PATCH 043/185] fix: DevGenerator Nullable --- .../DevGenerators/CompositionGenerator/Generator.ListProxy.cs | 2 +- src/tools/DevGenerators/CompositionGenerator/Generator.cs | 4 ++-- src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs | 2 +- src/tools/DevGenerators/GetProcAddressInitialization.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs index 135ab0426e..c293a9101d 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs @@ -112,7 +112,7 @@ class Template var defs = cl.Members.OfType().First(m => m.Identifier.Text == "InitializeDefaults"); - cl = cl.ReplaceNode(defs.Body, defs.Body.AddStatements( + cl = cl.ReplaceNode(defs.Body!, defs.Body!.AddStatements( ParseStatement($"_list = new ServerListProxyHelper<{itemType}, {serverItemType}>(this);"))); diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.cs index 3b5d3d8c3f..dfc8b45579 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.cs @@ -297,8 +297,8 @@ namespace Avalonia.SourceGenerator.CompositionGenerator server = server.WithBaseList( server.BaseList?.AddTypes(SimpleBaseType(ParseTypeName(impl.ServerName)))); - client = client.AddMembers( - ParseMemberDeclaration($"{impl.ServerName} {impl.Name}.Server => Server;")); + if(ParseMemberDeclaration($"{impl.ServerName} {impl.Name}.Server => Server;") is { } member) + client = client.AddMembers(member); } diff --git a/src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs b/src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs index 86dbb3a452..c975bb8444 100644 --- a/src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs +++ b/src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs @@ -32,7 +32,7 @@ public class EnumMemberDictionaryGenerator : IIncrementalGenerator ).Collect(); context.RegisterSourceOutput(all, static (context, methods) => { - foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default)) + foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default)) { var classBuilder = new StringBuilder(); if (typeGroup.Key.ContainingNamespace != null) diff --git a/src/tools/DevGenerators/GetProcAddressInitialization.cs b/src/tools/DevGenerators/GetProcAddressInitialization.cs index aedc13e7f6..e8d7c251fa 100644 --- a/src/tools/DevGenerators/GetProcAddressInitialization.cs +++ b/src/tools/DevGenerators/GetProcAddressInitialization.cs @@ -34,7 +34,7 @@ public class GetProcAddressInitializationGenerator : IIncrementalGenerator var all = fieldsWithAttribute.Collect(); context.RegisterSourceOutput(all, static (context, methods) => { - foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default)) + foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default)) { var nextContext = 0; var contexts = new Dictionary(); From 14fba2e53ac5348b1a86a5b28980ecd18bddc9ee Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 2 Feb 2023 17:39:36 +0100 Subject: [PATCH 044/185] Rework HitTestTextRange --- src/Avalonia.Base/Media/FormattedText.cs | 32 +- .../Media/TextFormatting/ITextSource.cs | 4 +- .../Media/TextFormatting/TextFormatter.cs | 2 +- .../Media/TextFormatting/TextFormatterImpl.cs | 18 +- .../Media/TextFormatting/TextLayout.cs | 37 +- .../Media/TextFormatting/TextLineImpl.cs | 684 ++++++++++-------- src/Avalonia.Controls/TextBlock.cs | 51 +- .../TextFormatting/TextFormatterTests.cs | 84 +++ .../Media/TextFormatting/TextLayoutTests.cs | 60 +- .../Media/TextFormatting/TextLineTests.cs | 13 +- 10 files changed, 633 insertions(+), 352 deletions(-) diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs index 0bab473442..28757b1a1d 100644 --- a/src/Avalonia.Base/Media/FormattedText.cs +++ b/src/Avalonia.Base/Media/FormattedText.cs @@ -741,6 +741,11 @@ namespace Avalonia.Media null // no previous line break ); + if(Current is null) + { + return false; + } + // check if this line fits the text height if (_totalHeight + Current.Height > _that._maxTextHeight) { @@ -779,7 +784,7 @@ namespace Avalonia.Media // maybe there is no next line at all if (Position + Current.Length < _that._text.Length) { - bool nextLineFits; + bool nextLineFits = false; if (_lineCount + 1 >= _that._maxLineCount) { @@ -795,7 +800,10 @@ namespace Avalonia.Media currentLineBreak ); - nextLineFits = (_totalHeight + Current.Height + _nextLine.Height <= _that._maxTextHeight); + if(_nextLine != null) + { + nextLineFits = (_totalHeight + Current.Height + _nextLine.Height <= _that._maxTextHeight); + } } if (!nextLineFits) @@ -819,16 +827,22 @@ namespace Avalonia.Media _previousLineBreak ); - currentLineBreak = Current.TextLineBreak; + if(Current != null) + { + currentLineBreak = Current.TextLineBreak; + } _that._defaultParaProps.SetTextWrapping(currentWrap); } } } - _previousHeight = Current.Height; + if(Current != null) + { + _previousHeight = Current.Height; - Length = Current.Length; + Length = Current.Length; + } _previousLineBreak = currentLineBreak; @@ -838,7 +852,7 @@ namespace Avalonia.Media /// /// Wrapper of TextFormatter.FormatLine that auto-collapses the line if needed. /// - private TextLine FormatLine(ITextSource textSource, int textSourcePosition, double maxLineLength, TextParagraphProperties paraProps, TextLineBreak? lineBreak) + private TextLine? FormatLine(ITextSource textSource, int textSourcePosition, double maxLineLength, TextParagraphProperties paraProps, TextLineBreak? lineBreak) { var line = _formatter.FormatLine( textSource, @@ -848,7 +862,7 @@ namespace Avalonia.Media lineBreak ); - if (_that._trimming != TextTrimming.None && line.HasOverflowed && line.Length > 0) + if (line != null && _that._trimming != TextTrimming.None && line.HasOverflowed && line.Length > 0) { // what I really need here is the last displayed text run of the line // textSourcePosition + line.Length - 1 works except the end of paragraph case, @@ -1601,11 +1615,11 @@ namespace Avalonia.Media } /// - public TextRun? GetTextRun(int textSourceCharacterIndex) + public TextRun GetTextRun(int textSourceCharacterIndex) { if (textSourceCharacterIndex >= _that._text.Length) { - return null; + return new TextEndOfParagraph(); } var thatFormatRider = new SpanRider(_that._formatRuns, _that._latestPosition, textSourceCharacterIndex); diff --git a/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs b/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs index 26966b37bc..32012ab8e9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs @@ -1,6 +1,4 @@ -using Avalonia.Metadata; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { /// /// Produces objects that are used by the . diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs index 0b5d7649d7..ff8c1c4860 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs @@ -38,7 +38,7 @@ /// A value that specifies the text formatter state, /// in terms of where the previous line in the paragraph was broken by the text formatting process. /// The formatted line. - public abstract TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, + public abstract TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 812c4e9eb8..7f74f49982 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -18,7 +18,7 @@ namespace Avalonia.Media.TextFormatting [ThreadStatic] private static BidiAlgorithm? t_bidiAlgorithm; /// - public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, + public override TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null) { TextLineBreak? nextLineBreak = null; @@ -41,6 +41,11 @@ namespace Avalonia.Media.TextFormatting fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, out var textEndOfLine, out var textSourceLength); + if (fetchedRuns.Count == 0) + { + return null; + } + shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager, out var resolvedFlowDirection); @@ -491,16 +496,7 @@ namespace Avalonia.Media.TextFormatting while (textRunEnumerator.MoveNext()) { - var textRun = textRunEnumerator.Current; - - if (textRun == null) - { - textRuns.Add(new TextEndOfParagraph()); - - textSourceLength += TextRun.DefaultTextSourceLength; - - break; - } + TextRun textRun = textRunEnumerator.Current!; if (textRun is TextEndOfLine textEndOfLine) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 8e85c10bba..4dbc472133 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -238,7 +238,7 @@ namespace Avalonia.Media.TextFormatting foreach (var textLine in _textLines) { //Current line isn't covered. - if (textLine.FirstTextSourceIndex + textLine.Length < start) + if (textLine.FirstTextSourceIndex + textLine.Length <= start) { currentY += textLine.Height; @@ -348,14 +348,36 @@ namespace Avalonia.Media.TextFormatting { var (x, y) = point; - var lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length; - var isInside = x >= 0 && x <= textLine.Width && y >= 0 && y <= textLine.Height; - if (x >= textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0) + var lastTrailingIndex = 0; + + if(_paragraphProperties.FlowDirection== FlowDirection.LeftToRight) { - lastTrailingIndex -= textLine.NewLineLength; + lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length; + + if (x >= textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0) + { + lastTrailingIndex -= textLine.NewLineLength; + } + + if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfLine textEndOfLine) + { + lastTrailingIndex -= textEndOfLine.Length; + } } + else + { + if (x <= textLine.WidthIncludingTrailingWhitespace - textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0) + { + lastTrailingIndex += textLine.NewLineLength; + } + + if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfLine textEndOfLine) + { + lastTrailingIndex += textEndOfLine.Length; + } + } var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; @@ -391,7 +413,7 @@ namespace Avalonia.Media.TextFormatting /// private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize, IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping, - TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight, + TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight, double letterSpacing) { var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground); @@ -456,7 +478,7 @@ namespace Avalonia.Media.TextFormatting var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth, _paragraphProperties, previousLine?.TextLineBreak); - if (textLine.Length == 0) + if (textLine is null) { if (previousLine != null && previousLine.NewLineLength > 0) { @@ -518,7 +540,6 @@ namespace Avalonia.Media.TextFormatting } } - //Make sure the TextLayout always contains at least on empty line if (textLines.Count == 0) { var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index d29063e07d..187b3154ad 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -10,6 +10,7 @@ namespace Avalonia.Media.TextFormatting private readonly double _paragraphWidth; private readonly TextParagraphProperties _paragraphProperties; private TextLineMetrics _textLineMetrics; + private TextLineBreak? _textLineBreak; private readonly FlowDirection _resolvedFlowDirection; public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, double paragraphWidth, @@ -18,7 +19,7 @@ namespace Avalonia.Media.TextFormatting { FirstTextSourceIndex = firstTextSourceIndex; Length = length; - TextLineBreak = lineBreak; + _textLineBreak = lineBreak; HasCollapsed = hasCollapsed; _textRuns = textRuns; @@ -38,7 +39,7 @@ namespace Avalonia.Media.TextFormatting public override int Length { get; } /// - public override TextLineBreak? TextLineBreak { get; } + public override TextLineBreak? TextLineBreak => _textLineBreak; /// public override bool HasCollapsed { get; } @@ -167,50 +168,54 @@ namespace Avalonia.Media.TextFormatting { if (_textRuns.Length == 0) { - return new CharacterHit(); + return new CharacterHit(FirstTextSourceIndex); } distance -= Start; - var firstRunIndex = 0; + var lastIndex = _textRuns.Length - 1; - if (_textRuns[firstRunIndex] is TextEndOfLine) + if (_textRuns[lastIndex] is TextEndOfLine) { - firstRunIndex++; + lastIndex--; } - if(firstRunIndex >= _textRuns.Length) + var currentPosition = FirstTextSourceIndex; + + if (lastIndex < 0) { - return new CharacterHit(FirstTextSourceIndex); + return new CharacterHit(currentPosition); } if (distance <= 0) { - var firstRun = _textRuns[firstRunIndex]; + var firstRun = _textRuns[0]; - return GetRunCharacterHit(firstRun, FirstTextSourceIndex, 0); + if (_paragraphProperties.FlowDirection == FlowDirection.RightToLeft) + { + currentPosition = Length - firstRun.Length; + } + + return GetRunCharacterHit(firstRun, currentPosition, 0); } if (distance >= WidthIncludingTrailingWhitespace) { - var lastRun = _textRuns[_textRuns.Length - 1]; - - var size = 0.0; + var lastRun = _textRuns[lastIndex]; - if (lastRun is DrawableTextRun drawableTextRun) + if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight) { - size = drawableTextRun.Size.Width; + currentPosition = Length - lastRun.Length; } - return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, size); + return GetRunCharacterHit(lastRun, currentPosition, distance); } // process hit that happens within the line var characterHit = new CharacterHit(); - var currentPosition = FirstTextSourceIndex; var currentDistance = 0.0; - for (var i = 0; i < _textRuns.Length; i++) + for (var i = 0; i <= lastIndex; i++) { var currentRun = _textRuns[i]; @@ -242,7 +247,7 @@ namespace Avalonia.Media.TextFormatting currentRun = _textRuns[j]; - if(currentRun is not ShapedTextRun) + if (currentRun is not ShapedTextRun) { continue; } @@ -274,10 +279,6 @@ namespace Avalonia.Media.TextFormatting continue; } } - else - { - continue; - } break; } @@ -422,10 +423,10 @@ namespace Avalonia.Media.TextFormatting { if (currentGlyphRun != null) { - distance = currentGlyphRun.Size.Width - distance; + currentDistance -= currentGlyphRun.Size.Width; } - return Math.Max(0, currentDistance - distance); + return currentDistance + distance; } if (currentRun is DrawableTextRun drawableTextRun) @@ -575,386 +576,505 @@ namespace Avalonia.Media.TextFormatting return GetPreviousCaretCharacterHit(characterHit); } - private IReadOnlyList GetTextBoundsLeftToRight(int firstTextSourceIndex, int textLength) + public override IReadOnlyList GetTextBounds(int firstTextSourceIndex, int textLength) { - var characterIndex = firstTextSourceIndex + textLength; + if (_textRuns.Length == 0) + { + return Array.Empty(); + } - var result = new List(_textRuns.Length); - var lastDirection = FlowDirection.LeftToRight; - var currentDirection = lastDirection; + var result = new List(); var currentPosition = FirstTextSourceIndex; var remainingLength = textLength; - var startX = Start; - double currentWidth = 0; - var currentRect = default(Rect); - - TextRunBounds lastRunBounds = default; - - for (var index = 0; index < _textRuns.Length; index++) + static FlowDirection GetDirection(TextRun textRun, FlowDirection currentDirection) { - if (_textRuns[index] is not DrawableTextRun currentRun) + if (textRun is ShapedTextRun shapedTextRun) { - continue; + return shapedTextRun.ShapedBuffer.IsLeftToRight ? + FlowDirection.LeftToRight : + FlowDirection.RightToLeft; } - var characterLength = 0; - var endX = startX; - - TextRunBounds currentRunBounds; + return currentDirection; + } - double combinedWidth; + if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight) + { + var currentX = Start; - if (currentRun is ShapedTextRun currentShapedRun) + for (int i = 0; i < _textRuns.Length; i++) { - var firstCluster = currentShapedRun.GlyphRun.Metrics.FirstCluster; + var currentRun = _textRuns[i]; + + var firstRunIndex = i; + var lastRunIndex = firstRunIndex; + var currentDirection = GetDirection(currentRun, FlowDirection.LeftToRight); + var directionalWidth = 0.0; - if (currentPosition + currentRun.Length <= firstTextSourceIndex) + if (currentRun is DrawableTextRun currentDrawable) { - startX += currentRun.Size.Width; + directionalWidth = currentDrawable.Size.Width; + } - currentPosition += currentRun.Length; + // Find consecutive runs of same direction + for (; lastRunIndex + 1 < _textRuns.Length; lastRunIndex++) + { + var nextRun = _textRuns[lastRunIndex + 1]; - continue; + var nextDirection = GetDirection(nextRun, currentDirection); + + if (currentDirection != nextDirection) + { + break; + } + + if (nextRun is DrawableTextRun nextDrawable) + { + directionalWidth += nextDrawable.Size.Width; + } } - if (currentShapedRun.ShapedBuffer.IsLeftToRight) + //Skip runs that are not part of the hit test range + switch (currentDirection) { - var startIndex = firstCluster + Math.Max(0, firstTextSourceIndex - currentPosition); + case FlowDirection.RightToLeft: + { + for (; lastRunIndex >= firstRunIndex; lastRunIndex--) + { + currentRun = _textRuns[lastRunIndex]; + + if (currentPosition + currentRun.Length > firstTextSourceIndex) + { + break; + } + + currentPosition += currentRun.Length; - double startOffset; + if (currentRun is DrawableTextRun drawableTextRun) + { + directionalWidth -= drawableTextRun.Size.Width; + currentX += drawableTextRun.Size.Width; + } - double endOffset; + if(lastRunIndex - 1 < 0) + { + break; + } + } - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + break; + } + default: + { + for (; firstRunIndex <= lastRunIndex; firstRunIndex++) + { + currentRun = _textRuns[firstRunIndex]; - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + if (currentPosition + currentRun.Length > firstTextSourceIndex) + { + break; + } - startX += startOffset; + currentPosition += currentRun.Length; - endX += endOffset; + if (currentRun is DrawableTextRun drawableTextRun) + { + currentX += drawableTextRun.Size.Width; + directionalWidth -= drawableTextRun.Size.Width; + } - var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); + if(firstRunIndex + 1 == _textRuns.Length) + { + break; + } + } - var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + break; + } + } - characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength); + i = lastRunIndex; - currentDirection = FlowDirection.LeftToRight; + if (directionalWidth == 0) + { + continue; } - else + + var coveredLength = 0; + TextBounds? textBounds = null; + + switch (currentDirection) { - var rightToLeftIndex = index; - var rightToLeftWidth = currentShapedRun.Size.Width; - while (rightToLeftIndex + 1 <= _textRuns.Length - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun) - { - if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) + case FlowDirection.RightToLeft: { + textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX + directionalWidth, firstTextSourceIndex, + currentPosition, remainingLength, out coveredLength, out currentPosition); + + currentX += directionalWidth; + break; } + default: + { + textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex, + currentPosition, remainingLength, out coveredLength, out currentPosition); - rightToLeftIndex++; - - rightToLeftWidth += nextShapedRun.Size.Width; + currentX = textBounds.Rectangle.Right; - if (currentPosition + nextShapedRun.Length > firstTextSourceIndex + textLength) - { break; } + } - currentShapedRun = nextShapedRun; - } + if (coveredLength > 0) + { + result.Add(textBounds); + + remainingLength -= coveredLength; + } + + if (remainingLength <= 0) + { + break; + } + } + } + else + { + var currentX = Start + WidthIncludingTrailingWhitespace; - startX += rightToLeftWidth; + for (int i = _textRuns.Length - 1; i >= 0; i--) + { + var currentRun = _textRuns[i]; + var firstRunIndex = i; + var lastRunIndex = firstRunIndex; + var currentDirection = GetDirection(currentRun, FlowDirection.RightToLeft); + var directionalWidth = 0.0; - currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); + if (currentRun is DrawableTextRun currentDrawable) + { + directionalWidth = currentDrawable.Size.Width; + } - remainingLength -= currentRunBounds.Length; - currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length; - endX = currentRunBounds.Rectangle.Right; - startX = currentRunBounds.Rectangle.Left; + // Find consecutive runs of same direction + for (; firstRunIndex - 1 > 0; firstRunIndex--) + { + var previousRun = _textRuns[firstRunIndex - 1]; - var rightToLeftRunBounds = new List { currentRunBounds }; + var previousDirection = GetDirection(previousRun, currentDirection); - for (int i = rightToLeftIndex - 1; i >= index; i--) + if (currentDirection != previousDirection) { - if (_textRuns[i] is not ShapedTextRun shapedRun) + break; + } + + if (currentRun is DrawableTextRun previousDrawable) + { + directionalWidth += previousDrawable.Size.Width; + } + } + + //Skip runs that are not part of the hit test range + switch (currentDirection) + { + case FlowDirection.RightToLeft: { - continue; - } + for (; lastRunIndex >= firstRunIndex; lastRunIndex--) + { + currentRun = _textRuns[lastRunIndex]; - currentShapedRun = shapedRun; + if (currentPosition + currentRun.Length <= firstTextSourceIndex) + { + currentPosition += currentRun.Length; - currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); + if (currentRun is DrawableTextRun drawableTextRun) + { + currentX -= drawableTextRun.Size.Width; + directionalWidth -= drawableTextRun.Size.Width; + } - rightToLeftRunBounds.Insert(0, currentRunBounds); + continue; + } - remainingLength -= currentRunBounds.Length; - startX = currentRunBounds.Rectangle.Left; + break; + } - currentPosition += currentRunBounds.Length; - } + break; + } + default: + { + for (; firstRunIndex <= lastRunIndex; firstRunIndex++) + { + currentRun = _textRuns[firstRunIndex]; - combinedWidth = endX - startX; + if (currentPosition + currentRun.Length <= firstTextSourceIndex) + { + currentPosition += currentRun.Length; - currentRect = new Rect(startX, 0, combinedWidth, Height); + if (currentRun is DrawableTextRun drawableTextRun) + { + currentX += drawableTextRun.Size.Width; + directionalWidth -= drawableTextRun.Size.Width; + } - currentDirection = FlowDirection.RightToLeft; + continue; + } - if (!MathUtilities.IsZero(combinedWidth)) - { - result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds)); - } + break; + } - startX = endX; + break; + } } - } - else - { - if (currentPosition + currentRun.Length <= firstTextSourceIndex) - { - startX += currentRun.Size.Width; - currentPosition += currentRun.Length; + i = firstRunIndex; + if (directionalWidth == 0) + { continue; } - if (currentPosition < firstTextSourceIndex) - { - startX += currentRun.Size.Width; - } + var coveredLength = 0; - if (currentPosition + currentRun.Length <= characterIndex) + TextBounds? textBounds = null; + + switch (currentDirection) { - endX += currentRun.Size.Width; + case FlowDirection.LeftToRight: + { + textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX - directionalWidth, firstTextSourceIndex, + currentPosition, remainingLength, out coveredLength, out currentPosition); + + currentX -= directionalWidth; - characterLength = currentRun.Length; + break; + } + default: + { + textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex, + currentPosition, remainingLength, out coveredLength, out currentPosition); + + currentX = textBounds.Rectangle.Left; + + break; + } } - } - if (endX < startX) - { - (endX, startX) = (startX, endX); - } + //Visual order is always left to right so we need to insert + result.Insert(0, textBounds); - //Lines that only contain a linebreak need to be covered here - if (characterLength == 0) - { - characterLength = NewLineLength; + remainingLength -= coveredLength; + + if (remainingLength <= 0) + { + break; + } } + } - combinedWidth = endX - startX; + return result; + } - currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun); + private TextBounds GetTextRunBoundsRightToLeft(int firstRunIndex, int lastRunIndex, double endX, + int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition) + { + coveredLength = 0; + var textRunBounds = new List(); + var startX = endX; - currentPosition += characterLength; + for (int i = lastRunIndex; i >= firstRunIndex; i--) + { + var currentRun = _textRuns[i]; - remainingLength -= characterLength; + if (currentRun is ShapedTextRun shapedTextRun) + { + var runBounds = GetRunBoundsRightToLeft(shapedTextRun, startX, firstTextSourceIndex, remainingLength, currentPosition, out var offset); - startX = endX; + textRunBounds.Insert(0, runBounds); - if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0) - { - if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right)) + if (offset > 0) { - currentRect = currentRect.WithWidth(currentWidth + combinedWidth); + endX = runBounds.Rectangle.Right; - var textBounds = result[result.Count - 1]; + startX = endX; + } - textBounds.Rectangle = currentRect; + startX -= runBounds.Rectangle.Width; - textBounds.TextRunBounds.Add(currentRunBounds); - } - else + currentPosition += runBounds.Length + offset; + + coveredLength += runBounds.Length; + + remainingLength -= runBounds.Length; + } + else + { + if (currentRun is DrawableTextRun drawableTextRun) { - currentRect = currentRunBounds.Rectangle; + startX -= drawableTextRun.Size.Width; - result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds })); + textRunBounds.Insert(0, + new TextRunBounds( + new Rect(startX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun)); } - } - lastRunBounds = currentRunBounds; + currentPosition += currentRun.Length; + + coveredLength += currentRun.Length; - currentWidth += combinedWidth; + remainingLength -= currentRun.Length; + } - if (remainingLength <= 0 || currentPosition >= characterIndex) + if (remainingLength <= 0) { break; } - - lastDirection = currentDirection; } - return result; - } + newPosition = currentPosition; - private IReadOnlyList GetTextBoundsRightToLeft(int firstTextSourceIndex, int textLength) - { - var characterIndex = firstTextSourceIndex + textLength; + var runWidth = endX - startX; - var result = new List(_textRuns.Length); - var lastDirection = FlowDirection.LeftToRight; - var currentDirection = lastDirection; + var bounds = new Rect(startX, 0, runWidth, Height); - var currentPosition = FirstTextSourceIndex; - var remainingLength = textLength; + return new TextBounds(bounds, FlowDirection.RightToLeft, textRunBounds); + } - var startX = WidthIncludingTrailingWhitespace; - double currentWidth = 0; - var currentRect = default(Rect); + private TextBounds GetTextBoundsLeftToRight(int firstRunIndex, int lastRunIndex, double startX, + int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition) + { + coveredLength = 0; + var textRunBounds = new List(); + var endX = startX; - for (var index = _textRuns.Length - 1; index >= 0; index--) + for (int i = firstRunIndex; i <= lastRunIndex; i++) { - if (_textRuns[index] is not DrawableTextRun currentRun) - { - continue; - } - - if (currentPosition + currentRun.Length < firstTextSourceIndex) - { - startX -= currentRun.Size.Width; - - currentPosition += currentRun.Length; - - continue; - } - - var characterLength = 0; - var endX = startX; + var currentRun = _textRuns[i]; - if (currentRun is ShapedTextRun currentShapedRun) + if (currentRun is ShapedTextRun shapedTextRun) { - var offset = Math.Max(0, firstTextSourceIndex - currentPosition); - - currentPosition += offset; - - var startIndex = currentPosition; - double startOffset; - double endOffset; + var runBounds = GetRunBoundsLeftToRight(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition, out var offset); - if (currentShapedRun.ShapedBuffer.IsLeftToRight) - { - if (currentPosition < startIndex) - { - startOffset = endOffset = 0; - } - else - { - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + textRunBounds.Add(runBounds); - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - } - } - else + if (offset > 0) { - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + startX = runBounds.Rectangle.Left; - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + endX = startX; } - startX -= currentRun.Size.Width - startOffset; - endX -= currentRun.Size.Width - endOffset; + currentPosition += runBounds.Length + offset; - var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); - var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + endX += runBounds.Rectangle.Width; - characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); + coveredLength += runBounds.Length; - currentDirection = currentShapedRun.ShapedBuffer.IsLeftToRight ? - FlowDirection.LeftToRight : - FlowDirection.RightToLeft; + remainingLength -= runBounds.Length; } else { - if (currentPosition + currentRun.Length <= characterIndex) + if (currentRun is DrawableTextRun drawableTextRun) { - endX -= currentRun.Size.Width; + textRunBounds.Add( + new TextRunBounds( + new Rect(endX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun)); + + endX += drawableTextRun.Size.Width; } - if (currentPosition < firstTextSourceIndex) - { - startX -= currentRun.Size.Width; + currentPosition += currentRun.Length; - characterLength = currentRun.Length; - } - } + coveredLength += currentRun.Length; - if (endX < startX) - { - (endX, startX) = (startX, endX); + remainingLength -= currentRun.Length; } - //Lines that only contain a linebreak need to be covered here - if (characterLength == 0) + if (remainingLength <= 0) { - characterLength = NewLineLength; + break; } + } - var runWidth = endX - startX; + newPosition = currentPosition; - var currentRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); + var runWidth = endX - startX; - if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0) - { - if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, Start + startX)) - { - currentRect = currentRect.WithWidth(currentWidth + runWidth); + var bounds = new Rect(startX, 0, runWidth, Height); - var textBounds = result[result.Count - 1]; + return new TextBounds(bounds, FlowDirection.LeftToRight, textRunBounds); + } - textBounds.Rectangle = currentRect; + private TextRunBounds GetRunBoundsLeftToRight(ShapedTextRun currentRun, double startX, + int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset) + { + var startIndex = currentPosition; - textBounds.TextRunBounds.Add(currentRunBounds); - } - else - { - currentRect = currentRunBounds.Rectangle; + offset = Math.Max(0, firstTextSourceIndex - currentPosition); - result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds })); - } - } + var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster; - currentWidth += runWidth; - currentPosition += characterLength; + if (currentPosition != firstCluster) + { + startIndex = firstCluster + offset; + } + else + { + startIndex += offset; + } - if (currentPosition > characterIndex) - { - break; - } + var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - lastDirection = currentDirection; - remainingLength -= characterLength; + var endX = startX + endOffset; + startX += startOffset; - if (remainingLength <= 0) - { - break; - } + var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); + + var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); + + if (endX < startX) + { + (endX, startX) = (startX, endX); + } + + //Lines that only contain a linebreak need to be covered here + if (characterLength == 0) + { + characterLength = NewLineLength; } - result.Reverse(); + var runWidth = endX - startX; - return result; + return new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); } - private TextRunBounds GetRightToLeftTextRunBounds(ShapedTextRun currentRun, double endX, int firstTextSourceIndex, int characterIndex, int currentPosition, int remainingLength) + private TextRunBounds GetRunBoundsRightToLeft(ShapedTextRun currentRun, double endX, + int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset) { var startX = endX; - var offset = Math.Max(0, firstTextSourceIndex - currentPosition); + var startIndex = currentPosition; - currentPosition += offset; + offset = Math.Max(0, firstTextSourceIndex - currentPosition); - var startIndex = currentPosition; + var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster; - double startOffset; - double endOffset; + if (currentPosition != firstCluster) + { + startIndex = firstCluster + offset; + } + else + { + startIndex += offset; + } - endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); startX -= currentRun.Size.Width - startOffset; endX -= currentRun.Size.Width - endOffset; @@ -980,16 +1100,6 @@ namespace Avalonia.Media.TextFormatting return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); } - public override IReadOnlyList GetTextBounds(int firstTextSourceIndex, int textLength) - { - if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight) - { - return GetTextBoundsLeftToRight(firstTextSourceIndex, textLength); - } - - return GetTextBoundsRightToLeft(firstTextSourceIndex, textLength); - } - public override void Dispose() { for (int i = 0; i < _textRuns.Length; i++) @@ -1005,6 +1115,11 @@ namespace Avalonia.Media.TextFormatting { _textLineMetrics = CreateLineMetrics(); + if (_textLineBreak is null && _textRuns.Length > 1 && _textRuns[_textRuns.Length - 1] is TextEndOfLine textEndOfLine) + { + _textLineBreak = new TextLineBreak(textEndOfLine); + } + BidiReorderer.Instance.BidiReorder(_textRuns, _resolvedFlowDirection); } @@ -1328,7 +1443,7 @@ namespace Avalonia.Media.TextFormatting { width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width; trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength; - newLineLength = textRun.GlyphRun.Metrics.NewLineLength; + newLineLength += textRun.GlyphRun.Metrics.NewLineLength; } widthIncludingWhitespace += textRun.Size.Width; @@ -1340,31 +1455,10 @@ namespace Avalonia.Media.TextFormatting { widthIncludingWhitespace += drawableTextRun.Size.Width; - switch (_paragraphProperties.FlowDirection) + if (index == lastRunIndex) { - case FlowDirection.LeftToRight: - { - if (index == lastRunIndex) - { - width = widthIncludingWhitespace; - trailingWhitespaceLength = 0; - newLineLength = 0; - } - - break; - } - - case FlowDirection.RightToLeft: - { - if (index == lastRunIndex) - { - width = widthIncludingWhitespace; - trailingWhitespaceLength = 0; - newLineLength = 0; - } - - break; - } + width = widthIncludingWhitespace; + trailingWhitespaceLength = 0; } if (drawableTextRun.Size.Height > height) diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 9bd1dc95f9..df9a3eb8f3 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -720,6 +720,16 @@ namespace Avalonia.Controls var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); + if (HasComplexContent) + { + ArrangeComplexContent(TextLayout, padding); + } + + if (MathUtilities.AreClose(_constraint.Inflate(padding).Width, finalSize.Width)) + { + return finalSize; + } + _constraint = new Size(Math.Ceiling(finalSize.Deflate(padding).Width), double.PositiveInfinity); _textLayout?.Dispose(); @@ -727,31 +737,36 @@ namespace Avalonia.Controls if (HasComplexContent) { - var currentY = padding.Top; + ArrangeComplexContent(TextLayout, padding); + } - foreach (var textLine in TextLayout.TextLines) - { - var currentX = padding.Left + textLine.Start; + return finalSize; + } - foreach (var run in textLine.TextRuns) + private static void ArrangeComplexContent(TextLayout textLayout, Thickness padding) + { + var currentY = padding.Top; + + foreach (var textLine in textLayout.TextLines) + { + var currentX = padding.Left + textLine.Start; + + foreach (var run in textLine.TextRuns) + { + if (run is DrawableTextRun drawable) { - if (run is DrawableTextRun drawable) + if (drawable is EmbeddedControlRun controlRun + && controlRun.Control is Control control) { - if (drawable is EmbeddedControlRun controlRun - && controlRun.Control is Control control) - { - control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize)); - } - - currentX += drawable.Size.Width; + control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize)); } - } - currentY += textLine.Height; + currentX += drawable.Size.Width; + } } - } - return finalSize; + currentY += textLine.Height; + } } protected override AutomationPeer OnCreateAutomationPeer() @@ -892,7 +907,7 @@ namespace Avalonia.Controls return textRun; } - return null; + return new TextEndOfParagraph(); } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 954169f975..8a2d4ecc6b 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -660,6 +660,90 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_Return_Null_For_Empty_TextSource() + { + using (Start()) + { + var defaultRunProperties = new GenericTextRunProperties(Typeface.Default); + var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties); + var textSource = new EmptyTextSource(); + + var textLine = TextFormatter.Current.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties); + + Assert.Null(textLine); + } + } + + [Fact] + public void Should_Retain_TextEndOfParagraph_With_TextWrapping() + { + using (Start()) + { + var defaultRunProperties = new GenericTextRunProperties(Typeface.Default); + var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties, textWrap: TextWrapping.Wrap); + + var text = "Hello World"; + + var textSource = new SimpleTextSource(text, defaultRunProperties); + + var pos = 0; + + TextLineBreak previousLineBreak = null; + TextLine textLine = null; + + while (pos < text.Length) + { + textLine = TextFormatter.Current.FormatLine(textSource, pos, 30, paragraphProperties, previousLineBreak); + + pos += textLine.Length; + + previousLineBreak = textLine.TextLineBreak; + } + + Assert.NotNull(textLine); + + Assert.NotNull(textLine.TextLineBreak.TextEndOfLine); + } + } + + protected readonly record struct SimpleTextSource : ITextSource + { + private readonly string _text; + private readonly TextRunProperties _defaultProperties; + + public SimpleTextSource(string text, TextRunProperties defaultProperties) + { + _text = text; + _defaultProperties = defaultProperties; + } + + public TextRun? GetTextRun(int textSourceIndex) + { + if (textSourceIndex > _text.Length) + { + return new TextEndOfParagraph(); + } + + var runText = _text.AsMemory(textSourceIndex); + + if (runText.IsEmpty) + { + return new TextEndOfParagraph(); + } + + return new TextCharacters(runText, _defaultProperties); + } + } + + private class EmptyTextSource : ITextSource + { + public TextRun GetTextRun(int textSourceIndex) + { + return null; + } + } + private class EndOfLineTextSource : ITextSource { public TextRun GetTextRun(int textSourceIndex) diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 2b63f24cf6..3735e9f6d7 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -9,7 +9,6 @@ using Avalonia.Media.TextFormatting.Unicode; using Avalonia.UnitTests; using Avalonia.Utilities; using Xunit; - namespace Avalonia.Skia.UnitTests.Media.TextFormatting { public class TextLayoutTests @@ -1028,6 +1027,65 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [InlineData("mgfg🧐df f sdf", "g🧐d", 20, 40)] + [InlineData("وه. وقد تعرض لانتقادات", "دات", 5, 30)] + [InlineData("وه. وقد تعرض لانتقادات", "تعرض", 20, 50)] + [InlineData(" علمية 😱ومضللة ،", " علمية 😱ومضللة ،", 40, 100)] + [InlineData("في عام 2018 ، رفعت ل", "في عام 2018 ، رفعت ل", 100, 120)] + [Theory] + public void HitTestTextRange_Range_ValidLength(string text, string textToSelect, double minWidth, double maxWidth) + { + using (Start()) + { + var layout = new TextLayout(text, Typeface.Default, 12, Brushes.Black); + var start = text.IndexOf(textToSelect); + var selectionRectangles = layout.HitTestTextRange(start, textToSelect.Length); + Assert.Equal(1, selectionRectangles.Count()); + var rect = selectionRectangles.First(); + Assert.InRange(rect.Width, minWidth, maxWidth); + } + } + + [InlineData("012🧐210", 2, 4, FlowDirection.LeftToRight, "14.40234375,40.8046875")] + [InlineData("210🧐012", 2, 4, FlowDirection.RightToLeft, "0,7.201171875;21.603515625,33.603515625;48.005859375,55.20703125")] + [InlineData("שנב🧐שנב", 2, 4, FlowDirection.LeftToRight, "11.63671875,39.779296875")] + [InlineData("שנב🧐שנב", 2, 4, FlowDirection.RightToLeft, "11.63671875,39.779296875")] + [Theory] + public void Should_HitTextTextRangeBetweenRuns(string text, int start, int length, + FlowDirection flowDirection, string expected) + { + using (Start()) + { + var expectedRects = expected.Split(';').Select(x => + { + var startEnd = x.Split(','); + + var start = double.Parse(startEnd[0], CultureInfo.InvariantCulture); + + var end = double.Parse(startEnd[1], CultureInfo.InvariantCulture); + + return new Rect(start, 0, end - start, 0); + }).ToArray(); + + var textLayout = new TextLayout(text, Typeface.Default, 12, Brushes.Black, flowDirection: flowDirection); + + var rects = textLayout.HitTestTextRange(start, length).ToArray(); + + Assert.Equal(expectedRects.Length, rects.Length); + + var endX = textLayout.TextLines[0].GetDistanceFromCharacterHit(new CharacterHit(2)); + var startX = textLayout.TextLines[0].GetDistanceFromCharacterHit(new CharacterHit(5, 1)); + + for (int i = 0; i < expectedRects.Length; i++) + { + var expectedRect = expectedRects[i]; + + Assert.Equal(expectedRect.Left, rects[i].Left); + + Assert.Equal(expectedRect.Right, rects[i].Right); + } + } + } private static IDisposable Start() diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 544b84912e..e47542af7a 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -604,19 +604,19 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textBounds = textLine.GetTextBounds(0, 20); - Assert.Equal(2, textBounds.Count); + Assert.Equal(1, textBounds.Count); Assert.Equal(144.0234375, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 30); - Assert.Equal(3, textBounds.Count); + Assert.Equal(1, textBounds.Count); Assert.Equal(216.03515625, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 40); - Assert.Equal(4, textBounds.Count); + Assert.Equal(1, textBounds.Count); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); } @@ -847,7 +847,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textBounds = textLine.GetTextBounds(0, textLine.Length); - Assert.Equal(6, textBounds.Count); + Assert.Equal(1, textBounds.Count); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 1); @@ -857,7 +857,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textBounds = textLine.GetTextBounds(0, firstRun.Length + 1); - Assert.Equal(2, textBounds.Count); + Assert.Equal(1, textBounds.Count); Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(1, firstRun.Length); @@ -867,7 +867,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textBounds = textLine.GetTextBounds(0, 1 + firstRun.Length); - Assert.Equal(2, textBounds.Count); + Assert.Equal(1, textBounds.Count); Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); } } @@ -958,6 +958,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(secondRun.Size.Width, textBounds[1].Rectangle.Width); Assert.Equal(7.201171875, textBounds[0].Rectangle.Width); + Assert.Equal(textLine.Start + 7.201171875, textBounds[0].Rectangle.Right); Assert.Equal(textLine.Start + firstRun.Size.Width, textBounds[1].Rectangle.Left); From 84cd896cd80a39668fee46c16285f3c0f102efa4 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 2 Feb 2023 17:49:14 +0100 Subject: [PATCH 045/185] fix: Waring CS0618 ListItemAutomationPeer --- .../Automation/Peers/ListItemAutomationPeer.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs index 85f139a6a3..aea91b5e26 100644 --- a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs @@ -1,5 +1,4 @@ -using System; -using Avalonia.Automation.Provider; +using Avalonia.Automation.Provider; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Controls.Selection; @@ -64,7 +63,7 @@ namespace Avalonia.Automation.Peers if (Owner.Parent is ItemsControl parent && parent.GetValue(ListBox.SelectionProperty) is ISelectionModel selectionModel) { - var index = parent.ItemContainerGenerator.IndexFromContainer(Owner); + var index = parent.IndexFromContainer(Owner); if (index != -1) selectionModel.Deselect(index); From 5ff8dc97fd8949fdb43dcb2a9dfbfa8163a5c28f Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Sat, 21 Jan 2023 11:42:52 +0100 Subject: [PATCH 046/185] fix: Linux Framebuffer nullable --- .../Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs | 2 +- src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 2dcce12df9..c3e90f5fd7 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -154,7 +154,7 @@ public static class LinuxFramebufferPlatformExtensions var lifetime = LinuxFramebufferPlatform.Initialize(builder, outputBackend, inputBackend); builder.SetupWithLifetime(lifetime); lifetime.Start(args); - builder.Instance.Run(lifetime.Token); + builder.Instance!.Run(lifetime.Token); return lifetime.ExitCode; } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index d61dcd4f91..0135cb3d1f 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -43,13 +43,13 @@ namespace Avalonia.LinuxFramebuffer.Output public IPlatformGraphics PlatformGraphics { get; private set; } public DrmOutput(DrmCard card, DrmResources resources, DrmConnector connector, DrmModeInfo modeInfo, - DrmOutputOptions? options = null) + DrmOutputOptions options = null) { if(options != null) _outputOptions = options; Init(card, resources, connector, modeInfo); } - public DrmOutput(string path = null, bool connectorsForceProbe = false, DrmOutputOptions? options = null) + public DrmOutput(string path = null, bool connectorsForceProbe = false, DrmOutputOptions options = null) { if(options != null) _outputOptions = options; @@ -63,7 +63,7 @@ namespace Avalonia.LinuxFramebuffer.Output if(connector == null) throw new InvalidOperationException("Unable to find connected DRM connector"); - DrmModeInfo? mode = null; + DrmModeInfo mode = null; if (options?.VideoMode != null) { From 3913fb333eadab53f070e4def78f59e52917a80a Mon Sep 17 00:00:00 2001 From: amwx <40413319+amwx@users.noreply.github.com> Date: Thu, 2 Feb 2023 17:50:31 -0500 Subject: [PATCH 047/185] Implement SelectedValue --- .../Primitives/SelectingItemsControl.cs | 251 ++++++++++++++++++ 1 file changed, 251 insertions(+) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 5210362505..2fac60c8d8 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Xml.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Selection; +using Avalonia.Controls.Utils; using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Platform; @@ -66,6 +67,19 @@ namespace Avalonia.Controls.Primitives (o, v) => o.SelectedItem = v, defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); + /// + /// Defines the property + /// + public static readonly StyledProperty SelectedValueProperty = + AvaloniaProperty.Register(nameof(SelectedValue), + defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property + /// + public static readonly StyledProperty SelectedValueBindingProperty = + AvaloniaProperty.Register(nameof(SelectedValueBinding)); + /// /// Defines the property. /// @@ -129,6 +143,8 @@ namespace Avalonia.Controls.Primitives private bool _ignoreContainerSelectionChanged; private UpdateState? _updateState; private bool _hasScrolledToSelectedItem; + private BindingHelper? _bindingHelper; + private bool _isSelectionChangeActive; /// /// Initializes static members of the class. @@ -209,6 +225,19 @@ namespace Avalonia.Controls.Primitives } } + [AssignBinding] + public IBinding? SelectedValueBinding + { + get => GetValue(SelectedValueBindingProperty); + set => SetValue(SelectedValueBindingProperty, value); + } + + public object? SelectedValue + { + get => GetValue(SelectedValueProperty); + set => SetValue(SelectedValueProperty, value); + } + /// /// Gets or sets the selected items. /// @@ -609,6 +638,60 @@ namespace Avalonia.Controls.Primitives { WrapFocus = WrapSelection; } + else if (change.Property == SelectedValueProperty) + { + if (_isSelectionChangeActive) + return; + + if (_updateState is not null) + { + _updateState.SelectedValue = change.NewValue; + return; + } + + SelectItemWithValue(change.NewValue); + } + else if (change.Property == SelectedValueBindingProperty) + { + var idx = SelectedIndex; + + // If no selection is active, don't do anything as SelectedValue is already null + if (idx == -1) + { + return; + } + + var value = change.GetNewValue(); + if (value is null) + { + // Clearing SelectedValueBinding makes the SelectedValue the item itself + SelectedValue = SelectedItem; + return; + } + + var selectedItem = SelectedItem; + + try + { + _isSelectionChangeActive = true; + + if (_bindingHelper is null) + { + _bindingHelper = new BindingHelper(value); + } + else + { + _bindingHelper.UpdateBinding(value); + } + + // Re-evaluate SelectedValue with the new binding + SelectedValue = _bindingHelper.Evaluate(selectedItem); + } + finally + { + _isSelectionChangeActive = false; + } + } } /// @@ -815,6 +898,10 @@ namespace Avalonia.Controls.Primitives new BindingValue(SelectedItems)); _oldSelectedItems = SelectedItems; } + else if (e.PropertyName == nameof(ISelectionModel.Source)) + { + ClearValue(SelectedValueProperty); + } } /// @@ -845,6 +932,11 @@ namespace Avalonia.Controls.Primitives Mark(i, false); } + if (!_isSelectionChangeActive) + { + UpdateSelectedValueFromItem(); + } + var route = BuildEventRoute(SelectionChangedEvent); if (route.HasHandlers) @@ -871,6 +963,109 @@ namespace Avalonia.Controls.Primitives } } + private void SelectItemWithValue(object? value) + { + if (ItemCount == 0 || _isSelectionChangeActive) + return; + + try + { + _isSelectionChangeActive = true; + var si = FindItemWithValue(value); + if (si != AvaloniaProperty.UnsetValue) + { + SelectedItem = si; + } + else + { + SelectedItem = null; + } + } + finally + { + _isSelectionChangeActive = false; + } + } + + private object FindItemWithValue(object? value) + { + if (ItemCount == 0 || value is null) + { + return AvaloniaProperty.UnsetValue; + } + + var items = Items; + var binding = SelectedValueBinding; + + if (binding is null) + { + // No SelectedValueBinding set, SelectedValue is the item itself + // Still verify the value passed in is in the Items list + var index = items!.IndexOf(value); + + if (index >= 0) + { + return value; + } + else + { + return AvaloniaProperty.UnsetValue; + } + } + + _bindingHelper ??= new BindingHelper(binding); + + // Matching UWP behavior, if duplicates are present, return the first item matching + // the SelectedValue provided + foreach (var item in items!) + { + var itemValue = _bindingHelper.Evaluate(item); + + if (itemValue.Equals(value)) + { + return item; + } + } + + return AvaloniaProperty.UnsetValue; + } + + private void UpdateSelectedValueFromItem() + { + if (_isSelectionChangeActive) + return; + + var binding = SelectedValueBinding; + var item = SelectedItem; + + if (binding is null || item is null) + { + // No SelectedValueBinding, SelectedValue is Item itself + try + { + _isSelectionChangeActive = true; + SelectedValue = item; + } + finally + { + _isSelectionChangeActive = false; + } + return; + } + + _bindingHelper ??= new BindingHelper(binding); + + try + { + _isSelectionChangeActive = true; + SelectedValue = _bindingHelper.Evaluate(item); + } + finally + { + _isSelectionChangeActive = false; + } + } + private void AutoScrollToSelectedItemIfNecessary() { if (AutoScrollToSelectedItem && @@ -1037,6 +1232,13 @@ namespace Avalonia.Controls.Primitives Selection.Clear(); } + if (state.SelectedValue.HasValue) + { + var item = FindItemWithValue(state.SelectedValue.Value); + if (item != AvaloniaProperty.UnsetValue) + state.SelectedItem = item; + } + if (state.SelectedIndex.HasValue) { SelectedIndex = state.SelectedIndex.Value; @@ -1098,6 +1300,7 @@ namespace Avalonia.Controls.Primitives { private Optional _selectedIndex; private Optional _selectedItem; + private Optional _selectedValue; public int UpdateCount { get; set; } public Optional Selection { get; set; } @@ -1122,6 +1325,54 @@ namespace Avalonia.Controls.Primitives _selectedIndex = default; } } + + public Optional SelectedValue + { + get => _selectedValue; + set + { + _selectedValue = value; + } + } + } + + /// + /// Helper class for evaluating a binding from an Item and IBinding instance + /// + private class BindingHelper : StyledElement + { + public BindingHelper(IBinding binding) + { + UpdateBinding(binding); + } + + public static readonly StyledProperty ValueProperty = + AvaloniaProperty.Register("Value"); + + public object Evaluate(object? dataContext) + { + dataContext = dataContext ?? throw new ArgumentNullException(nameof(dataContext)); + + // Only update the DataContext if necessary + if (!dataContext.Equals(DataContext)) + DataContext = dataContext; + + return GetValue(ValueProperty); + } + + public void UpdateBinding(IBinding binding) + { + _lastBinding = binding; + var ib = binding.Initiate(this, ValueProperty); + if (ib is null) + { + throw new InvalidOperationException("Unable to create binding"); + } + + BindingOperations.Apply(this, ValueProperty, ib, null); + } + + private IBinding? _lastBinding; } } } From b9c666e1935182c657436c9e8cd909a262e2cca8 Mon Sep 17 00:00:00 2001 From: amwx <40413319+amwx@users.noreply.github.com> Date: Thu, 2 Feb 2023 17:50:38 -0500 Subject: [PATCH 048/185] Add Tests --- ...electingItemsControlTests_SelectedValue.cs | 330 ++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_SelectedValue.cs diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_SelectedValue.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_SelectedValue.cs new file mode 100644 index 0000000000..df81b1faae --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_SelectedValue.cs @@ -0,0 +1,330 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Styling; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Primitives +{ + public class SelectingItemsControlTests_SelectedValue + { + [Fact] + public void Setting_SelectedItem_Sets_SelectedValue() + { + var items = TestClass.GetItems(); + var sic = new SelectingItemsControl + { + Items = items, + SelectedValueBinding = new Binding("Name"), + Template = Template() + }; + + sic.SelectedItem = items[0]; + + Assert.Equal(items[0].Name, sic.SelectedValue); + } + + [Fact] + public void Setting_SelectedIndex_Sets_SelectedValue() + { + var items = TestClass.GetItems(); + var sic = new SelectingItemsControl + { + Items = items, + SelectedValueBinding = new Binding("Name"), + Template = Template() + }; + + sic.SelectedIndex = 0; + + Assert.Equal(items[0].Name, sic.SelectedValue); + } + + [Fact] + public void Setting_SelectedItems_Sets_SelectedValue() + { + var items = TestClass.GetItems(); + var sic = new ListBox + { + Items = items, + SelectedValueBinding = new Binding("Name"), + Template = Template() + }; + + sic.SelectedItems = new List + { + items[1], + items[3], + items[4] + }; + + // When interacting, SelectedItem is the first item in the SelectedItems collection + // But when set here, it's the last + Assert.Equal(items[4].Name, sic.SelectedValue); + } + + [Fact] + public void Setting_SelectedValue_Sets_SelectedIndex() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var items = TestClass.GetItems(); + var sic = new SelectingItemsControl + { + Items = items, + SelectedValueBinding = new Binding("Name"), + Template = Template() + }; + + Prepare(sic); + + sic.SelectedValue = items[1].Name; + + Assert.Equal(1, sic.SelectedIndex); + } + } + + [Fact] + public void Setting_SelectedValue_Sets_SelectedItem() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var items = TestClass.GetItems(); + var sic = new SelectingItemsControl + { + Items = items, + SelectedValueBinding = new Binding("Name"), + Template = Template() + }; + + Prepare(sic); + + sic.SelectedValue = "Item2"; + + Assert.Equal(items[1], sic.SelectedItem); + } + } + + [Fact] + public void Changing_SelectedValueBinding_Updates_SelectedValue() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var items = TestClass.GetItems(); + var sic = new SelectingItemsControl + { + Items = items, + SelectedValueBinding = new Binding("Name"), + Template = Template() + }; + + sic.SelectedValue = "Item2"; + + sic.SelectedValueBinding = new Binding("AltProperty"); + + // Ensure SelectedItem didn't change + Assert.Equal(items[1], sic.SelectedItem); + + + Assert.Equal("Alt2", sic.SelectedValue); + } + } + + [Fact] + public void SelectedValue_With_Null_SelectedValueBinding_Is_Item() + { + var items = TestClass.GetItems(); + var sic = new SelectingItemsControl + { + Items = items, + Template = Template() + }; + + sic.SelectedIndex = 0; + + Assert.Equal(items[0], sic.SelectedValue); + } + + [Fact] + public void Setting_SelectedValue_Before_Initialize_Should_Retain_Selection() + { + var items = TestClass.GetItems(); + var sic = new SelectingItemsControl + { + Items = items, + Template = Template(), + SelectedValueBinding = new Binding("Name"), + SelectedValue = "Item2" + }; + + sic.BeginInit(); + sic.EndInit(); + + Assert.Equal(items[1].Name, sic.SelectedValue); + } + + [Fact] + public void Setting_SelectedValue_During_Initialize_Should_Take_Priority_Over_Previous_Value() + { + var items = TestClass.GetItems(); + var sic = new SelectingItemsControl + { + Items = items, + Template = Template(), + SelectedValueBinding = new Binding("Name"), + SelectedValue = "Item2" + }; + + sic.BeginInit(); + sic.SelectedValue = "Item1"; + sic.EndInit(); + + Assert.Equal(items[0].Name, sic.SelectedValue); + } + + [Fact] + public void Changing_Items_Should_Clear_SelectedValue() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var items = TestClass.GetItems(); + var sic = new SelectingItemsControl + { + Items = items, + Template = Template(), + SelectedValueBinding = new Binding("Name"), + SelectedValue = "Item2" + }; + + Prepare(sic); + + sic.Items = new List + { + new TestClass("NewItem", string.Empty) + }; + + Assert.Equal(null, sic.SelectedValue); + } + } + + [Fact] + public void Setting_SelectedValue_Should_Raise_SelectionChanged_Event() + { + // Unlike SelectedIndex/SelectedItem tests, we need the ItemsControl to + // initialize so that SelectedValue can actually be looked up + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var items = TestClass.GetItems(); + var sic = new SelectingItemsControl + { + Items = items, + Template = Template(), + SelectedValueBinding = new Binding("Name"), + }; + + Prepare(sic); + + var called = false; + sic.SelectionChanged += (s, e) => + { + Assert.Same(items[1], e.AddedItems.Cast().Single()); + Assert.Empty(e.RemovedItems); + called = true; + }; + + sic.SelectedValue = "Item2"; + Assert.True(called); + } + } + + [Fact] + public void Clearing_SelectedValue_Should_Raise_SelectionChanged_Event() + { + var items = TestClass.GetItems(); + var sic = new SelectingItemsControl + { + Items = items, + Template = Template(), + SelectedValueBinding = new Binding("Name"), + SelectedValue = "Item2" + }; + + var called = false; + sic.SelectionChanged += (s, e) => + { + Assert.Same(items[1], e.RemovedItems.Cast().Single()); + Assert.Empty(e.AddedItems); + called = true; + }; + + sic.SelectedValue = null; + Assert.True(called); + } + + private static FuncControlTemplate Template() + { + return new FuncControlTemplate((control, scope) => + new ItemsPresenter + { + Name = "itemsPresenter", + [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], + }.RegisterInNameScope(scope)); + } + + private static void Prepare(SelectingItemsControl target) + { + var root = new TestRoot + { + Child = target, + Width = 100, + Height = 100, + Styles = + { + new Style(x => x.Is()) + { + Setters = + { + new Setter(ListBox.TemplateProperty, Template()), + }, + }, + }, + }; + + root.LayoutManager.ExecuteInitialLayoutPass(); + } + } + + internal class TestClass + { + public TestClass(string name, string alt) + { + Name = name; + AltProperty = alt; + } + + public string Name { get; set; } + + public string AltProperty { get; set; } + + public static List GetItems() + { + return new List + { + new TestClass("Item1", "Alt1"), + new TestClass("Item2", "Alt2"), + new TestClass("Item3", "Alt3"), + new TestClass("Item4", "Alt4"), + new TestClass("Item5", "Alt5"), + }; + } + } +} + + From a8cb64160957920b85c9c865753c42f829a5b294 Mon Sep 17 00:00:00 2001 From: amwx <40413319+amwx@users.noreply.github.com> Date: Thu, 2 Feb 2023 18:02:32 -0500 Subject: [PATCH 049/185] Missing xml docs --- src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 2fac60c8d8..67ad1963ff 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -225,6 +225,10 @@ namespace Avalonia.Controls.Primitives } } + /// + /// Gets the instance used to obtain the + /// property + /// [AssignBinding] public IBinding? SelectedValueBinding { @@ -232,6 +236,10 @@ namespace Avalonia.Controls.Primitives set => SetValue(SelectedValueBindingProperty, value); } + /// + /// Gets or sets the value of the selected item, obtained using + /// + /// public object? SelectedValue { get => GetValue(SelectedValueProperty); From 07adf1d6dc82bf776b2adef730797fd585662def Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 3 Feb 2023 09:12:18 +0100 Subject: [PATCH 050/185] Try a different font --- .../Assets/NotoSansHebrew-Regular.ttf | Bin 0 -> 42284 bytes .../Avalonia.Skia.RenderTests.csproj | 2 +- .../Avalonia.Skia.UnitTests.csproj | 2 +- .../Media/CustomFontManagerImpl.cs | 4 +++- 4 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 tests/Avalonia.RenderTests/Assets/NotoSansHebrew-Regular.ttf diff --git a/tests/Avalonia.RenderTests/Assets/NotoSansHebrew-Regular.ttf b/tests/Avalonia.RenderTests/Assets/NotoSansHebrew-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..703cfa472d19b1ee2b371da8b88e69caf2ba9c26 GIT binary patch literal 42284 zcmcG%2Vk4U`9FU5%9gw>S<99@v?bfJBukd$ZOcR6Q+B*E?ChOD5<=K}1j-7lKxx=z zl?`PSQbvIkC{T8R1PU#Cm5~-K{XTcEWGeyM@Av=z{hXZi>e0Q=-97i*^L*~PyH|t~ zLgaW!A*7?NJv>#h_>Y8~ehMMu@sv#B3mO2}0Y5h8I8 zMBLsp*X4di$fjGsWjZnv8u{kFeP%+g{Q#famTj2XyyJ>3GlXo;A|&;~RWn;Qv(mu- z7`&&gTEAoELr;vRp)7pA^wHHTW|kM_Tn*lXUpmhTaR2X>rKb^ zUlSs`aQ&ubGh<_$&Lw2o7pTuO8)lB&OlQf{`20Nb8#c~tSaIRK4}M6}gC^Kv8=EGNPUM)TQRD8c1`DH{Pi&2+<344hIpU4)4QDHSIjEm{z(Mzd6 z`X^!UY``ZhN4`-)gg**{Xp4c68HG>b^EL{8ysM;Avk{*(8nxQ&ud><9W(Ac^dP5@~ z)nxFrBchLf+cx2xEUdC#Os|gKA$L!7(xT|!ww_ia4BAIp{`#~#pp8nv*F(w#lTe6y zLDEDz$N(85OUNp+iEJmw6B1IHP5vsM*B*b@9`c@`%i?qzf43g;`OxnZ zpA-BNd=C9g_ZR5(1)T+jg#|nGI=xQ8KJL@uZ$z)t>8XjmMla^qHFH^`Zx<9IuTHPm zz0LAI%dT+0PT$FM?!lEBX&1dRzmqmUMLJQ(PP?vAhxJK)$mo5qHN&)W*v|Iw-E~d}mqsI7Q zL&;>b`+2j)q2!MX0L7wQE*JW#rL|(gNbQa#)zi+3)t>25_heIhxNe{{TyJai>z%g4 zIyY|+%$~3_Jtbqjqj4sn&e1*;bZ~0*?;$Iil9r$O3ME%xeRU6gCu)92S8g-QmCoGg zpP9^>c)RuzBihAm!a;3fUC9NgG^&lVK!7T{*4H&}9|_JFEvwxA0q256g`FCwp013( zuTa#|d%cTVyVupIlo3sVvArtXlaX3ThX(&FNkh$pC@C8fG!rlMvZ_&nKGCXSe@r=o z%s1afW|K?MdgDb))v974s3_|`Y)I{s=T&Ii7y9!h(p*=lym3)g^@^}>yw&*?d?Iczf2)jI;-hjey`PQ zmgic07Ox+2_M*;g6mU6Xj7sKolxaxvkS!MMypp2Mx&$E&fJ^(=* zLJ%}0-5L9eJWh6yOhUF-F;fBIp;Dce0-e>WE3gEt#l=>O0eJ;6uV6!7?ZLTW1%7!EjgXcg}Fsm(HTkSD(nz3L14WIU-;@saK?IrHMuj?#1O zVcT(M8BV*v(djtP(e5}tdi=R1=hC}(x4wYC*4-^H;jd-)Zb%zepRVNyfrO};(zVfV z7tjob3_y&3zfSx-=)83sypj4a4yEZDYzH-h1*Co*L|DaS1iz!l8|rM)o3s@x+A_2 z?jF4Cth%OOT-rBq*?Eoi=Ul;RR*3r-;(jY3?HE$rqWZyuI~aA2poZRX!^;O2UDz<; zDGWFqfkN+Ot;wqo1grfzulY)9>hSL}l`6YvUa9dyf7skfRSx5MzJ?5GMuTscp;Sp( zX$F)wEtWPW783NsnTe%xc*Ojf8it&eD>WO;l_(+@&{bL*$32BXyFFOwov2$tRi%dW zK22qv=WdXCb!x^iQsAvEFojb3TtnQq3AV)!LDPgz{ zImY08{YV8nckbW6KzOb52LM`uWYL%D6mn!R9}t4gG{dj40WDt8sdSY*b%`v!&?G;S zT`rcU7aHZlUfcMHInz8eY?-?_m>c{E=LTjz8ROO{fuEgXH@9Rc6{(BwWS7mdob1e$ z#lj=z;USA{Y!obUQP>=6)S>pM8@YzpjYJq9#wLs17UGqn`_03{W_DLnUW~#>qPS~# zURmyZ5h7=SI7l^pg}w?a4J;552U}2lbNr_1?6aGG^T(SS&px~HCYtlK<>s5MPiKp7 ztc`Zu`*PymkT^sciU7c&FN=3Qc_X{|MoOQy-h8tKH}f~#8=_CqF|>%|?1;oHF(U0& z1yxjo7h!KSN(Gi!?;E~Tt!hs)P+9CfMl+ zvx*^T1Uxx<-S4BJC!g%Ze|k#CbI)~fAKgq&q5l>dc}@-h<864#d((~Hn{TMP@usR9 zg+|{^H~DV7(Rb60RX3wFFWDia3mx3L=}|WPo6N+#m1smIN!MQb>L*&GQL8l=wDEV^ z(9!XDk-?d7jR8Qs-}CR0lJs&?&x^3ql33?D@vbczID6R z>Q7atdg+en>GaiT>7&28tN&v!ZU0r#`eM;X=?JZNi0N;96nNlt1&`W8J-7eH}gFUQBpwcAE{8o2`BH?3yk( z;3Tr}(zdm0w{Ks&c3b1n;NbAk(BM!Nvz*aS67;6QK~RSW2V=3=7GXO%TX-WyM#Is! z35ms`N?~#IO(6?C0^`H)k(Vw0S*9+ZoO>u}d_tPS1? zc33!!oGZM;`aHR{3sog0Dot^*@Q%Ky5PwB_^a*ok#|h8EcH{y1+Kn(?KpwqL0nxA0{V?^q7UU zc#b|q?_hF_*MH+2y^s8cG|bV@pQHC{!Z8OVPGT=Jlw@Lj;s%wfpXag=CQ zC`CAHR;w}b7UxH_;Sw$Asrp6G0My{)=lht7V{6%RNfBn0-Z^=}dCf;MW?R6N*7nnuzHj92VEyLtP2$fdlU@$#fn(gQkW`RErYALDcek(h+;&@ZBg(Q~7JqUPvZ6dJ~q zMdV7q;9UDfsgjpCSI#Ag-b1E;lHPp~osA=rPZ)TOjU$l{lQyR#9`i~PosBOse+M}| ziO$BCNFOF!MS9G_S~W)>qG)$gYZ2kvVotR7!Aagy6Xs2O2WAIYKGy5!KtGMlw>i!pZSoM7vnP1g}2ffG&(Xu zg^`gerBJ7;s#489p=9lK#9H82_hL@7n6;Ozq7mLM#;1|%52wR!_i;H;&?8Co?%3;` zj^3m}3DX}je>ip~r}O%7`H1|3h~+W866s8T#QYs7oza0rQ7+RTkv<&zRLl?kLA&B~ zDIHpfjIldoE#zLb7W0FSxwuIn9+XVUx8%qzs)CBbN!Q53GHGv$ROPJ@{x}wCKys5W?0bhOV&hB0y9ZuH9a7L9mZ6Y90nt+Vy5V`Uh- z%wjx`2&RG@(gqgF~yln|BPY z@zRx%)0bBHXU>RhymsN#b(Qyf9kF$D^2knCyjoC8*CkX!S=#M=a3ec)M+OXyjb@mc^Et z`?XUsifdJAv{W?Jq8zFhSv(C@PA6nhHDPr%_*|YPzUUrZdl^I$E!XDLrh<@TPBQu0 zHb-~m*<+ASzPhEf!*lldiCxj!JQbwWu-r-?Q{=Uun6Tl`OPEfGb?ygdV+aAVk}OA0 z?plUZ zc#xYGpM646%f`Xs$$O?c+a{gk!rqO`okPvm=tsidzUbeJnycCY=I43kpe>^8G225F z04kR%vI1wJtgvO{@X+Ms^!dj{Uk^`|k1d^`$5jkBSuYp%9<%XZMWL$p*680EzlPWX zuEQFRHW}2^ni0wSkDBaW*nabgJEhsupH=6vbCzoFyo&ymM#tN*1oFsb@vB6nSf(^f z=4HYnC)75vrMfI+d3?>S=S=Qdxo&LG+AHj>m~5-*t;mkKa}1R9}pdQc`mLGN&3tg1>V&Bqlfj6MrD;So5+$JzEy7aYHg4&6bk%Du>uUU+mg26EtKFroUDaB*)KO?1b63p&0HYY6;_$eo>HyC?rkos8FFg!`sBu6SQv&M-L3J9_2ZB`+vdP--8(e z^PCyzIoyeMGCJC0PPFF)@Pl`X_Z-IqY&ye`fp9|o(MwJ}YjV-z(2#B72h`#G?ev*w z<=8Z8MW18kF^QbO%Y)q5oRungc_N)*Ly_J~Zc3svn<~<~nGc%d!)&lf563aoq5L}# z%bw3a&!>xAkVI#gNaQn2?%?)p5_4A8QZ*@sm*Z;%@d+E#&0r8q2BPX7BmnwizFMJ8 z%PCa(y#|-6so0_XZL&z#6&0k?>Xg(fpP|82Gtj>Lagl&kKSu9}5BB&I`W0SGPS7pkxQ&Y1Q83=FjC`h0vP??|a4HP5f1#Q5pxa1<-jTVJ) zw@YHrk;p_g5-sA0fGx$i06)n^4^N#X&B~BbnwpU*JALZ+i#JI#(xnoKEF(*L+@h0Z z_)a3p$VuDCilQ&6osJ@ztjJNJjj}0luePGxp;VPsxHR;(XusC&EL9W)>r3ceOb2*9 zxDH$%*8y=Jb11!=invvzv%VJdhso`{?+(r1LG4^hDUeba?PRk&vqWN6LQqPdL;=lb*$hb zPxND|p!c&D@VvYQSK*RHULNK(I+Wf`x|8Uv1!Dei?B7Xrrj;VSgB-?NU_)Khs0(Ip zh;xYRT}DA2RnQ}&7t+_GXV8<{gaDSk_OjaW+`Kl|$7}Nh=YA-?hYTgr*}RJ9$Gr1N zuJbO~<^6&YQ6ABU1IyYGv1axeN*@fuWx!0ag;FJ-5wfYepBk1fZdRmdE1N1jea_=f zo~rei^&0i&>Hg(?!Sc3A1hf`Mcl8$8^GqFq!LbyHp|z?&-lEh;-%Y9ZRh0GgdsrL9 zvZJWeP4PB7#OJg+r^!}va(kD{@T{m0|ixrseXNDV0ucK*O#X#@L5g1LiA9eyHt>vLRI;S7L~5F zqSO_Ahj){)t;+M#K)0)%DHR!o755-ct^qCw-`wB1si}5Nr@780 zPm#Bjon+grc4?2U4jYXf!AP&tQ76piTGXw9nzF-o4lOypscu70&$`+yol2;gw)F&? zbM#r!7aYCSmA&ON3rm8fEK14t6x=QxfQ&GUy3^EOQppn?F$^4vi8)Bx+XL4bGeX*_L|u06zs4TEBJh;-CQt(En>6% zIk3jKLie)(DBqLEr%g)#XmM&*xN7Zi?TpQ~vbKEyOXh`<;OcGLwp273yTYZlwmfVM zF*!;*X7}~3s!}M1RGOi%I;$eR)N#SdCtT9)S=h2{jk+T2QFl$2aZO-#Vt#Hf`VoF^ zh1g$=%JfF0j#SQ5S)Vg1>+|REPWdjv0*oVF+$P6&>9AP^bKFuMn}Q*hDyo`Ip)u!J zZDV~~(fGJfpmfL!Ey~gb?a^!KSg5&TtO-H61B@pSh`@N7z>~-4nX&&V#%!G4`qE1x zcXMb#f_rbTaA2}!OljvOjI~Gaqpc`mK~pp;ENq!%Hj+iN@4@OB-`f#Eb%RN(g13%W zBv#S#XFm|~pPpMv6I)Df74mT>i*WmHMO*TjgnChRHJ959T$-I0P-CxwT9!oZzMWCO z0X0Wppca)a01Iy1#WS-(w3j>fw=76_xdd#)vXizeq4GT8$wy(bPP-Ao?1e zD61)Cn*EWG-&JVMbGlq`Xo+#j>?^Z22bJn$MbWY8r5KA7ZH#%Izd6no19AD{1TQ87 zNCsn(S%bgDB?gWD5glG+Ij5pG3OG_>xiN!pna|6xsYvZ#z-t(j3p&U}#3+f_M6%h4 ziv+cYiqf6=rtF-;ydqm|LtVINydx(y&(~~f87nVoN~jjSIn?BYEIt%v!EDPgw3Ur_ zh6C&Q14JIfhl}2IA6grL?=r!^imRs>^NDCK^I<%gi*UA9$wRoQRW`dsjOz;ahRuD} z=0ZnpMTzmWE!LJodtF75DY`B*`%OiK4kD<{%6wDdEPzO8_QbIvg}mC{Xj##G>Ul~< zwXMm#tkD)Kpx&(P^-HXc`O)Vxv%HJu>&o;%tZV-ut7`yrl;bc1w7@ggacw=+>D$G4 zUBde|3aR4GNge?LM6$-Al5kzyOjYy>A=2mXb2$3_ZL19>D_g=Fn(8+8HZ3n!1>2)H z4}?ov{Z%anLo4m94+HM%f`YMreV2cFl7pz~V6ZxL?8Nxdq3VF8vY>0_iq?g0*JMk0 z%0>ITU2Qh#(kL|716@kQo#Rsw%t07lf$3t3%N)2YzJ$ppBXnfhj^MJ^(&h>V9LGDe zQsfm)`anxDEo`l=Y%1vxOgqn-I<`q;Rx7HMZ%Wd-uwk;hbht^;vd|M)RKIZJT*(Ju z!^`G+6~zK@^KkK;pTrtDn>BL)0ej*JE^Fb4l}ZG-D~h5U%a`q#3U?2;388V6PP0+8 z+M<>jOqu&7AMDSm$Oh6s`-~MU7A{mSTjyCMjv*1l@EKTpGFsuNh#?xY1W~#8q@7JH z9uOo_?NIZ~j_T#1rosG_?C|)d!Pa8hHr5&L$jtL7Odd7`Yx0=8emFqgj!0wP$~A{A zS+Xu}!{|AXvuMM_xfixFz;KEtY#`U1ota&dQ&R0L81KwTQFs~!lSDEkNzu7mN~7-z z`Adr{i6YrD62p|cwO&)>uj+PenyehP7Mr^5ff`+d&(~?Y zuqtfHlMl-EP7*N-E zZYdo+p0$g`i<7CNvs}usjNQPw^-QC3Bur&d^Tf2;UyPmc@en!oFgGGhKIgMUG^`Oa z3d_n0vu##ORiS$%=5UZ&iJrEIgF+{#1 zX)g;y=0S039U7OWD$CBP$}j71_V$^pF2CH}C`)OTr5AOEXmy$S@T1$Jdn?^c8jRa3 z%)3(vl+6suZmtt@NC~CH>F9kpElLI;B{fhE)?7nLAu0x+0Mc0`T2Nyms0cKho5!3J zwGH*{MFPNMN1;Vgx}XiAyc?Q@|FZP%bDDRI-l+vz=c`Cd|oX7ByBGn?G@^iHtqA=769xV10uXP{WGXdGK zmmeS-%$T-_@nP6MM0O>A`qSdW)ADj8DJi*1+2MQ2ka6jCH<>6xiZrWeX` zv+!MJZd%XeMXRV=U1~LCW*Tgz>gZok^1DTKer1-$;gC~T^hIq|rBdN*D5k}z2HGap zgY^g&Q5Z6e$J-92caz_7S%`EdeU=|Hz1OWN7%HWn*(<$0rKyszkfJRw>OhOd z5?G7b?stZTujVZ~m=0}ov6hKc)?Sty?R|Tmn&2U&9n8=&q)Tr@-P4mJ$%x4xNe$62 z>E`Hr)Lct1Yio&aWIV*&j0e^Yc|8ELe?*66pEa+?< zAL+solz%%bE1fI}r9%^Ak>N3WPq4NQ+xKetevs1G@raXbWou;H`QB&jmEObmK4&Pg zV|z_vb2Tfw1bH775+@;eZfwj>KBRCTyR$`>n}dznYZFHyI1j#inr@6AhG5(}W8LI& z_&0Gz>{FVt*d)dx(3;h?1?#sZPCL-LS0zXwC|2&H4h1r9f|f&ldFRjn1-S zPgQ{;tS($URMF%wE-Wi8QtV0;kDBMln(4Kuc?n5SDnLEf>dHbx1*?HhK39XH(z3$&8bDWZ0FpdTua)BL2z0+3 zW1x?GNz3Vn%qFr^5=zGXhKR~~uT9{HSj9<3Tv2>Xf~*`9d%s<;`R=}v9z z21yszj#>yjg}7%1uyjuLXuVb@RmI^b3umz-T?}c&gXriZ1ie^@r&z$8gi#SOh5|_? zU212u)8&_@=7s&ek*Xyj=V*QgwHB0=YkN+PjGi*#3ZK5TVN-eey3i57nW)|Pt7ZKs z&-kMku+zkf6NC&WiM=Vq3JE*@!ZueV zt)hs#g_LtTDB-geHG2Z0suGQyPHCQUc*60c9@+wQP zrL0I0oOVlt6(<5MF7#Y5cA{WKqypXtXG++=q=}SKu-2}u&9y5U7y5$BLp1MrNz)I1 zpy#Eey4FukZ(zF^>tf$y-C>pl*xC`a7+oUTw)is0JXFNKAZRyw(S8tK3^FV!Hrxhl z-0CT@I94vH?zieqZJzqd{2E_Xr`=U#wr^P#95!gGn@hT@mBCs2F zxv9WmRmxnf|JVtETv#X_PU*D(J@ODM8l+1a6JtW$^z3q*h0 zvZb!cP)T!PQGr`s7piH|))eG>y@RJM3j~&(HrRXWiW>Ibym^7g6*;V}?eK`p6*>IW zqgRAh)W~xhva?6Jdn4Hy%^5l8vV4jA*t_2~dU$*2@CB8LcbsTX)I1yOUS*^ab!VHP zA#A2}W_6HbbF*2k{BQNvo-o|&Dc-tZd860vbT}&;5+(mkjd#*}nnUhj)}y!H`mv+L zY}gq+@xRwsAPSr;`#mgyip3d2SdnA95}`HNl!$+9evG}dY)^=Swl7{hGu4dQ=nDUI z3RWtF#tmI$fR$x2w|SqF345D_re%d%LTm-CkE!=B%o6mSN$Fk}1pv z-UnYZY7M3R@j4%3l74Yy2XO>v#@1e-)3xXW9VTtlh zkXKB#w~v=8viq`gg_-5)^@mSPZEZ{+Vfxq@`y-I*cZhj7+3Lr3OdxSaSUX<;4iB?B z*wuU{hO&d)d>}?6+D+|jaJ9j-*4Hvp>RwRW>>sn}4SkN5@ru<;nw#qas!C_G#idoc z9F0FWK?dZZ zg~f9Vf5F)2s})=xKp0pPV|HAn#RZ=kCQcOC5sSA~p`+OuC4?7`@s5{T3w!L=Mz<;b z>+dOj>*Xx(V8A|5QqtoYIy{^^*hy*UKz4Ym@E1*fXRc1`>GXs$@-ouZnW5^zGG%su zwz6@{KqyO|ovD_FhL6O#bz*=DXi+QHOY?&_zD9ucGVHNt2ld#x0GuxE500Bkin~f{ z>YsT=S&~Ojh^7}gb<@+{g~DI*F~iQUYdNcBwZm`79PE}EYn*FZF03D^Ksmtlf)iS- zK{){3Xe=*7Jb(r6*ric)*TScdx#W^d79bt< z-w(&VnanS)A0_)enr(4w3LEqD>n%k!E{j`T#IFnMXp=nCRM4-}XQ|jlpRO<)JZ*?Q zYjA2+3hl?<8EiGNz2yC<|L)y1lRh4;r>mmB#LB?4I2%-j6$2>=UI#24o51OzoB4U7 z?pP&5*Re5BZse4n81}@0vI3MVg!gd5s5jQYDHnpWSyBl~Uu-?2MAiJH(mqi7XFuh6 z?!dXePV6_A5(_VB4`SxcvB!{OG?vYCT#5C8<#;#;q7dh?a(;{Ti3rKX=b_Wsh`vbJ zPJb)Bfw>U?HN1>y-r!4uN!J$@DwTySJ){>e^umogtyWinzxX?oxC1r*Qi!tMJ`rqF zQkyZQ^q-$;wC2_+%1VvtLR1s1QfB4Y%N;pP%aikC=4poZ1btton@UUbYIDt|%(cST zIgWCBPL{GNs6kzkpPtM4vptS;cgW_h=(%adcDurxV<^d(zCCeWoH;Fgo$YYO8D-30 ziglnzfhWAQSZ54-UrzfvoS6!g5)-@_CE3$;eT7Q)?Tj9~`J&J5)m0ug@uta!|t>NIL zC-dVzKDq-zx-gF6|+HX2D15}WonPfbnr@at^qDU?8PDB zI$l@UF*+1|Rk)6ym3&wjrvczNXf;2F>qlV3M*}I^;#^N^M!HNG6F$!i)M&DDY^8Q| zAtm=iPivu5h&ROVfL_zuG)0bNEH&SlS1Ej@@K?Q9;&#{u=F z6d3b7xJeib=LM@ZSqguR8ujyX>o|yW5egQcn4xx`Qfm}j3-_cB;@B}Zny~o}eKZ<7 zckP$Ij%L}re0pPr=JR4>xq8wXrpLV)z3ihav8h_#n&s{DU;De3EN^esr5ALrX@iwW zl!1Bv0v;7uO0JI`4m^Z;u}}(Guv53(V$86Uj3^9huck=_KN8)%mYz5J&Iu<>QeC`I zVQ}_#Iva^tUwI{Z@ge2Kj)lb^oGb6w@$*35SlL1J>*FymFKyswb7hb)Kib33xCV|e6=gsrjWq{tYP+>fhUOu4CyNshL(vEl+S@hurpn%v#B zpML7;Z}K|iGR_E`FhGS=aLQzJ+8MS^2S>=37BR5YbWvk)@AE5{MOV>(&=bRf$mFu< zh6rDwir)vxEC&~Ve?{sGbJUyLm>zoZ1cq&Ni0NUnC-COw<|>uBxp~y7%*#_M zm3gQSG=YAF`tbADGYG^|R*#0s=ob2cV3^&{I+~Rf&xewhiREL8%6dmm9*aE;Eh3PL zJ*bvxn?nPqv6&rk0ndmX>NY1!{ExA9uNk^8FQUfJECBY?i@93dpCR z$6A_4+^6qqIegUfg`j93bLB;f!&}E@KSzt=ccDeA#TFgT^d*d!dhp`s;PKoi)-`za z6YHlitw7f-r!9@aXN5uDHPjUjFzJM2o7i38wU&>wljy^Er$hz$yuy2%j3xE;F-1kL zPJjCs!mZ|_NMrYGCo4Gv>tPgmv)O(ov_sSk-V6rNnI)J*JoJW@D_(rFeQl>DdUsu~ zWl~dCl%`0_%>P2jU%eqx-oGw1`#ycbv!LdoyY0RLiv3#b3~syx)N%u_?M?( z3i-W^l2f@A$L3k3LSmoS@iR_da$J#YL|v@TlH^UwCFv@2b@>-`ADxL_94(_44e6ws zlJdfbZ!y(+OAUZ4q7)zgNhun`^tP4DX5XccHwL43B#V(f-C&AbElLZ1E9le0h-Qh; zL%`z}jB+}~<8Azj2wZQ3o#ZKrmT4B#b38&q?Hvre0>aMLZm-+Pbnn09U{8CP!qXlG zq2ExY2m>yxRQ_xRcJUa0G6OIDUkBCX;72ovRl+l2gq?Jn*GP<5@j98~b$faUb=y18 zIak4!9zmliltec`DIxtV>+5r^PX@yEs=F_Mkv_IbZPhCGoQ-U3>2=b~nel%agd@ zdvAhm694&H&vA!bFq8PXns05nGj!4+b^mOg+r2L5C%DhoUi5>Q5sk5V599wITENO! z@}oA0rTkzl48&Yo?is*$+Ak8Ve&O~$D8=u_--$7DT-+Ld z!)-RB4`MXJkYRQl-}A%5e)RN}%O)FxFHVY9i}8y}kdgbiO=KJIUA8N1t|ue$PB=f& zhW};W*goMxVg!^b?gjpgrCCpE(fV{$$8wyUO-cY7?+p0F2Gzo5E@Ob_EuJVi*&joA;xAd zn6V(-gy$FGvelDekfXcFo0mrbd=8zu^w%-Ws-#N477H`?QfW2GDQuyU}lF z#{2CgHbain85Ux6DZROtl?6KtY;b^`4T%`lI8lQ0<7?>Q4=#BKC(Z?&!}yxceviB3 z`Ed8y{O+Yg>~3%Df77p0@)VFNWHiR6KH5_wk=97$-x2&VUd4ExiyW=SEDADI7k?!P z5IXl6W+xGrx1}{(#LE@Y_{I*vn8{B75F`5cf2-Fw4>lYTX zs2+5eqe>q`5^NU@+HPk%J|uK;`?NRu!%a*eG)l_|SQ-pdN zdp#2k(fjFT_l_t{dO741=M4E=5SRLwcxO*RGtg7uQ70eNII=tS1WRq?^#d_m~1Q$>N3i zPmI_t*qJ}?I*XquDq4Grzdb_NiY~G`t|!Qimi$ueX&jlsc|W{tit}KPKw~Bk+ird8 z$#B(C(}l$k(xWmNfmbfvPr5g z&}tnXL4teHOL!^eBhDtyf({*Xu<3_DOz+2R6yADYd=xe(!_0$*P zr9r}1iMm3yv5)8IY=km8M(9zAcFm!-72nFS12mqXh zEq1AlJ(Dcp(5cR_sdSD2`y`Xvw=4C9dCH<9s#g^jDf0^T!cL1lLz`_6_#O5@hRv1{ zuwx=evZDsy5r4@IX0$%E0~O=2!V3wT=KZ5^HI!Afy3e64@LS1qT)MR}_{Fy?m(96i)(U2Mkr%DFg=@vqjR~!I zSYqXz@f@)u!{M;7vyXWxftX|e6t;uAl0~rF`H~G*Mwr{j`bAEBu?eAKES<70TVB7i zyHuIkP3spP+hp%t(N-CDX}nX7XGqnNYJV_A|WCCmHSxNLh50Yh`>U{1z!7rZ23fX^k} zapZWAhe$3d86BQ?NC^p0D8%xVsyx<#>k=1cJ0fzaY^2bllH^QgrDPkM{K8;VQqr=j^V3glOBN4$_73UN4OP}( zo?aHNb}~;EF9|(*PrOGvW4F%Hv9FfX?Q|*TO1EMs&HaLb;|uY=L%)~O-*fNoMK5g= z(O0q$Pawc~sP?0hey)T$&XDm8WSsB^Zh&O;*8c(<{$G3ACmVa{ryf4CvChE9b^>oB z<`RF#jG2B3+><2aO#J8U{(Cl9?;n|WVW%xM^(QPj ztX>c7(q>^&+>4J@PQI;?`DyqMG4Re;uJ~dkHZwIY>#$AQ2b-D(?GlCEQqZhZhDsYd z&6#b-W}CW>7@>~nYaOert5_sFK^Wji||yLefU-D7dt!Ta$MX&`UJ z{scUxoX2CyR;bk-@b(9C)#_ZiTD`y0Qy%J>QOk2RYPnp^ed#$JT+H+VsXATKMTR?G@&zk%L6SD zMazpV=>|`u{kS6$rbb)h9MP61(FVjejZgBHY?`yM2icb79!uENGvhX9&bA!vrT&Lw z6Yat(CRPUE($v=Z z0KHHfz2MqybmXDg6WP*$*Gmx)7ncUExI23MpYP;L19^)Fe|Ig9)ZcPFTNywNvGNIB zJv8q7;Npo=+6Fqi6W>NwkyL+K5{uJai-&J%=8dj5p9CW_1w1<^J?%Ets$c_m5f?h~Q8p)Q$>+Xwt_(aT5xbP?9IS@bRx;>6*eiqRjiW`7lejI<~^CB9Sx@j=+Bs4lh@m~@&v zMNx)HiVSioz_WW6VY=(G$?ME&TCdJWPmnI>j=bCFkOQrAC6p zbbGoly(vACetP<@^k1jnoc?I~Khk3vnHky)bA~&kE~6tOlCe1B*o@0EuFJSPV^792 z8Lwu1nOTrom)VgS$y}1TE^~M0Uo!V)ew_JTmLw}TOP{qSYg^XcS#M+u+4b40v$tlS zkbPG63)ydFf0+GE4#{cG8Om9dvpVO@oQra<%DFY?{+uUrUdVYXw=ma{+mU-#?j^a` zlN21 zUR1oT_)zgp9?c8pou0QV?*(OwQm)i1ZOTezgR)Z@QC_S36Ltj|RONWE@r-Jh>UPyb zswY)1s@_(8sQN}t)S2pP^{9Hg`Xu!^>dVyEsc%<5rIBdt8lR?7)1?{FoTYhKvsd%7 zX20eW&39Ts+ov7Z-l6?0zaoD{{?7c1^RLRkCI936?+S#1?1KD)4Fwk#>@K*u;KhQs z3qCCPx*(>@)M<1^U4^bjH>SH>cZ2RO-NU-Qy1(gFdV{`PAJB*Nu$nRKRV(_+(UrVCB?o8B~i zXco*mbESEid6W5a^9|;^%#WMjv&bw?OQ$7bS!~&AIm5Eca-HQ~%PW>IEk9VbR7*`Ba{ZFks@w!dWm(mw0ZIw~BkjtR#m$I*_{ z9Tz!%>v-1jy5l{^fl^6nL1}5JzjUhfjM58BZ!6tX`c&yF`1fh)fwJ7Pk}_vmLs?hZ zsNQDrhK@3arv(DUzgugzNh>-XNptqY2?pNHOdXyfYr`0p$S?F2q+3GpPbDQVo%Ct&zrK@s=k7j{9am$Xw#+KyPe?uga z57=;j%X#h|_Wt&_&uxe*V=0n7lE2}j6c(8O3FG{El7zy)uq4rtQDK<JM3 zE}@=OB26H9kSdVWNKHsVBqtL4-iu%F>0}8jV4XA@JDTjIR5*%@RAkl!WkP+ZmIOAokmmFA>kz*CPI3e1$9VoYvHJd(B)O9WP`A-| z3esPJx0pKc-bnWm-=Rs^jy!Adi(m1CvIqVriT;4T%p;kHCP6{+XcW49XnFU!$_E8~%$n4P+QX`TU$$*rD#B2v*-?3+rCXmLERv=vno1w=09Y}kS zFmZ|fR%jy^;bfG9C)g3}J{8mHcxN{35u~qUv#?D$qNqN?p&VECSd`I;x|@=4&*~Zf2K-uuwUg~ z*v$Anbdz|EeH6JYCFqB~gV!ieyZH<@US4b;+P;Xkpe;y=d+^I+#fgNz&1EU^9j#__ zJhbUlG0oBA_jAAU3)nW6E@5jd$Ol`V$j3iVffi56bpG8DWKcj#&>=4-H(tJQBQdj- z_%87-pl^~B=M$%id1CvRo(Y%Zex$kkt|mQ*W@Ans?eq&LL9t#i(#4WVN283cE?Jq-dTuW?VhwW6ap_jR*(Wu!Q%1=;Unw+Q*zt4)Z3#XdMfVDBn?LXGgAl6nH(PryGZ-U(LQ+*nIF zj$A{o!&7l@CwG(Ikq4=aDyg2DsU7FGd^AWKaR#&xdu(Rt3VIyk{CaHsd{TH?cuRr< zrxI)^k~ky-sZXYUl=^k?sl`_o|I;8hs0=zovB6}p8EOqT8E!F}OnKI4z7Kp~|9#{4 zq3_$iXRE+s4Gh34wPZJHa0B@*YH%-kfILDosD>6(8!e+Q{DM#o4bg5oNEg%PI2Q3N zS{cMI_B}1UCLt23Bm*_Dp$6xtKA-wU@d>EGSB5Nu(vWW`CszK5B)u=)9_pRTD zQ3J}JKNDkfZ$e;2=3e>be|m{t6Fnu$)~NYk)Dta=9-a8~4saJC?*Pf*RkH7k{a@{y z+<(@7#lBPCs9`iI;xYV>^$8)zAYDogKol0(C4%;PX%&4OzvQx?endZ^pV0%@shc9? zq17&-3QtK~EKK7b{0Y+t^c;@SC4Sl}?3bt{IBg=aNMI)=P6<%p!GHK>0lrx%SuB~x zHxle*aX@baq>{SuWXeKPgwvblv;-@*D(q!aQ`S2!;-)5Af#11k!#uMaPYfC(<76RO zOjhA3;cM~Rg`3F{(5aK~1isVp#M1M~Wq4NPZah`-dSbvjwvpCR2kpSK+U^CSdkDJw zG}%j@!&7-)$FpkQBJUzf@geyqtnjzw-xSB7X*SN;DX9v!yO@;0Z`9IDsge3=HTBRi zt;88a8>BPHw9BL@AynQP!}7)$p+3;&3x$UTN9n7&M2B!3|8Vl zkWa}+umE3@Pp}I41x=$VREqJUCST(?dlQ~sT1##r&3K+;7$d2RJcxPw{iK&Xj8l-0 zz)nAj{r*po1>`B%!$ zHjw>fJzmvns`(zvD{Kvo&A5A{PZ%2MbP9fis)5yQ@l#~PH4DvnM zNoMgZ@gK-Jl#)wm2EiT=@=Mswi)lJEE|dI<%E>iUL#`z^P#w9E7LwmkJ$5zSij(qD zaxQ)luNU~Mj}Fm(AbcOGq75`e02K*f^h)|VeS#j2 z7{oSu1N{TtO&8Kj=(+SFdLgXGwe(T?8oh#^PFK?R==t<+dJH|19z{>2Pt!Z-ujzI4 zWO@p{3F|fw(evod^jGwL`g{5?{SzV@m(%O%0=k4AL6^Y_JwxB6C(y^}8+0{2lb#K$ zZ~;TGDbQ67&{)XN{@1c*-Khx*wTl6r@UVcsU$Q4-qyAsd6y^iL?3d#_f zOsA(23$fBtVj^Z(oHz07c5lPhyg*;1f1xkocab);-&KMIe1JYk@1wt?6LgY};pf1% z!yX+^pQV4pc>OC~LwC|&(97uUbR)eQzln4sy_Mce_t58HInSck(DC2V*r~Ut`~m|H zqw8B{l~dAjeHoH?O)*|Bi(d=CC2Ylr<&i+EpNe0n5D)rNq^A-ct&LwxiIa}RuVti- zo)EuIBW3jQ_;osP!DsR74AKHDEAq(%)*6prXOVj0;`ntoHQ_|Jm?wt>Bwxp`3rJxK z+l`32!H8w0gyYwE=5fk!{5o~M{umosDGM@L2(SP?}U`1eRhW=iZ6$5;=x(y$YqT$G3;0wl37#!=KW_ ze!+DBqr7g8M}kB0{S~l6$sFd}n8Q%C4Q~OHTA~ICP6g10ZFm8tpjZntdIe1+Jt|r&}P;vU69H$l-kE@z~s3Dau>g0cQVOu z#Or^`*@pbA4Xcoo_4f+gw*fw7p1J}xF+l1&@cQGDSS{!8-x|N)HKzrl4s1t{Zbwbo zZ|WxMKd%d`aM#u(onV?E>d?V=Z#bl#^BlGy|2EW<^##*eQI=tzpJ{0XS1XZcJC`Su zkeFjV-j|_trd?t?x8a(#>2Ty>dd>1iAn9Jb4{+%+In8tKJ&1!7-yNzKm|r83;=(uc zH9NT6L~Ul^v1h=KY4p-~A8*HJRu2PctlSX1C%fK?(PfyEQ(~-bL2KAZ-i+_JpiI^? z>+#M;=PF2e033S%Ki>79w0aD3O4yGvTrKByW_KLUb$C6jX&fu#!~&Za{pb(qo4~sP zJi>pcau1#cpQwaYR%1O>3k*~M^rHtJV&{5pCMiJ5QtWGJ!irNCaL+WbMl-PLJsi>8 z2xOCrS>F;^yf5JQ4aA5&)M>!5nKX-G8TfGvc^c0bkOKiMCAZ>1)_IWLZ-JDon95T*FxZ`um0@)2#L_?=U-nYN?Hw$Ly+jCRmY=zTZ& z7ww|mm|L;u{lAKSJp!X>8|?)K=|@Ci01=r%G5|a>Oe4UIgJg(~&{4X897)GuF?Rq7 z9Yct1$1`@M$=~)38V1VQ*&ZWG>f%@AO3Yr1F=sLXH$Dc$dnfQ>3vl8S zz>f;f_#1JC#i;Jep= z?-(9_9kGbF$$sFXqk)f(1==|tSm#7~5=Td;($j#S&H(D!2~=@55XU*dSLXpuT|jpM zWnDxs2I9IDSm-ifrYnG zyBnD4UXF6^=XmEK@;S#~j{wR10r>4PV4Wv`_MQa7c^W@t_$OeoXMu4T%9=+^3@^O~ zEcga+(VIYDZv!Fj2R?ikSm{0bKK(mRM}0s)#0=+S%yB-&Ea!9j1^tqKMgK{^rr*$S z>Hlf%Y=ESyvIG2X_w@Ve;op%mmI|`!A`)t*X9gIGAk))5D+Iwpz}?YR=ci!?=ErJg zU?eUDRI;@a7SS@wA|klx8j1j+gt!z-HbQN&#*Ee$Wk%w%WQsCmLs==7Wh1&f`ObOw z&GZ1eRwe!F-h1x3=bm@&xj*l|dro)%j`yBV%@y;R`P}^8{DEH$a`pWYHvxER#$6USyL0{OQSSn^I4nLO3G>c^k7-OepRlqqpcyR zZ|v$+$d;vTTH#hVLux>?O7vjgQ`)>h4iRYR7kRPq}GBt`c%)aPS!=6Z81>6}8+@~Q4TmD8Q4yh8@% zX?K2zonL^hsElXZmknRg+||CUKHt^WTHn>_EeN@~3q$&a1@x6u-3Jw`O!51cv@Bbd zbr%g%rOK+^#j1|si-#~?Y{gw1ikn!RZ)soVKBNqbA8KsLH+HoxZOwHD4>h&q^0}2Q zE8T}w_r>2DEOfu6D7Q|lCF=&X;?@^-LDs9ccdNHu&^Sohq{6y6O_I5R0xxIl8vW>!4oB%4^*A5ZhjWt(Y3m(FYwvRMio3 zck>~AzJR`Rn!8f5whyY@&cV`DS=#Lib>B6FVV4!R%j&*Mb-!8}6|WvtZ?Jkm_p3wQ zKRHx!cI0zGD1KW<(bUw6Z#EOJ%jLE~S-KS`R+D z3L%fCwIMC7Ojk^)Z|w}_$b_pB3h$dvrAkZX-nL%4+~Uix!k1Zm*-(5&??Zd+{W7-Q>55`)S7DpdR>$dZ zB`z(qx~*2(YtvHD8;qh;(P~ zmyXJ{9_6-vm8Hu@W6|C}ZRM@9eOzV7MmlPr$_*czrmQ|wR=+itU&@ZrR3t}KZu!@^FThR@Ou#%8vMQn^ssZwYF8Y zk!-?`pJ!uzrqeZ-S#GI+T86>c%En$ah*qMHlO$?_;z>lyajdNO658PlH zfZ2Zn=gczTX43)8J`uRfbP=;x1k-K41H6{g9m9@rJuoL`#Ow%(pTj!vp)p>d-Epn zA@dgSkNGp6xa<&tkD8wVvqJ=Cj|j{z5tw};B)32=SjnO1G|a$s)@YW7^et5>Y1Lv(8#aMNqUn_gD~ao zh?frD=_c}SR>a#yiuc&G6VKGnTjb1b&Nbwn`K|Sx?YynWSLE{T&Mb{HZT0yTP{%kK zH09@;@ZRuSRb%LP(#w#0HpjBBm9gzMekmc*y{T3vBcg)XQI=s3oa&}N}Tcz zCq8jExu1J85*xiM!FaDbK`#tns&day*6KAoIdRA}L32}*&-3eB;WZ_B)iwMkXrj`= zbV~98uGz`DWRqSM$-%2V8C@Ha-CTnu1yJ#^;?I*a;IPMujhZD=jyf~?4DZ|cqK6;# z(~U9_?lKx8e6`P)QAXVRFLU({Qug=#G2ZeI27JV-dd(-|8WzC!goTbsis%1zK7;p$ z;=*+Z6xDx0B!z;zM^ybiz(X|B-xqR|`$|jXjw)}7TBAdpw6G+)PyYddB90OEdg&ld z1lfCCh;(*6Uj02tE|Q2u$Sa=+MnAT_{Rakke1+bPY87x)KK&;mecSsC(f432>P7c` z9Qg8A>C2Gg)$stIBeYUlD*9H9l1hsue}W!B-!dAaR$k3#Xx@&np~x@GUH5eBnhFDb zeoe_h&qzuC+OS^-sic+HkzD)*qE9p%Y9qw8DdTs@+!~s@nx2tt^%#~6?b)lTC2#v; zrY(yk8Z5B|eG*fio*S%Jk`#M7i%>0|m$BF7xJIkowkU7F*`DXG~{|6eetCFZ&H znBu~cuvUcY(2~$?j=tz*ye)ZeA{u>EQgJI*utH2%!KX;v<6XuZ!ot?&@Oia(^>j|)tVuzjHn zL)ySR=rMbh$9{#63yhb`W14H0S;|=&AJeAr(~5siF}oCFTVrd>{gOy1_1YBow92E} zp#38AUS?OX?SVsz`NsljlcrJSY1Z1&T|eyq(AIuKY4>Wq{TgeJp&9vz z6pmUU`roe-AJSGI7nl(}mAz&I`${?03@x+W=E;H6#r1lsC?|??j`*yeAil`{Fg!Jk z&I{$Na3A|WvG%?x`#n7ilygA#b$Zelp6{LGe$)Qn@Au;`z2k6BfTeG~t#LR$!wu>^Bgj15=LhdC9-*1Q5+a~oFP4n4sQ z&u0hEW91xHPG7?lS2<~AC#@%}(aEaVk>xy9&Qj$ZH9SEL&q`(AJ1UxGI`*TnqrJ}) zrNo47S*OAl&Y~YEW|xkEXf8>ua7;xrBODcCg)>+M#9L_@r}@TAfVaSp5$|O5#enPZ z(U=HC-tZraKzA{A?-5Ao!9i!CK1$!i!)*d5^F^G4mtftH-FFH0ixPIQCDa;A?Qw*= z36sc=aK6t9znSn1VH@0DB5WtTOxQtqg}_{QUM2jHu$%B2VXETu@mMDGKf)OfE#^uNDd zL!3^D`=yF=yz|ui1U{DtS2Rd@%y*q4pMMYYhXHQ^9%J*jk=_A*5_ov{t)x@Q&je08 z3I8ui?*lg<+#=FCT7^Yu9M+y^u-R>MUg3Rq7oKlZ@r(KgY--22W?~8Y6*jfuW(;1x zCA`&U%ZQdPd z`YXV}6mJsrrJl?uc5&WJFYVn&I^SCazRp9lUkEStT7%_YJ{Sc}0SwP>@M}GKq7e2r zdRyT6g0~&|Liknh4e0mD>gB!Z9r1qZ{p=d}UGEpr=sTEq#yjVpcR%ue4SfL|jP?E- z{+GSax$h9zkGbc4-)EihOZ=OI<^Fj8t}lTn`ze3AKg-WRQwY!XAArvi{}J$oaD(3r zzTNKxUkI=9*ZJS|H~Bno{4avH`a7t*+us9CA$-6;jGRaPx4{>}C;gsad9cbq1-=kI z?Vt5O_AmGs{r~a*FYq6NBKLfdls@$f;9zWU3-}4aMA2;lJSC_AKO>keeO&<82J?c2 zT;Bx$MetL>2I_t<*bL1V!H)(_!E(1RcmkRNn3;Anb64JNCh&YQI^zOnPny%BEVJSU zr^nkhbh;)D0Ete`Rno3W&y z!^7ZXu2y{QE@92SjDOu4j{0I)j)yZh*W)Y6e8yAt>&|AreK*))8lFBy> z`R-&DxQpvXT^+{qOuQRRv__;^DYDqVeO8M5sV6=qBk+4_$C514$B=$K?b*N;)3swH zo&!JNi7G3Ii-mnB^sIa=FUqU|)= zPD4B8-)N+r!?m4bbR~-8k9h;${jwg#wavq{&7)}ZBs~A`;S}yhU6saa`$r*Zg|@$# zRdOsc-O4qFwQn5p?ObE%m9HW9om?YSJ0+?e@qf4*jA$pJ{h7c%UIP!&QH(x&n0OT) zkFf%cj8=`fs*#{T7sFH+9=h0%#t(2A`~nUEALjDaA60b3P6D6Qf5Fc~bmU`)KT9nA zA6LD^RWEV;R4+pL8?HFx;Sw~zI|)sovZ9``4xkhoQ1xq@lO;ZZ#2@k5--yDM;dQdn$HwIsquFd zb5i5A8n0ITUrWsYdJ5;)6!SLYL zYmQ-l3&;Dz}A-Z&5tYG)muAZv0&*cig3z1xnwj z^kEP3q@{E_Zy}~%wP8AyexcxF3bTp`&2p_(tEI~Yqu+}$c>aj49#;ILikI($7<~AC z1AeUFnX7^cWgekYZBYCM!L!fSSn&Oq6u(6+XS!Y zI>tGx_)ALvUy9dvRcdqMz^ZqfwNTWlg`!R^6g!+@JR27Bq-tP=Th5iolYI@I+Z!CO z5qq-pd}CwV6V5@6f6>(1vdr1m+R@nR?9}-6j(k%)&jgv_MKVk9iz-q%F7o^k`Fm_$ zo)3~g80PWx3iEH+w1?8rvT;f+8)vnC%=w5BFPvhmn5r)z;Ugcz9VHGlmT^A>0#2 zvKybYb$Ee=yzb!6oA3pbx+1yo-Ast7x4xwEtq6nvn8Axo>`!6&bCm4l=`J!(=jpeH zCy}iAhISZfn@V+z^rwWk2`30KXhk-8Uj8RB{@Ls!M9#RW!4EGBj$d++cWE;df4u~L zZZXd2M33v$R=G*-a;Nb~Dk*-m39IjZbRaD>2_t5E>ah)A6o;lqdWiTKB$@J`k*oFy8oy(dz1^ i&SryFS1)VsBoRH0B4&)RHZ_q-tr*^(%-z^g$N68ESKami literal 0 HcmV?d00001 diff --git a/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj b/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj index ba45bbbc2e..0d182678ef 100644 --- a/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj +++ b/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj @@ -8,7 +8,7 @@ - + diff --git a/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj index ea91b8c196..86a680fac5 100644 --- a/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj +++ b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj @@ -9,7 +9,7 @@ - + diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index a748f6cf00..24ecb21d18 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs @@ -17,6 +17,8 @@ namespace Avalonia.Skia.UnitTests.Media new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); private readonly Typeface _arabicTypeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans Arabic"); + private readonly Typeface _hebrewTypeface = + new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans Hebrew"); private readonly Typeface _italicTypeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans", FontStyle.Italic); private readonly Typeface _emojiTypeface = @@ -24,7 +26,7 @@ namespace Avalonia.Skia.UnitTests.Media public CustomFontManagerImpl() { - _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _arabicTypeface, _defaultTypeface }; + _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _arabicTypeface, _hebrewTypeface, _defaultTypeface }; _defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName; } From 467ef300bf559123f3dc355185ad78efdf0675ef Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 3 Feb 2023 10:43:26 +0100 Subject: [PATCH 051/185] Remove unneeded method. --- src/Avalonia.Base/PropertyStore/ValueStore.cs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index f36a96992b..a758360545 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -380,23 +380,6 @@ namespace Avalonia.PropertyStore } } - /// - /// Called by non-LocalValue binding entries to re-evaluate the effective value when the - /// binding produces an unset value. - /// - /// The bound property. - /// The priority of binding which produced a new value. - public void OnBindingValueCleared(AvaloniaProperty property, BindingPriority priority) - { - Debug.Assert(priority != BindingPriority.LocalValue); - - if (TryGetEffectiveValue(property, out var existing)) - { - if (priority <= existing.Priority) - ReevaluateEffectiveValue(property, existing); - } - } - /// /// Called by a when its /// state changes. From 42f8c58aef5930cb5ec3def93c981c39beab5143 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 3 Feb 2023 10:45:53 +0100 Subject: [PATCH 052/185] Update teste for the new typeface --- .../Media/TextFormatting/TextCharacters.cs | 21 +++++-------------- .../Media/CustomFontManagerImpl.cs | 6 ++++++ .../Media/TextFormatting/TextLayoutTests.cs | 14 ++++++------- .../Media/TextFormatting/TextLineTests.cs | 14 ++++++------- 4 files changed, 25 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 82cf3297fd..c9dafaced7 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -82,24 +82,15 @@ namespace Avalonia.Media.TextFormatting var previousGlyphTypeface = previousProperties?.CachedGlyphTypeface; var textSpan = text.Span; - if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count, out var script)) + if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count)) { - if (script == Script.Common && previousGlyphTypeface is not null) - { - if (TryGetShapeableLength(textSpan, previousGlyphTypeface, null, out var fallbackCount, out _)) - { - return new UnshapedTextRun(text.Slice(0, fallbackCount), - defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel); - } - } - return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(defaultTypeface), biDiLevel); } if (previousGlyphTypeface is not null) { - if (TryGetShapeableLength(textSpan, previousGlyphTypeface, defaultGlyphTypeface, out count, out _)) + if (TryGetShapeableLength(textSpan, previousGlyphTypeface, defaultGlyphTypeface, out count)) { return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel); @@ -130,7 +121,7 @@ namespace Avalonia.Media.TextFormatting var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface); - if (matchFound && TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count, out _)) + if (matchFound && TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count)) { //Fallback found return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface), @@ -160,17 +151,15 @@ namespace Avalonia.Media.TextFormatting /// The typeface that is used to find matching characters. /// The default typeface. /// The shapeable length. - /// /// internal static bool TryGetShapeableLength( ReadOnlySpan text, IGlyphTypeface glyphTypeface, IGlyphTypeface? defaultGlyphTypeface, - out int length, - out Script script) + out int length) { length = 0; - script = Script.Unknown; + var script = Script.Unknown; if (text.IsEmpty) { diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index 24ecb21d18..5a6d7f2cdf 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs @@ -90,6 +90,12 @@ namespace Avalonia.Skia.UnitTests.Media skTypeface = typefaceCollection.Get(typeface); break; } + case "Noto Sans Hebrew": + { + var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_hebrewTypeface.FontFamily); + skTypeface = typefaceCollection.Get(typeface); + break; + } case FontFamily.DefaultFontFamilyName: case "Noto Mono": { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 3735e9f6d7..9a7460c218 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -724,7 +724,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var selectedRect = rects[0]; - Assert.Equal(selectedText.Bounds.Width, selectedRect.Width); + Assert.Equal(selectedText.Bounds.Width, selectedRect.Width, 2); } } @@ -885,7 +885,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var distance = hitRange.First().Left; - Assert.Equal(currentX, distance); + Assert.Equal(currentX, distance, 2); currentX += advance; } @@ -915,7 +915,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var distance = hitRange.First().Left + 0.5; - Assert.Equal(currentX, distance); + Assert.Equal(currentX, distance, 2); currentX += advance; } @@ -1048,8 +1048,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting [InlineData("012🧐210", 2, 4, FlowDirection.LeftToRight, "14.40234375,40.8046875")] [InlineData("210🧐012", 2, 4, FlowDirection.RightToLeft, "0,7.201171875;21.603515625,33.603515625;48.005859375,55.20703125")] - [InlineData("שנב🧐שנב", 2, 4, FlowDirection.LeftToRight, "11.63671875,39.779296875")] - [InlineData("שנב🧐שנב", 2, 4, FlowDirection.RightToLeft, "11.63671875,39.779296875")] + [InlineData("שנב🧐שנב", 2, 4, FlowDirection.LeftToRight, "11.268,38.208")] + [InlineData("שנב🧐שנב", 2, 4, FlowDirection.RightToLeft, "11.268,38.208")] [Theory] public void Should_HitTextTextRangeBetweenRuns(string text, int start, int length, FlowDirection flowDirection, string expected) @@ -1080,9 +1080,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var expectedRect = expectedRects[i]; - Assert.Equal(expectedRect.Left, rects[i].Left); + Assert.Equal(expectedRect.Left, rects[i].Left, 2); - Assert.Equal(expectedRect.Right, rects[i].Right); + Assert.Equal(expectedRect.Right, rects[i].Right, 2); } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index e47542af7a..70e74cdf83 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -658,7 +658,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(TextTestHelper.GetStartCharIndex(run.Text), bounds.TextSourceCharacterIndex); Assert.Equal(run, bounds.TextRun); - Assert.Equal(run.Size.Width, bounds.Rectangle.Width); + Assert.Equal(run.Size.Width, bounds.Rectangle.Width, 2); } for (var i = 0; i < textBounds.Count; i++) @@ -667,19 +667,19 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting if (lastBounds != null) { - Assert.Equal(lastBounds.Rectangle.Right, currentBounds.Rectangle.Left); + Assert.Equal(lastBounds.Rectangle.Right, currentBounds.Rectangle.Left, 2); } var sumOfRunWidth = currentBounds.TextRunBounds.Sum(x => x.Rectangle.Width); - Assert.Equal(sumOfRunWidth, currentBounds.Rectangle.Width); + Assert.Equal(sumOfRunWidth, currentBounds.Rectangle.Width, 2); lastBounds = currentBounds; } var sumOfBoundsWidth = textBounds.Sum(x => x.Rectangle.Width); - Assert.Equal(lineWidth, sumOfBoundsWidth); + Assert.Equal(lineWidth, sumOfBoundsWidth, 2); } } @@ -959,14 +959,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(secondRun.Size.Width, textBounds[1].Rectangle.Width); Assert.Equal(7.201171875, textBounds[0].Rectangle.Width); - Assert.Equal(textLine.Start + 7.201171875, textBounds[0].Rectangle.Right); - Assert.Equal(textLine.Start + firstRun.Size.Width, textBounds[1].Rectangle.Left); + Assert.Equal(textLine.Start + 7.201171875, textBounds[0].Rectangle.Right, 2); + Assert.Equal(textLine.Start + firstRun.Size.Width, textBounds[1].Rectangle.Left, 2); textBounds = textLine.GetTextBounds(0, text.Length); Assert.Equal(2, textBounds.Count); Assert.Equal(7, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length))); - Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); + Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width), 2); } } From 73cede0bf5f36c72ae918e758858b5b815c11bc3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 3 Feb 2023 13:24:12 +0100 Subject: [PATCH 053/185] Added integration tests for window transparency. Failing on macOS, passing on win32. --- samples/IntegrationTestApp/MainWindow.axaml | 54 ++++++----- .../IntegrationTestApp/MainWindow.axaml.cs | 91 +++++++++++++++++++ .../Avalonia.IntegrationTests.Appium.csproj | 1 + .../WindowTests.cs | 45 ++++++++- 4 files changed, 166 insertions(+), 25 deletions(-) diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 54c0cb0655..b116e4c789 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -120,30 +120,36 @@ - - - - NonOwned - Owned - Modal - - - Manual - CenterScreen - CenterOwner - - - Normal - Minimized - Maximized - FullScreen - - - - - - - + + + + + NonOwned + Owned + Modal + + + Manual + CenterScreen + CenterOwner + + + Normal + Minimized + Maximized + FullScreen + + + + + + + + + + + + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 841947673a..3cd5350cce 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -7,9 +7,13 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Media; using Avalonia.Markup.Xaml; using Avalonia.VisualTree; using Microsoft.CodeAnalysis; +using Avalonia.Controls.Primitives; +using Avalonia.Threading; +using Avalonia.Controls.Primitives.PopupPositioning; namespace IntegrationTestApp { @@ -103,6 +107,89 @@ namespace IntegrationTestApp } } + private void ShowTransparentWindow() + { + // Show a background window to make sure the color behind the transparent window is + // a known color (green). + var backgroundWindow = new Window + { + Title = "Transparent Window Background", + Name = "TransparentWindowBackground", + Width = 300, + Height = 300, + Background = Brushes.Green, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + }; + + // This is the transparent window with a red circle. + var window = new Window + { + Title = "Transparent Window", + Name = "TransparentWindow", + SystemDecorations = SystemDecorations.None, + Background = Brushes.Transparent, + TransparencyLevelHint = WindowTransparencyLevel.Transparent, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + Width = 200, + Height = 200, + Content = new Border + { + Background = Brushes.Red, + CornerRadius = new CornerRadius(100), + } + }; + + window.PointerPressed += (_, _) => + { + window.Close(); + backgroundWindow.Close(); + }; + + backgroundWindow.Show(this); + window.Show(backgroundWindow); + } + + private void ShowTransparentPopup() + { + var popup = new Popup + { + WindowManagerAddShadowHint = false, + PlacementMode = PlacementMode.AnchorAndGravity, + PlacementAnchor = PopupAnchor.Top, + PlacementGravity = PopupGravity.Bottom, + Width= 200, + Height= 200, + Child = new Border + { + Background = Brushes.Red, + CornerRadius = new CornerRadius(100), + } + }; + + // Show a background window to make sure the color behind the transparent window is + // a known color (green). + var backgroundWindow = new Window + { + Title = "Transparent Popup Background", + Name = "TransparentPopupBackground", + Width = 200, + Height = 200, + Background = Brushes.Green, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + Content = new Border + { + Name = "PopupContainer", + Child = popup, + [AutomationProperties.AccessibilityViewProperty] = AccessibilityView.Content, + } + }; + + backgroundWindow.PointerPressed += (_, _) => backgroundWindow.Close(); + backgroundWindow.Show(this); + + popup.Open(); + } + private void SendToBack() { var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!; @@ -175,6 +262,10 @@ namespace IntegrationTestApp this.Get("BasicListBox").SelectedIndex = -1; if (source?.Name == "MenuClickedMenuItemReset") this.Get("ClickedMenuItem").Text = "None"; + if (source?.Name == "ShowTransparentWindow") + ShowTransparentWindow(); + if (source?.Name == "ShowTransparentPopup") + ShowTransparentPopup(); if (source?.Name == "ShowWindow") ShowWindow(); if (source?.Name == "SendToBack") diff --git a/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj b/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj index 57338a1e08..3ff91139f1 100644 --- a/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj +++ b/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj @@ -16,4 +16,5 @@ + diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index 4d833cdb1f..3b74ed314b 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -1,11 +1,14 @@ using System; +using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading; using Avalonia.Controls; +using Avalonia.Media.Imaging; using OpenQA.Selenium; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Interactions; +using SixLabors.ImageSharp.PixelFormats; using Xunit; using Xunit.Sdk; @@ -141,7 +144,6 @@ namespace Avalonia.IntegrationTests.Appium } } - [Theory] [InlineData(ShowWindowMode.NonOwned)] [InlineData(ShowWindowMode.Owned)] @@ -187,6 +189,47 @@ namespace Avalonia.IntegrationTests.Appium } } + [Fact] + public void TransparentWindow() + { + var showTransparentWindow = _session.FindElementByAccessibilityId("ShowTransparentWindow"); + showTransparentWindow.Click(); + Thread.Sleep(1000); + + var window = _session.FindElementByAccessibilityId("TransparentWindow"); + var screenshot = window.GetScreenshot(); + + window.Click(); + + var img = SixLabors.ImageSharp.Image.Load(screenshot.AsByteArray); + var topLeftColor = img[1, 1]; + var centerColor = img[img.Width / 2, img.Height / 2]; + + Assert.Equal(new Rgba32(0, 128, 0), topLeftColor); + Assert.Equal(new Rgba32(255, 0, 0), centerColor); + } + + [Fact] + public void TransparentPopup() + { + var showTransparentWindow = _session.FindElementByAccessibilityId("ShowTransparentPopup"); + showTransparentWindow.Click(); + Thread.Sleep(1000); + + var window = _session.FindElementByAccessibilityId("TransparentPopupBackground"); + var container = window.FindElementByAccessibilityId("PopupContainer"); + var screenshot = container.GetScreenshot(); + + window.Click(); + + var img = SixLabors.ImageSharp.Image.Load(screenshot.AsByteArray); + var topLeftColor = img[1, 1]; + var centerColor = img[img.Width / 2, img.Height / 2]; + + Assert.Equal(new Rgba32(0, 128, 0), topLeftColor); + Assert.Equal(new Rgba32(255, 0, 0), centerColor); + } + public static TheoryData StartupLocationData() { var sizes = new Size?[] { null, new Size(400, 300) }; From 6210018f3b5e98063252d2bf2933e1739ed17f85 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 3 Feb 2023 14:10:16 +0100 Subject: [PATCH 054/185] Propertly handle an InlineUIContainer's logical children --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 2 +- .../Utils/TreeHelper.cs | 2 +- .../AutoCompleteBox/AutoCompleteBox.cs | 2 +- src/Avalonia.Controls/Control.cs | 4 ---- .../Documents/InlineUIContainer.cs | 18 ++++++++++++++++++ .../Platform/DefaultMenuInteractionHandler.cs | 2 +- .../Primitives/OverlayPopupHost.cs | 2 +- src/Avalonia.Controls/Primitives/Popup.cs | 2 +- src/Avalonia.Controls/Primitives/PopupRoot.cs | 4 ++-- src/Avalonia.Controls/ProgressBar.cs | 2 +- src/Avalonia.Controls/TextBlock.cs | 4 ---- 11 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index f35124ee0a..91b65a1f72 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -3979,7 +3979,7 @@ namespace Avalonia.Controls { if (focusedObject is Control element) { - parent = element.Parent; + parent = element.VisualParent; if (parent != null) { dataGridWillReceiveRoutedEvent = false; diff --git a/src/Avalonia.Controls.DataGrid/Utils/TreeHelper.cs b/src/Avalonia.Controls.DataGrid/Utils/TreeHelper.cs index f4ba644ae6..6aebf05d6b 100644 --- a/src/Avalonia.Controls.DataGrid/Utils/TreeHelper.cs +++ b/src/Avalonia.Controls.DataGrid/Utils/TreeHelper.cs @@ -36,7 +36,7 @@ namespace Avalonia.Controls.Utils { if (child is Control childElement) { - parent = childElement.Parent; + parent = childElement.VisualParent; } } child = parent; diff --git a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs index 98885e11ca..55649660f7 100644 --- a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs @@ -792,7 +792,7 @@ namespace Avalonia.Controls Control? element = focused as Control; if (element != null) { - parent = element.Parent; + parent = element.VisualParent; } } focused = parent; diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index ed24c3c7c2..ab7c9948c4 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -2,14 +2,12 @@ using System; using System.Collections.Generic; using System.ComponentModel; using Avalonia.Automation.Peers; -using Avalonia.Controls.Documents; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.LogicalTree; -using Avalonia.Media; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Threading; @@ -211,8 +209,6 @@ namespace Avalonia.Controls remove => RemoveHandler(SizeChangedEvent, value); } - public new Control? Parent => (Control?)base.Parent; - /// bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; diff --git a/src/Avalonia.Controls/Documents/InlineUIContainer.cs b/src/Avalonia.Controls/Documents/InlineUIContainer.cs index 58afb24b5c..f06c8515ee 100644 --- a/src/Avalonia.Controls/Documents/InlineUIContainer.cs +++ b/src/Avalonia.Controls/Documents/InlineUIContainer.cs @@ -64,5 +64,23 @@ namespace Avalonia.Controls.Documents internal override void AppendText(StringBuilder stringBuilder) { } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ChildProperty) + { + if(change.OldValue is Control oldChild) + { + LogicalChildren.Remove(oldChild); + } + + if(change.NewValue is Control newChild) + { + LogicalChildren.Add(newChild); + } + } + } } } diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index de3aca76d9..4dd868253e 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -553,7 +553,7 @@ namespace Avalonia.Controls.Platform } } - protected static IMenuItem? GetMenuItem(Control? item) + protected static IMenuItem? GetMenuItem(StyledElement? item) { while (true) { diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index e265f4eb6a..d466edeb33 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -48,7 +48,7 @@ namespace Avalonia.Controls.Primitives set { /* Not currently supported in overlay popups */ } } - protected internal override Interactive? InteractiveParent => Parent; + protected internal override Interactive? InteractiveParent => (Interactive?)VisualParent; public void Dispose() => Hide(); diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index c85199a665..3b68cd2ae8 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -723,7 +723,7 @@ namespace Avalonia.Controls.Primitives while (e is object && (!e.Focusable || !e.IsEffectivelyEnabled || !e.IsVisible)) { - e = e.Parent; + e = e.VisualParent as Control; } if (e is object) diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 57ec864cad..b3436d4176 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -72,12 +72,12 @@ namespace Avalonia.Controls.Primitives /// /// Popup events are passed to their parent window. This facilitates this. /// - protected internal override Interactive? InteractiveParent => Parent; + protected internal override Interactive? InteractiveParent => (Interactive?)Parent; /// /// Gets the control that is hosting the popup root. /// - Visual? IHostedVisualTreeRoot.Host => Parent; + Visual? IHostedVisualTreeRoot.Host => VisualParent; /// /// Gets the styling parent of the popup root. diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 7e0d695264..ce3158b282 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -231,7 +231,7 @@ namespace Avalonia.Controls private void UpdateIndicator() { // Gets the size of the parent indicator container - var barSize = _indicator?.Parent?.Bounds.Size ?? Bounds.Size; + var barSize = _indicator?.VisualParent?.Bounds.Size ?? Bounds.Size; if (_indicator != null) { diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index df9a3eb8f3..ec31470126 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -673,8 +673,6 @@ namespace Avalonia.Controls controlRun.Control is Control control) { VisualChildren.Remove(control); - - LogicalChildren.Remove(control); } } } @@ -693,8 +691,6 @@ namespace Avalonia.Controls { VisualChildren.Add(control); - LogicalChildren.Add(control); - control.Measure(Size.Infinity); } } From 93396ea6d870aa7eb58e525aaf9b4f127d04507f Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 3 Feb 2023 15:20:51 +0100 Subject: [PATCH 055/185] perf: Animatable small adjust - delegate caching - try to partially avoid changing the Transitions property during subscribe/unsubscribe operations --- src/Avalonia.Base/Animation/Animatable.cs | 32 ++++++++++++++--------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Base/Animation/Animatable.cs b/src/Avalonia.Base/Animation/Animatable.cs index eddb89c3e8..50d9a48e9e 100644 --- a/src/Avalonia.Base/Animation/Animatable.cs +++ b/src/Avalonia.Base/Animation/Animatable.cs @@ -29,6 +29,12 @@ namespace Avalonia.Animation private bool _transitionsEnabled = true; private bool _isSubscribedToTransitionsCollection = false; private Dictionary? _transitionState; + readonly NotifyCollectionChangedEventHandler _collectionChanged; + + public Animatable() + { + _collectionChanged = TransitionsCollectionChanged; + } /// /// Gets or sets the clock which controls the animations on the control. @@ -61,14 +67,14 @@ namespace Avalonia.Animation { _transitionsEnabled = true; - if (Transitions is object) + if (Transitions is Transitions transitions) { if (!_isSubscribedToTransitionsCollection) { _isSubscribedToTransitionsCollection = true; - Transitions.CollectionChanged += TransitionsCollectionChanged; + transitions.CollectionChanged += _collectionChanged; } - AddTransitions(Transitions); + AddTransitions(transitions); } } } @@ -86,14 +92,14 @@ namespace Avalonia.Animation { _transitionsEnabled = false; - if (Transitions is object) + if (Transitions is Transitions transitions) { if (_isSubscribedToTransitionsCollection) { _isSubscribedToTransitionsCollection = false; - Transitions.CollectionChanged -= TransitionsCollectionChanged; + transitions.CollectionChanged -= _collectionChanged; } - RemoveTransitions(Transitions); + RemoveTransitions(transitions); } } } @@ -120,7 +126,7 @@ namespace Avalonia.Animation toAdd = newTransitions.Except(oldTransitions).ToList(); } - newTransitions.CollectionChanged += TransitionsCollectionChanged; + newTransitions.CollectionChanged += _collectionChanged; _isSubscribedToTransitionsCollection = true; AddTransitions(toAdd); } @@ -134,19 +140,19 @@ namespace Avalonia.Animation toRemove = oldTransitions.Except(newTransitions).ToList(); } - oldTransitions.CollectionChanged -= TransitionsCollectionChanged; + oldTransitions.CollectionChanged -= _collectionChanged; RemoveTransitions(toRemove); } } else if (_transitionsEnabled && - Transitions is object && + Transitions is Transitions transitions && _transitionState is object && !change.Property.IsDirect && change.Priority > BindingPriority.Animation) { - for (var i = Transitions.Count -1; i >= 0; --i) + for (var i = transitions.Count - 1; i >= 0; --i) { - var transition = Transitions[i]; + var transition = transitions[i]; if (transition.Property == change.Property && _transitionState.TryGetValue(transition, out var state)) @@ -166,11 +172,11 @@ namespace Avalonia.Animation { oldValue = animatedValue; } - + var clock = Clock ?? AvaloniaLocator.Current.GetRequiredService(); state.Instance?.Dispose(); state.Instance = transition.Apply( this, - Clock ?? AvaloniaLocator.Current.GetRequiredService(), + clock, oldValue, newValue); return; From eb1aa547a88a25813ec6dfa68cb336dff9d973b2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 3 Feb 2023 15:22:57 +0100 Subject: [PATCH 056/185] Fix default transparency level. The macOS backend sets up a window with a transparency level of `None` but the managed code thinks it's `Transparent`, meaning that making the window transparent did nothing as it thinks it's already set. Fixes #8419 --- src/Avalonia.Native/WindowImplBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 1f290acd86..50bee0d395 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -501,7 +501,7 @@ namespace Avalonia.Native } } - public WindowTransparencyLevel TransparencyLevel { get; private set; } = WindowTransparencyLevel.Transparent; + public WindowTransparencyLevel TransparencyLevel { get; private set; } = WindowTransparencyLevel.None; public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { From b1ca75a30b9cda3f91dbd9af4c2d89246d52b66f Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 3 Feb 2023 16:13:12 +0100 Subject: [PATCH 057/185] fix: Address review --- src/Avalonia.Base/Animation/Animatable.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Base/Animation/Animatable.cs b/src/Avalonia.Base/Animation/Animatable.cs index 50d9a48e9e..6a05dfbe6d 100644 --- a/src/Avalonia.Base/Animation/Animatable.cs +++ b/src/Avalonia.Base/Animation/Animatable.cs @@ -29,12 +29,9 @@ namespace Avalonia.Animation private bool _transitionsEnabled = true; private bool _isSubscribedToTransitionsCollection = false; private Dictionary? _transitionState; - readonly NotifyCollectionChangedEventHandler _collectionChanged; - - public Animatable() - { - _collectionChanged = TransitionsCollectionChanged; - } + private NotifyCollectionChangedEventHandler _collectionChanged; + private NotifyCollectionChangedEventHandler TransitionsCollectionChangedHandler => + _collectionChanged ??= TransitionsCollectionChanged; /// /// Gets or sets the clock which controls the animations on the control. @@ -72,7 +69,7 @@ namespace Avalonia.Animation if (!_isSubscribedToTransitionsCollection) { _isSubscribedToTransitionsCollection = true; - transitions.CollectionChanged += _collectionChanged; + transitions.CollectionChanged += TransitionsCollectionChangedHandler; } AddTransitions(transitions); } @@ -97,7 +94,7 @@ namespace Avalonia.Animation if (_isSubscribedToTransitionsCollection) { _isSubscribedToTransitionsCollection = false; - transitions.CollectionChanged -= _collectionChanged; + transitions.CollectionChanged -= TransitionsCollectionChangedHandler; } RemoveTransitions(transitions); } @@ -126,7 +123,7 @@ namespace Avalonia.Animation toAdd = newTransitions.Except(oldTransitions).ToList(); } - newTransitions.CollectionChanged += _collectionChanged; + newTransitions.CollectionChanged += TransitionsCollectionChangedHandler; _isSubscribedToTransitionsCollection = true; AddTransitions(toAdd); } @@ -140,7 +137,7 @@ namespace Avalonia.Animation toRemove = oldTransitions.Except(newTransitions).ToList(); } - oldTransitions.CollectionChanged -= _collectionChanged; + oldTransitions.CollectionChanged -= TransitionsCollectionChangedHandler; RemoveTransitions(toRemove); } } From 41966d314fe8c0e7d8e82b513e285adfb3379c48 Mon Sep 17 00:00:00 2001 From: workgroupengineering Date: Fri, 3 Feb 2023 16:40:53 +0100 Subject: [PATCH 058/185] fix: Builld --- src/Avalonia.Base/Animation/Animatable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Animation/Animatable.cs b/src/Avalonia.Base/Animation/Animatable.cs index 6a05dfbe6d..5208c8b218 100644 --- a/src/Avalonia.Base/Animation/Animatable.cs +++ b/src/Avalonia.Base/Animation/Animatable.cs @@ -29,7 +29,7 @@ namespace Avalonia.Animation private bool _transitionsEnabled = true; private bool _isSubscribedToTransitionsCollection = false; private Dictionary? _transitionState; - private NotifyCollectionChangedEventHandler _collectionChanged; + private NotifyCollectionChangedEventHandler? _collectionChanged; private NotifyCollectionChangedEventHandler TransitionsCollectionChangedHandler => _collectionChanged ??= TransitionsCollectionChanged; From 2bbaf74d8f05184b4d7873e526dc97935efd6096 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 4 Feb 2023 00:13:25 +0100 Subject: [PATCH 059/185] Fix integration tests on macOS. Seems that the version of macOS on the test runner applies some shading to the top of the window. Check the color of a pixel lower down. --- tests/Avalonia.IntegrationTests.Appium/WindowTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index 3b74ed314b..7bb991aae6 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -202,7 +202,7 @@ namespace Avalonia.IntegrationTests.Appium window.Click(); var img = SixLabors.ImageSharp.Image.Load(screenshot.AsByteArray); - var topLeftColor = img[1, 1]; + var topLeftColor = img[10, 10]; var centerColor = img[img.Width / 2, img.Height / 2]; Assert.Equal(new Rgba32(0, 128, 0), topLeftColor); @@ -223,7 +223,7 @@ namespace Avalonia.IntegrationTests.Appium window.Click(); var img = SixLabors.ImageSharp.Image.Load(screenshot.AsByteArray); - var topLeftColor = img[1, 1]; + var topLeftColor = img[10, 10]; var centerColor = img[img.Width / 2, img.Height / 2]; Assert.Equal(new Rgba32(0, 128, 0), topLeftColor); From db78f7c8701c94f13794d6e9e5f7c872f89f4387 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 3 Feb 2023 21:18:55 -0500 Subject: [PATCH 060/185] Update angle to 2.1.0.2023020321 --- samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj | 1 - samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj | 1 - src/Windows/Avalonia.Win32/Avalonia.Win32.csproj | 2 +- .../Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index e4c83dca49..e465e9caf3 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -31,7 +31,6 @@ - diff --git a/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj b/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj index 1b83a3e567..a24e55de81 100644 --- a/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj +++ b/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj @@ -24,7 +24,6 @@ - diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index b7dca78845..a24fe31df8 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj index 0c0fe5b921..f3af312d1a 100644 --- a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj +++ b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj @@ -23,7 +23,7 @@ - + From 757f82c385c7f23b790b3d20e428c55b044c1317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Sat, 4 Feb 2023 17:44:14 +0000 Subject: [PATCH 061/185] Updated browser packages to stable. --- .../ControlCatalog.Browser.Blazor.csproj | 4 ++-- .../Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj b/samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj index d0fb614840..733a4b7194 100644 --- a/samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj +++ b/samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj b/src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj index a9cad0538f..9017ce1546 100644 --- a/src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj +++ b/src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj @@ -15,7 +15,7 @@ - + From d0fe9e0e14f3dbe72ece505ddb2d2ea88606d371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Sat, 4 Feb 2023 18:06:42 +0000 Subject: [PATCH 062/185] Updated Skia and HarfBuzz to latest stable. --- build/HarfBuzzSharp.props | 6 +++--- build/SkiaSharp.props | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props index 620ec58ff3..75d317be1a 100644 --- a/build/HarfBuzzSharp.props +++ b/build/HarfBuzzSharp.props @@ -1,7 +1,7 @@  - - - + + + diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index 31619399f9..f45addaa2a 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,7 +1,7 @@  - - - + + + From 9fdf98dde6e4d466e9ff72bf9635f8fdad1455f6 Mon Sep 17 00:00:00 2001 From: amwx <40413319+amwx@users.noreply.github.com> Date: Sat, 4 Feb 2023 14:18:35 -0500 Subject: [PATCH 063/185] Only use font fallback if match found --- .../Media/TextFormatting/TextCharacters.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index c9dafaced7..8480be3882 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -118,14 +118,18 @@ namespace Avalonia.Media.TextFormatting fontManager.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo, out var fallbackTypeface); - - var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface); - - if (matchFound && TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count)) + + if (matchFound) { + var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface); + //Fallback found - return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface), - biDiLevel); + if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count)) + { + + return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface), + biDiLevel); + } } // no fallback found From 1cfdae9448d9eac12bba5ac02ac859ab7e459134 Mon Sep 17 00:00:00 2001 From: amwx <40413319+amwx@users.noreply.github.com> Date: Sat, 4 Feb 2023 14:23:03 -0500 Subject: [PATCH 064/185] Cleanup --- src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 8480be3882..b4734d702b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -121,12 +121,11 @@ namespace Avalonia.Media.TextFormatting if (matchFound) { + // Fallback found var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface); - - //Fallback found + if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count)) - { - + { return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface), biDiLevel); } From 571b7746eac6cc694aa586ec83d48ad5c9f4b9ac Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 22 Jan 2023 11:46:28 +0100 Subject: [PATCH 065/185] Added failing integration test. --- samples/IntegrationTestApp/MainWindow.axaml | 1 + .../IntegrationTestApp/MainWindow.axaml.cs | 2 ++ .../WindowTests_MacOS.cs | 24 ++++++++++++++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index b116e4c789..7b1f0eddce 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -139,6 +139,7 @@ Maximized FullScreen + Can Resize diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 3cd5350cce..087f25666b 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -66,11 +66,13 @@ namespace IntegrationTestApp var locationComboBox = this.GetControl("ShowWindowLocation"); var stateComboBox = this.GetControl("ShowWindowState"); var size = !string.IsNullOrWhiteSpace(sizeTextBox.Text) ? Size.Parse(sizeTextBox.Text) : (Size?)null; + var canResizeCheckBox = this.GetControl("ShowWindowCanResize"); var owner = (Window)this.GetVisualRoot()!; var window = new ShowWindowTest { WindowStartupLocation = (WindowStartupLocation)locationComboBox.SelectedIndex, + CanResize = canResizeCheckBox.IsChecked.Value, }; if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index d9817ecdd1..06180be74a 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -323,11 +323,30 @@ namespace Avalonia.IntegrationTests.Appium secondaryWindow.GetChromeButtons().close.Click(); } - private IDisposable OpenWindow(PixelSize? size, ShowWindowMode mode, WindowStartupLocation location) + [PlatformTheory(TestPlatforms.MacOS)] + [InlineData(ShowWindowMode.NonOwned)] + [InlineData(ShowWindowMode.Owned)] + [InlineData(ShowWindowMode.Modal)] + public void Window_Has_Disabled_Zoom_Button_When_CanResize_Is_False(ShowWindowMode mode) + { + using (OpenWindow(null, mode, WindowStartupLocation.Manual, canResize: false)) + { + var secondaryWindow = GetWindow("SecondaryWindow"); + var (_, _, zoomButton) = secondaryWindow.GetChromeButtons(); + Assert.False(zoomButton.Enabled); + } + } + + private IDisposable OpenWindow( + PixelSize? size, + ShowWindowMode mode, + WindowStartupLocation location, + bool canResize = true) { var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize"); var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode"); var locationComboBox = _session.FindElementByAccessibilityId("ShowWindowLocation"); + var canResizeCheckBox = _session.FindElementByAccessibilityId("ShowWindowCanResize"); var showButton = _session.FindElementByAccessibilityId("ShowWindow"); if (size.HasValue) @@ -338,6 +357,9 @@ namespace Avalonia.IntegrationTests.Appium locationComboBox.Click(); _session.FindElementByName(location.ToString()).SendClick(); + + if (canResizeCheckBox.GetIsChecked() != canResize) + canResizeCheckBox.Click(); return showButton.OpenWindowWithClick(); } From 2951b80c39601a8368cc902f76ef4f01bc9307c5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 4 Feb 2023 21:26:19 +0100 Subject: [PATCH 066/185] Add additional failing integration tests. For problems introduced in #10153. --- .../WindowTests.cs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index 7bb991aae6..23381c2e58 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -30,9 +30,9 @@ namespace Avalonia.IntegrationTests.Appium [Theory] [MemberData(nameof(StartupLocationData))] - public void StartupLocation(Size? size, ShowWindowMode mode, WindowStartupLocation location) + public void StartupLocation(Size? size, ShowWindowMode mode, WindowStartupLocation location, bool canResize) { - using var window = OpenWindow(size, mode, location); + using var window = OpenWindow(size, mode, location, canResize: canResize); var info = GetWindowInfo(); if (size.HasValue) @@ -230,10 +230,10 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal(new Rgba32(255, 0, 0), centerColor); } - public static TheoryData StartupLocationData() + public static TheoryData StartupLocationData() { var sizes = new Size?[] { null, new Size(400, 300) }; - var data = new TheoryData(); + var data = new TheoryData(); foreach (var size in sizes) { @@ -243,7 +243,8 @@ namespace Avalonia.IntegrationTests.Appium { if (!(location == WindowStartupLocation.CenterOwner && mode == ShowWindowMode.NonOwned)) { - data.Add(size, mode, location); + data.Add(size, mode, location, true); + data.Add(size, mode, location, false); } } } @@ -311,14 +312,16 @@ namespace Avalonia.IntegrationTests.Appium Size? size, ShowWindowMode mode, WindowStartupLocation location = WindowStartupLocation.Manual, - WindowState state = Controls.WindowState.Normal) + WindowState state = Controls.WindowState.Normal, + bool canResize = true) { var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize"); var modeComboBox = _session.FindElementByAccessibilityId("ShowWindowMode"); var locationComboBox = _session.FindElementByAccessibilityId("ShowWindowLocation"); var stateComboBox = _session.FindElementByAccessibilityId("ShowWindowState"); + var canResizeCheckBox = _session.FindElementByAccessibilityId("ShowWindowCanResize"); var showButton = _session.FindElementByAccessibilityId("ShowWindow"); - + if (size.HasValue) sizeTextBox.SendKeys($"{size.Value.Width}, {size.Value.Height}"); @@ -331,6 +334,9 @@ namespace Avalonia.IntegrationTests.Appium stateComboBox.Click(); _session.FindElementByAccessibilityId($"ShowWindowState{state}").SendClick(); + if (canResizeCheckBox.GetIsChecked() != canResize) + canResizeCheckBox.Click(); + return showButton.OpenWindowWithClick(); } From fe2233d25a53e654ff82705cba3069319538743c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sat, 4 Feb 2023 22:03:50 +0100 Subject: [PATCH 067/185] Update ItemsPresenter.cs --- src/Avalonia.Controls/Presenters/ItemsPresenter.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 8594b584fa..e8eaac7d17 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -28,19 +28,19 @@ namespace Avalonia.Controls.Presenters /// Defines the property. /// public static readonly StyledProperty AreHorizontalSnapPointsRegularProperty = - AvaloniaProperty.Register(nameof(AreHorizontalSnapPointsRegular)); + AvaloniaProperty.Register(nameof(AreHorizontalSnapPointsRegular)); /// /// Defines the property. /// public static readonly StyledProperty AreVerticalSnapPointsRegularProperty = - AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular)); + AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular)); /// /// Defines the event. /// public static readonly RoutedEvent HorizontalSnapPointsChangedEvent = - RoutedEvent.Register( + RoutedEvent.Register( nameof(HorizontalSnapPointsChanged), RoutingStrategies.Bubble); @@ -48,7 +48,7 @@ namespace Avalonia.Controls.Presenters /// Defines the event. /// public static readonly RoutedEvent VerticalSnapPointsChangedEvent = - RoutedEvent.Register( + RoutedEvent.Register( nameof(VerticalSnapPointsChanged), RoutingStrategies.Bubble); @@ -139,7 +139,7 @@ namespace Avalonia.Controls.Presenters Size IScrollable.Viewport => _logicalScrollable?.Viewport ?? default; /// - /// Gets or sets whether the horizontal snap points for the are equidistant from each other. + /// Gets or sets whether the horizontal snap points for the are equidistant from each other. /// public bool AreHorizontalSnapPointsRegular { @@ -148,7 +148,7 @@ namespace Avalonia.Controls.Presenters } /// - /// Gets or sets whether the vertical snap points for the are equidistant from each other. + /// Gets or sets whether the vertical snap points for the are equidistant from each other. /// public bool AreVerticalSnapPointsRegular { From f41749170277d8e79c7a5e16af1a1db083705008 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 4 Feb 2023 22:44:24 +0100 Subject: [PATCH 068/185] Speed up integration tests. Don't change combo boxes that are already set to the correct value. --- .../WindowTests.cs | 21 +++++++++++++------ .../WindowTests_MacOS.cs | 14 +++++++++---- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index 23381c2e58..f3861ca267 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -325,14 +325,23 @@ namespace Avalonia.IntegrationTests.Appium if (size.HasValue) sizeTextBox.SendKeys($"{size.Value.Width}, {size.Value.Height}"); - modeComboBox.Click(); - _session.FindElementByName(mode.ToString()).SendClick(); + if (modeComboBox.GetComboBoxValue() != mode.ToString()) + { + modeComboBox.Click(); + _session.FindElementByName(mode.ToString()).SendClick(); + } - locationComboBox.Click(); - _session.FindElementByName(location.ToString()).SendClick(); + if (locationComboBox.GetComboBoxValue() != location.ToString()) + { + locationComboBox.Click(); + _session.FindElementByName(location.ToString()).SendClick(); + } - stateComboBox.Click(); - _session.FindElementByAccessibilityId($"ShowWindowState{state}").SendClick(); + if (stateComboBox.GetComboBoxValue() != state.ToString()) + { + stateComboBox.Click(); + _session.FindElementByAccessibilityId($"ShowWindowState{state}").SendClick(); + } if (canResizeCheckBox.GetIsChecked() != canResize) canResizeCheckBox.Click(); diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 06180be74a..0839cbf183 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -352,11 +352,17 @@ namespace Avalonia.IntegrationTests.Appium if (size.HasValue) sizeTextBox.SendKeys($"{size.Value.Width}, {size.Value.Height}"); - modeComboBox.Click(); - _session.FindElementByName(mode.ToString()).SendClick(); + if (modeComboBox.GetComboBoxValue() != mode.ToString()) + { + modeComboBox.Click(); + _session.FindElementByName(mode.ToString()).SendClick(); + } - locationComboBox.Click(); - _session.FindElementByName(location.ToString()).SendClick(); + if (locationComboBox.GetComboBoxValue() != location.ToString()) + { + locationComboBox.Click(); + _session.FindElementByName(location.ToString()).SendClick(); + } if (canResizeCheckBox.GetIsChecked() != canResize) canResizeCheckBox.Click(); From 465d72f2903572c66a444f9f2b31392b7ad6cd69 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Sat, 4 Feb 2023 16:46:13 -0700 Subject: [PATCH 069/185] Add nullable ref annotation to Windows.Close --- src/Avalonia.Controls/Window.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index a20b4eee58..b1110ece55 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -450,7 +450,7 @@ namespace Avalonia.Controls /// resulting task will produce the value when the window /// is closed. /// - public void Close(object dialogResult) + public void Close(object? dialogResult) { _dialogResult = dialogResult; CloseCore(WindowCloseReason.WindowClosing, true); From f8af158a38bd1718126d0b2e3fc35d8b89a18ad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Sat, 4 Feb 2023 17:35:08 +0000 Subject: [PATCH 070/185] Updated packages used by tests. --- build/ImageSharp.props | 2 +- build/Moq.props | 2 +- build/XUnit.props | 15 +++++++-------- .../Rendering/SceneGraph/DrawOperationTests.cs | 4 ++-- .../Avalonia.Benchmarks.csproj | 2 +- .../Avalonia.IntegrationTests.Appium.csproj | 2 +- .../Avalonia.LeakTests/Avalonia.LeakTests.csproj | 2 +- 7 files changed, 14 insertions(+), 15 deletions(-) diff --git a/build/ImageSharp.props b/build/ImageSharp.props index 178c274ac9..66e6580070 100644 --- a/build/ImageSharp.props +++ b/build/ImageSharp.props @@ -1,5 +1,5 @@ - + diff --git a/build/Moq.props b/build/Moq.props index 9e2fd1db5d..357f0c9a5f 100644 --- a/build/Moq.props +++ b/build/Moq.props @@ -1,5 +1,5 @@  - + diff --git a/build/XUnit.props b/build/XUnit.props index 17ead91aa3..3c89c8b52b 100644 --- a/build/XUnit.props +++ b/build/XUnit.props @@ -1,13 +1,12 @@  - - - - - - - - + + + + + + + diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs index 07d2d672ae..c1468a28e4 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs @@ -29,7 +29,7 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph double height, double scaleX, double scaleY, - double? penThickness, + double penThickness, double expectedX, double expectedY, double expectedWidth, @@ -38,7 +38,7 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph var target = new TestRectangleDrawOperation( new Rect(x, y, width, height), Matrix.CreateScale(scaleX, scaleY), - penThickness.HasValue ? new Pen(Brushes.Black, penThickness.Value) : null); + new Pen(Brushes.Black, penThickness)); Assert.Equal(new Rect(expectedX, expectedY, expectedWidth, expectedHeight), target.Bounds); } diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index 0ddee2ad7a..9ea0482abc 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -14,7 +14,7 @@ - + diff --git a/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj b/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj index 3ff91139f1..5de2b85569 100644 --- a/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj +++ b/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj @@ -10,7 +10,7 @@ - + diff --git a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj index 15815c81b6..c3d9aa0622 100644 --- a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj +++ b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj @@ -1,6 +1,6 @@  - net461 + net462 From 8c07476a1a61ff0d70af12672697b301958394b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Sun, 5 Feb 2023 09:01:44 +0000 Subject: [PATCH 071/185] Fixed NuGet package authors. --- build/SharedVersion.props | 1 + 1 file changed, 1 insertion(+) diff --git a/build/SharedVersion.props b/build/SharedVersion.props index eca3ba37b0..2849262591 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -3,6 +3,7 @@ Avalonia 11.0.999 + Avalonia Team Copyright 2022 © The AvaloniaUI Project https://avaloniaui.net https://github.com/AvaloniaUI/Avalonia/ From cc850694cdf17aaca4fc48571090620371fe4e1f Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Sun, 5 Feb 2023 12:00:38 +0100 Subject: [PATCH 072/185] editorconfig: don't apply s_ prefix to public fields --- .editorconfig | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/.editorconfig b/.editorconfig index e6ae266849..a144ec8843 100644 --- a/.editorconfig +++ b/.editorconfig @@ -55,16 +55,17 @@ dotnet_naming_symbols.constant_fields.required_modifiers = const dotnet_naming_style.pascal_case_style.capitalization = pascal_case -# static fields should have s_ prefix -dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion -dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields -dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style +# private static fields should have s_ prefix +dotnet_naming_rule.private_static_fields_should_have_prefix.severity = suggestion +dotnet_naming_rule.private_static_fields_should_have_prefix.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_should_have_prefix.style = private_static_prefix_style -dotnet_naming_symbols.static_fields.applicable_kinds = field -dotnet_naming_symbols.static_fields.required_modifiers = static +dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.required_modifiers = static +dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private -dotnet_naming_style.static_prefix_style.required_prefix = s_ -dotnet_naming_style.static_prefix_style.capitalization = camel_case +dotnet_naming_style.private_static_prefix_style.required_prefix = s_ +dotnet_naming_style.private_static_prefix_style.capitalization = camel_case # internal and private fields should be _camelCase dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion @@ -117,7 +118,7 @@ csharp_space_after_dot = false csharp_space_after_keywords_in_control_flow_statements = true csharp_space_after_semicolon_in_for_statement = true csharp_space_around_binary_operators = before_and_after -csharp_space_around_declaration_statements = do_not_ignore +csharp_space_around_declaration_statements = false csharp_space_before_colon_in_inheritance_clause = true csharp_space_before_comma = false csharp_space_before_dot = false @@ -211,5 +212,5 @@ indent_size = 2 # Shell scripts [*.sh] end_of_line = lf -[*.{cmd, bat}] +[*.{cmd,bat}] end_of_line = crlf From 7eda49e0616dfed92badbfb1dfddec952e377612 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Sun, 5 Feb 2023 22:33:45 +0100 Subject: [PATCH 073/185] Ensure IRenderRoot.Renderer is never null --- .../Composition/CompositingRenderer.cs | 82 +++++++++++++------ src/Avalonia.Base/Rendering/IRenderRoot.cs | 2 - src/Avalonia.Base/Rendering/IRenderer.cs | 3 + src/Avalonia.Base/Visual.cs | 10 +-- src/Avalonia.Controls/TopLevel.cs | 40 +++------ src/Avalonia.Controls/Window.cs | 6 +- src/Avalonia.Controls/WindowBase.cs | 6 +- .../Diagnostics/Controls/Application.cs | 2 +- src/Avalonia.Native/WindowImpl.cs | 3 +- .../ButtonTests.cs | 20 ++--- .../TopLevelTests.cs | 39 +++++---- .../Utils/HotKeyManagerTests.cs | 12 ++- .../WindowBaseTests.cs | 14 +++- .../WindowTests.cs | 2 + .../WindowingPlatformMock.cs | 35 -------- tests/Avalonia.UnitTests/TestTemplatedRoot.cs | 54 ------------ 16 files changed, 138 insertions(+), 192 deletions(-) delete mode 100644 tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs delete mode 100644 tests/Avalonia.UnitTests/TestTemplatedRoot.cs diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 668d650ffd..7fa2d4955f 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -20,15 +20,17 @@ public class CompositingRenderer : IRendererWithCompositor { private readonly IRenderRoot _root; private readonly Compositor _compositor; - CompositionDrawingContext _recorder = new(); - DrawingContext _recordingContext; - private HashSet _dirty = new(); - private HashSet _recalculateChildren = new(); + private readonly CompositionDrawingContext _recorder = new(); + private readonly DrawingContext _recordingContext; + private readonly HashSet _dirty = new(); + private readonly HashSet _recalculateChildren = new(); + private readonly Action _update; + private bool _queuedUpdate; - private Action _update; private bool _updating; + private bool _isDisposed; - internal CompositionTarget CompositionTarget; + internal CompositionTarget CompositionTarget { get; } /// /// Asks the renderer to only draw frames on the render thread. Makes Paint to wait until frame is rendered. @@ -38,6 +40,17 @@ public class CompositingRenderer : IRendererWithCompositor /// public RendererDiagnostics Diagnostics { get; } + /// + public Compositor Compositor => _compositor; + + /// + /// Initializes a new instance of + /// + /// The render root using this renderer. + /// The associated compositors. + /// + /// A function returning the list of native platform's surfaces that can be consumed by rendering subsystems. + /// public CompositingRenderer(IRenderRoot root, Compositor compositor, Func> surfaces) { _root = root; @@ -66,7 +79,7 @@ public class CompositingRenderer : IRendererWithCompositor /// public event EventHandler? SceneInvalidated; - void QueueUpdate() + private void QueueUpdate() { if(_queuedUpdate) return; @@ -77,9 +90,11 @@ public class CompositingRenderer : IRendererWithCompositor /// public void AddDirty(Visual visual) { + if (_isDisposed) + return; if (_updating) throw new InvalidOperationException("Visual was invalidated during the render pass"); - _dirty.Add((Visual)visual); + _dirty.Add(visual); QueueUpdate(); } @@ -126,9 +141,11 @@ public class CompositingRenderer : IRendererWithCompositor /// public void RecalculateChildren(Visual visual) { + if (_isDisposed) + return; if (_updating) throw new InvalidOperationException("Visual was invalidated during the render pass"); - _recalculateChildren.Add((Visual)visual); + _recalculateChildren.Add(visual); QueueUpdate(); } @@ -171,7 +188,7 @@ public class CompositingRenderer : IRendererWithCompositor if (sortedChildren != null) for (var c = 0; c < visualChildren.Count; c++) { - if (!ReferenceEquals(compositionChildren[c], ((Visual)sortedChildren[c].visual).CompositionVisual)) + if (!ReferenceEquals(compositionChildren[c], sortedChildren[c].visual.CompositionVisual)) { mismatch = true; break; @@ -179,7 +196,7 @@ public class CompositingRenderer : IRendererWithCompositor } else for (var c = 0; c < visualChildren.Count; c++) - if (!ReferenceEquals(compositionChildren[c], ((Visual)visualChildren[c]).CompositionVisual)) + if (!ReferenceEquals(compositionChildren[c], visualChildren[c].CompositionVisual)) { mismatch = true; break; @@ -201,7 +218,7 @@ public class CompositingRenderer : IRendererWithCompositor { foreach (var ch in sortedChildren) { - var compositionChild = ((Visual)ch.visual).CompositionVisual; + var compositionChild = ch.visual.CompositionVisual; if (compositionChild != null) compositionChildren.Add(compositionChild); } @@ -210,7 +227,7 @@ public class CompositingRenderer : IRendererWithCompositor else foreach (var ch in v.GetVisualChildren()) { - var compositionChild = ((Visual)ch).CompositionVisual; + var compositionChild = ch.CompositionVisual; if (compositionChild != null) compositionChildren.Add(compositionChild); } @@ -289,13 +306,18 @@ public class CompositingRenderer : IRendererWithCompositor _updating = false; } } - + + /// public void Resized(Size size) { } + /// public void Paint(Rect rect) { + if (_isDisposed) + return; + QueueUpdate(); CompositionTarget.RequestRedraw(); if(RenderOnlyOnRenderThread && Compositor.Loop.RunsInBackground) @@ -304,17 +326,34 @@ public class CompositingRenderer : IRendererWithCompositor CompositionTarget.ImmediateUIThreadRender(); } - public void Start() => CompositionTarget.IsEnabled = true; - - public void Stop() + /// + public void Start() { - CompositionTarget.IsEnabled = false; + if (_isDisposed) + return; + + CompositionTarget.IsEnabled = true; } - public ValueTask TryGetRenderInterfaceFeature(Type featureType) => Compositor.TryGetRenderInterfaceFeature(featureType); + /// + public void Stop() + => CompositionTarget.IsEnabled = false; + + /// + public ValueTask TryGetRenderInterfaceFeature(Type featureType) + => Compositor.TryGetRenderInterfaceFeature(featureType); + /// public void Dispose() { + if (_isDisposed) + return; + + _isDisposed = true; + _dirty.Clear(); + _recalculateChildren.Clear(); + SceneInvalidated = null; + Stop(); CompositionTarget.Dispose(); @@ -323,9 +362,4 @@ public class CompositingRenderer : IRendererWithCompositor if (Compositor.Loop.RunsInBackground) _compositor.Commit().Wait(); } - - /// - /// The associated object - /// - public Compositor Compositor => _compositor; } diff --git a/src/Avalonia.Base/Rendering/IRenderRoot.cs b/src/Avalonia.Base/Rendering/IRenderRoot.cs index fa3260ffb4..09df7b7830 100644 --- a/src/Avalonia.Base/Rendering/IRenderRoot.cs +++ b/src/Avalonia.Base/Rendering/IRenderRoot.cs @@ -1,6 +1,4 @@ using Avalonia.Metadata; -using Avalonia.Platform; -using Avalonia.VisualTree; namespace Avalonia.Rendering { diff --git a/src/Avalonia.Base/Rendering/IRenderer.cs b/src/Avalonia.Base/Rendering/IRenderer.cs index ba960ff5f3..7e32504e17 100644 --- a/src/Avalonia.Base/Rendering/IRenderer.cs +++ b/src/Avalonia.Base/Rendering/IRenderer.cs @@ -90,6 +90,9 @@ namespace Avalonia.Rendering public interface IRendererWithCompositor : IRenderer { + /// + /// The associated object + /// Compositor Compositor { get; } } } diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index e6d7492c51..fc215fc890 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -348,7 +348,7 @@ namespace Avalonia /// public void InvalidateVisual() { - VisualRoot?.Renderer?.AddDirty(this); + VisualRoot?.Renderer.AddDirty(this); } /// @@ -449,7 +449,7 @@ namespace Avalonia protected override void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { base.LogicalChildrenCollectionChanged(sender, e); - VisualRoot?.Renderer?.RecalculateChildren(this); + VisualRoot?.Renderer.RecalculateChildren(this); } /// @@ -477,7 +477,7 @@ namespace Avalonia OnAttachedToVisualTree(e); AttachedToVisualTree?.Invoke(this, e); InvalidateVisual(); - _visualRoot.Renderer?.RecalculateChildren(_visualParent!); + _visualRoot.Renderer.RecalculateChildren(_visualParent!); if (ZIndex != 0 && VisualParent is Visual parent) parent.HasNonUniformZIndexChildren = true; @@ -540,7 +540,7 @@ namespace Avalonia } DetachedFromVisualTree?.Invoke(this, e); - e.Root?.Renderer?.AddDirty(this); + e.Root.Renderer.AddDirty(this); var visualChildren = VisualChildren; @@ -659,7 +659,7 @@ namespace Avalonia parentVisual.HasNonUniformZIndexChildren = true; sender?.InvalidateVisual(); - parent?.VisualRoot?.Renderer?.RecalculateChildren(parent); + parent?.VisualRoot?.Renderer.RecalculateChildren(parent); } /// diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 676fa1519a..4db71abfa8 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -94,7 +94,6 @@ namespace Avalonia.Controls private readonly IInputManager? _inputManager; private readonly IAccessKeyHandler? _accessKeyHandler; private readonly IKeyboardNavigationHandler? _keyboardNavigationHandler; - private readonly IPlatformRenderInterface? _renderInterface; private readonly IGlobalStyles? _globalStyles; private readonly IGlobalThemeVariantProvider? _applicationThemeHost; private readonly PointerOverPreProcessor? _pointerOverPreProcessor; @@ -136,36 +135,21 @@ namespace Avalonia.Controls /// public TopLevel(ITopLevelImpl impl, IAvaloniaDependencyResolver? dependencyResolver) { - if (impl == null) - { - throw new InvalidOperationException( - "Could not create window implementation: maybe no windowing subsystem was initialized?"); - } - - PlatformImpl = impl; + PlatformImpl = impl ?? throw new InvalidOperationException( + "Could not create window implementation: maybe no windowing subsystem was initialized?"); _actualTransparencyLevel = PlatformImpl.TransparencyLevel; - dependencyResolver = dependencyResolver ?? AvaloniaLocator.Current; + dependencyResolver ??= AvaloniaLocator.Current; _accessKeyHandler = TryGetService(dependencyResolver); _inputManager = TryGetService(dependencyResolver); _keyboardNavigationHandler = TryGetService(dependencyResolver); - _renderInterface = TryGetService(dependencyResolver); _globalStyles = TryGetService(dependencyResolver); _applicationThemeHost = TryGetService(dependencyResolver); Renderer = impl.CreateRenderer(this); - - if (Renderer != null) - { - Renderer.SceneInvalidated += SceneInvalidated; - } - else - { - // Prevent nullable error. - Renderer = null!; - } + Renderer.SceneInvalidated += SceneInvalidated; impl.SetInputRoot(this); @@ -216,7 +200,7 @@ namespace Avalonia.Controls if(impl.TryGetFeature() is {} systemNavigationManager) { - systemNavigationManager.BackRequested += (s, e) => + systemNavigationManager.BackRequested += (_, e) => { e.RoutedEvent = BackRequestedEvent; RaiseEvent(e); @@ -337,7 +321,7 @@ namespace Avalonia.Controls { _layoutManager = CreateLayoutManager(); - if (_layoutManager is LayoutManager typedLayoutManager && Renderer is not null) + if (_layoutManager is LayoutManager typedLayoutManager) { _layoutDiagnosticBridge = new LayoutDiagnosticBridge(Renderer.Diagnostics, typedLayoutManager); _layoutDiagnosticBridge.SetupBridge(); @@ -356,7 +340,7 @@ namespace Avalonia.Controls /// /// Gets the renderer for the window. /// - public IRenderer Renderer { get; private set; } + public IRenderer Renderer { get; } internal PixelPoint? LastPointerPosition => _pointerOverPreProcessor?.LastPosition; @@ -450,7 +434,7 @@ namespace Avalonia.Controls /// The dirty area. protected virtual void HandlePaint(Rect rect) { - Renderer?.Paint(rect); + Renderer.Paint(rect); } /// @@ -468,8 +452,8 @@ namespace Avalonia.Controls _applicationThemeHost.ActualThemeVariantChanged -= GlobalActualThemeVariantChanged; } - Renderer?.Dispose(); - Renderer = null!; + Renderer.SceneInvalidated -= SceneInvalidated; + Renderer.Dispose(); _layoutDiagnosticBridge?.Dispose(); _layoutDiagnosticBridge = null; @@ -488,7 +472,7 @@ namespace Avalonia.Controls OnClosed(EventArgs.Empty); - LayoutManager?.Dispose(); + LayoutManager.Dispose(); } /// @@ -503,7 +487,7 @@ namespace Avalonia.Controls Width = clientSize.Width; Height = clientSize.Height; LayoutManager.ExecuteLayoutPass(); - Renderer?.Resized(clientSize); + Renderer.Resized(clientSize); } /// diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index b1110ece55..ba1b599421 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -573,7 +573,7 @@ namespace Avalonia.Controls return; } - Renderer?.Stop(); + Renderer.Stop(); if (Owner is Window owner) { @@ -721,7 +721,7 @@ namespace Avalonia.Controls SetWindowStartupLocation(owner?.PlatformImpl); PlatformImpl?.Show(ShowActivated, false); - Renderer?.Start(); + Renderer.Start(); OnOpened(EventArgs.Empty); } } @@ -798,7 +798,7 @@ namespace Avalonia.Controls PlatformImpl?.Show(ShowActivated, true); - Renderer?.Start(); + Renderer.Start(); Observable.FromEventPattern( x => Closed += x, diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index aad0482b50..0c9a91148b 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -129,7 +129,7 @@ namespace Avalonia.Controls { using (FreezeVisibilityChangeHandling()) { - Renderer?.Stop(); + Renderer.Stop(); PlatformImpl?.Hide(); IsVisible = false; } @@ -153,7 +153,7 @@ namespace Avalonia.Controls } PlatformImpl?.Show(true, false); - Renderer?.Start(); + Renderer.Start(); OnOpened(EventArgs.Empty); } } @@ -219,7 +219,7 @@ namespace Avalonia.Controls { ClientSize = clientSize; LayoutManager.ExecuteLayoutPass(); - Renderer?.Resized(clientSize); + Renderer.Resized(clientSize); } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs index 7426c4e2ed..a0ff3a714f 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs @@ -33,7 +33,7 @@ namespace Avalonia.Diagnostics.Controls RendererRoot = application.ApplicationLifetime switch { Lifetimes.IClassicDesktopStyleApplicationLifetime classic => classic.MainWindow?.Renderer, - Lifetimes.ISingleViewApplicationLifetime single => (single.MainView as Visual)?.VisualRoot?.Renderer, + Lifetimes.ISingleViewApplicationLifetime single => single.MainView?.VisualRoot?.Renderer, _ => null }; diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index f27d94b61a..64c1d0da10 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -119,7 +119,8 @@ namespace Avalonia.Native { if(e.Type == RawPointerEventType.LeftButtonDown) { - var visual = (_inputRoot as Window).Renderer.HitTestFirst(e.Position, _inputRoot as Window, x => + var window = _inputRoot as Window; + var visual = window?.Renderer.HitTestFirst(e.Position, window, x => { if (x is IInputElement ie && (!ie.IsHitTestVisible || !ie.IsEffectivelyVisible)) { diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs index 4ff98bdedd..2679d4ce06 100644 --- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs @@ -140,10 +140,9 @@ namespace Avalonia.Controls.UnitTests .Returns>((p, r, f) => r.Bounds.Contains(p) ? new Visual[] { r } : new Visual[0]); - var target = new TestButton() + var target = new TestButton(renderer.Object) { - Bounds = new Rect(0, 0, 100, 100), - Renderer = renderer.Object + Bounds = new Rect(0, 0, 100, 100) }; bool clicked = false; @@ -172,10 +171,9 @@ namespace Avalonia.Controls.UnitTests .Returns>((p, r, f) => r.Bounds.Contains(p) ? new Visual[] { r } : new Visual[0]); - var target = new TestButton() + var target = new TestButton(renderer.Object) { - Bounds = new Rect(0, 0, 100, 100), - Renderer = renderer.Object + Bounds = new Rect(0, 0, 100, 100) }; bool clicked = false; @@ -206,11 +204,10 @@ namespace Avalonia.Controls.UnitTests r.Bounds.Contains(p.Transform(r.RenderTransform.Value.Invert())) ? new Visual[] { r } : new Visual[0]); - var target = new TestButton() + var target = new TestButton(renderer.Object) { Bounds = new Rect(0, 0, 100, 100), - RenderTransform = new TranslateTransform { X = 100, Y = 0 }, - Renderer = renderer.Object + RenderTransform = new TranslateTransform { X = 100, Y = 0 } }; //actual bounds of button should be 100,0,100,100 x -> translated 100 pixels @@ -386,9 +383,10 @@ namespace Avalonia.Controls.UnitTests private class TestButton : Button, IRenderRoot { - public TestButton() + public TestButton(IRenderer renderer) { IsVisible = true; + Renderer = renderer; } public new Rect Bounds @@ -399,7 +397,7 @@ namespace Avalonia.Controls.UnitTests public Size ClientSize => throw new NotImplementedException(); - public IRenderer Renderer { get; set; } + public IRenderer Renderer { get; } public double RenderScaling => throw new NotImplementedException(); diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index d63251c1f5..2644e7184a 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -6,6 +6,7 @@ using Avalonia.Input.Raw; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Platform; +using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.UnitTests; using Moq; @@ -20,7 +21,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); var target = new TestTopLevel(impl.Object); Assert.True(((ILogical)target).IsAttachedToLogicalTree); @@ -32,7 +33,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.Setup(x => x.ClientSize).Returns(new Size(123, 456)); var target = new TestTopLevel(impl.Object); @@ -46,7 +47,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.Setup(x => x.ClientSize).Returns(new Size(123, 456)); var target = new TestTopLevel(impl.Object); @@ -60,7 +61,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.Setup(x => x.ClientSize).Returns(new Size(123, 456)); var target = new TestTopLevel(impl.Object); @@ -76,7 +77,7 @@ namespace Avalonia.Controls.UnitTests using (UnitTestApplication.Start(services)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); var target = new TestTopLevel(impl.Object, Mock.Of()); @@ -91,7 +92,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.SetupProperty(x => x.Resized); impl.SetupGet(x => x.RenderScaling).Returns(1); @@ -117,7 +118,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.Setup(x => x.ClientSize).Returns(new Size(123, 456)); var target = new TestTopLevel(impl.Object); @@ -133,7 +134,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.SetupAllProperties(); impl.Setup(x => x.ClientSize).Returns(new Size(123, 456)); @@ -151,7 +152,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.SetupAllProperties(); bool raised = false; @@ -169,7 +170,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.SetupAllProperties(); var target = new TestTopLevel(impl.Object); @@ -200,7 +201,7 @@ namespace Avalonia.Controls.UnitTests using (UnitTestApplication.Start(services)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.SetupAllProperties(); var target = new TestTopLevel(impl.Object); @@ -222,7 +223,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.SetupAllProperties(); var target = new TestTopLevel(impl.Object); var child = new TestTopLevel(impl.Object); @@ -240,7 +241,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.SetupAllProperties(); var target = new TestTopLevel(impl.Object); var raised = false; @@ -257,7 +258,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.SetupAllProperties(); var layoutManager = new Mock(); @@ -274,7 +275,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.SetupGet(x => x.RenderScaling).Returns(1); var child = new Border { Classes = { "foo" } }; @@ -317,6 +318,14 @@ namespace Avalonia.Controls.UnitTests }.RegisterInNameScope(scope)); } + private static Mock CreateMockTopLevelImpl() + { + var renderer = new Mock(); + renderer.Setup(r => r.CreateRenderer(It.IsAny())) + .Returns(RendererMocks.CreateRenderer().Object); + return renderer; + } + private class TestTopLevel : TopLevel { private readonly ILayoutManager _layoutManager; diff --git a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs index f367112cc0..336aad79da 100644 --- a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs @@ -4,9 +4,7 @@ using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Platform; -using Avalonia.Styling; using Avalonia.UnitTests; -using Moq; using Xunit; using Factory = System.Func, Avalonia.Controls.Window, Avalonia.AvaloniaObject>; @@ -20,7 +18,7 @@ namespace Avalonia.Controls.UnitTests.Utils using (AvaloniaLocator.EnterScope()) { AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new WindowingPlatformMock()); + .Bind().ToConstant(new MockWindowingPlatform()); var gesture1 = new KeyGesture(Key.A, KeyModifiers.Control); var gesture2 = new KeyGesture(Key.B, KeyModifiers.Control); @@ -64,7 +62,7 @@ namespace Avalonia.Controls.UnitTests.Utils var commandResult = 0; var expectedParameter = 1; AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new WindowingPlatformMock()); + .Bind().ToConstant(new MockWindowingPlatform()); var gesture = new KeyGesture(Key.A, KeyModifiers.Control); @@ -106,7 +104,7 @@ namespace Avalonia.Controls.UnitTests.Utils var target = new KeyboardDevice(); var isExecuted = false; AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new WindowingPlatformMock()); + .Bind().ToConstant(new MockWindowingPlatform()); var gesture = new KeyGesture(Key.A, KeyModifiers.Control); @@ -146,7 +144,7 @@ namespace Avalonia.Controls.UnitTests.Utils var target = new KeyboardDevice(); var clickExecutedCount = 0; AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new WindowingPlatformMock()); + .Bind().ToConstant(new MockWindowingPlatform()); var gesture = new KeyGesture(Key.A, KeyModifiers.Control); @@ -199,7 +197,7 @@ namespace Avalonia.Controls.UnitTests.Utils var clickExecutedCount = 0; var commandExecutedCount = 0; AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new WindowingPlatformMock()); + .Bind().ToConstant(new MockWindowingPlatform()); var gesture = new KeyGesture(Key.A, KeyModifiers.Control); diff --git a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs index a10b1324d6..d65fa06183 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs @@ -22,7 +22,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockWindowBaseImpl(); var target = new TestWindowBase(impl.Object); target.Activate(); @@ -36,7 +36,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockWindowBaseImpl(); impl.SetupAllProperties(); bool raised = false; @@ -55,7 +55,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockWindowBaseImpl(); impl.SetupAllProperties(); bool raised = false; @@ -241,6 +241,14 @@ namespace Avalonia.Controls.UnitTests }.RegisterInNameScope(scope)); } + private static Mock CreateMockWindowBaseImpl() + { + var renderer = new Mock(); + renderer.Setup(r => r.CreateRenderer(It.IsAny())) + .Returns(RendererMocks.CreateRenderer().Object); + return renderer; + } + private class TestWindowBase : WindowBase { public bool IsClosed { get; private set; } diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 014174990e..cada2bfa6f 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -15,6 +15,8 @@ namespace Avalonia.Controls.UnitTests public void Setting_Title_Should_Set_Impl_Title() { var windowImpl = new Mock(); + windowImpl.Setup(r => r.CreateRenderer(It.IsAny())) + .Returns(RendererMocks.CreateRenderer().Object); var windowingPlatform = new MockWindowingPlatform(() => windowImpl.Object); using (UnitTestApplication.Start(new TestServices(windowingPlatform: windowingPlatform))) diff --git a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs deleted file mode 100644 index e8471d41fb..0000000000 --- a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using Moq; -using Avalonia.Platform; - -namespace Avalonia.Controls.UnitTests -{ - public class WindowingPlatformMock : IWindowingPlatform - { - private readonly Func _windowImpl; - private readonly Func _popupImpl; - - public WindowingPlatformMock(Func windowImpl = null, Func popupImpl = null ) - { - _windowImpl = windowImpl; - _popupImpl = popupImpl; - } - - public IWindowImpl CreateWindow() - { - return _windowImpl?.Invoke() ?? Mock.Of(x => x.RenderScaling == 1); - } - - public IWindowImpl CreateEmbeddableWindow() - { - throw new NotImplementedException(); - } - - public ITrayIconImpl CreateTrayIcon() - { - return null; - } - - public IPopupImpl CreatePopup() => _popupImpl?.Invoke() ?? Mock.Of(x => x.RenderScaling == 1); - } -} diff --git a/tests/Avalonia.UnitTests/TestTemplatedRoot.cs b/tests/Avalonia.UnitTests/TestTemplatedRoot.cs deleted file mode 100644 index 38ab3c3c5d..0000000000 --- a/tests/Avalonia.UnitTests/TestTemplatedRoot.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using Avalonia.Controls; -using Avalonia.Controls.Presenters; -using Avalonia.Controls.Templates; -using Avalonia.Layout; -using Avalonia.LogicalTree; -using Avalonia.Platform; -using Avalonia.Rendering; -using Avalonia.Styling; - -namespace Avalonia.UnitTests -{ - public class TestTemplatedRoot : ContentControl, ILayoutRoot, IRenderRoot, ILogicalRoot - { - private readonly NameScope _nameScope = new NameScope(); - - public TestTemplatedRoot() - { - LayoutManager = new LayoutManager(this); - Template = new FuncControlTemplate((x, scope) => new ContentPresenter - { - Name = "PART_ContentPresenter", - }.RegisterInNameScope(scope)); - } - - public Size ClientSize => new Size(100, 100); - - public Size MaxClientSize => Size.Infinity; - - public double LayoutScaling => 1; - - public ILayoutManager LayoutManager { get; set; } - - public double RenderScaling => 1; - - public IRenderTarget RenderTarget => null; - - public IRenderer Renderer => null; - - public IRenderTarget CreateRenderTarget() - { - throw new NotImplementedException(); - } - - public void Invalidate(Rect rect) - { - throw new NotImplementedException(); - } - - public Point PointToClient(PixelPoint p) => p.ToPoint(1); - - public PixelPoint PointToScreen(Point p) => PixelPoint.FromPoint(p, 1); - } -} From 1642129d0f7477ce0b02c38d8482f253525f2d41 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 6 Feb 2023 04:07:02 -0500 Subject: [PATCH 074/185] Implement simple PreloadDepsAssemblies --- .../DesignXamlLoader.cs | 71 +++++++++++++++++-- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs b/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs index 181883656c..690926a193 100644 --- a/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs +++ b/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs @@ -1,16 +1,79 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; +using System.Text.RegularExpressions; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.XamlIl; -namespace Avalonia.Designer.HostApp +namespace Avalonia.Designer.HostApp; + +class DesignXamlLoader : AvaloniaXamlLoader.IRuntimeXamlLoader { - class DesignXamlLoader : AvaloniaXamlLoader.IRuntimeXamlLoader + public object Load(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration) + { + PreloadDepsAssemblies(configuration.LocalAssembly ?? Assembly.GetEntryAssembly()); + + return AvaloniaXamlIlRuntimeCompiler.Load(document, configuration); + } + + private void PreloadDepsAssemblies(Assembly targetAssembly) { - public object Load(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration) + // Assemblies loaded in memory (e.g. single file) return empty string from Location. + // In these cases, don't try probing next to the assembly. + var assemblyLocation = targetAssembly.Location; + if (string.IsNullOrEmpty(assemblyLocation)) + { + return; + } + + var depsJsonFile = Path.ChangeExtension(assemblyLocation, ".deps.json"); + if (!File.Exists(depsJsonFile)) + { + return; + } + + using var stream = File.OpenRead(depsJsonFile); + + /* + We can't use any references in the Avalonia.Designer.HostApp. Including even json. + Ideally we would prefer Microsoft.Extensions.DependencyModel package, but can't use it here. + So, instead we need to fallback to some JSON parsing using pretty easy regex. + + Json part example: +"Avalonia.Xaml.Interactions/11.0.0-preview5": { + "dependencies": { + "Avalonia": "11.0.999", + "Avalonia.Xaml.Interactivity": "11.0.0-preview5" + }, + "runtime": { + "lib/net6.0/Avalonia.Xaml.Interactions.dll": { + "assemblyVersion": "11.0.0.0", + "fileVersion": "11.0.0.0" + } + } +}, + We want to extract "lib/net6.0/Avalonia.Xaml.Interactions.dll" from here. + No need to resolve real path of ref assemblies. + No need to handle special cases with .NET Framework and GAC. + */ + var text = new StreamReader(stream).ReadToEnd(); + var matches = Regex.Matches( text, """runtime"\s*:\s*{\s*"([^"]+)"""); + + foreach (Match match in matches) { - return AvaloniaXamlIlRuntimeCompiler.Load(document, configuration); + if (match.Groups[1] is { Success: true } g) + { + var assemblyName = Path.GetFileNameWithoutExtension(g.Value); + try + { + _ = Assembly.Load(new AssemblyName(assemblyName)); + } + catch + { + } + } } } } From e7a6d6fbc690b96af757199c98ff1f324f3d75ec Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 6 Feb 2023 12:00:48 +0100 Subject: [PATCH 075/185] Implement windowWillUseStandardFrame MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seems that `NSWindow.isZoomed` returns the wrong result unless you implement this? ¯\_(ツ)_/¯ --- native/Avalonia.Native/src/OSX/AvnWindow.mm | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index d3b7b4ede6..784072221d 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -223,6 +223,19 @@ } } +// From chromium: +// +// > The delegate or the window class should implement this method so that +// > -[NSWindow isZoomed] can be then determined by whether or not the current +// > window frame is equal to the zoomed frame. +// +// If we don't implement this, then isZoomed always returns true for a non- +// resizable window ¯\_(ツ)_/¯ +- (NSRect)windowWillUseStandardFrame:(NSWindow*)window + defaultFrame:(NSRect)newFrame { + return newFrame; +} + -(BOOL)canBecomeKeyWindow { if(_canBecomeKeyWindow) From 044a499db331b0d13e33d6427643bc1868470d30 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 6 Feb 2023 14:27:09 +0100 Subject: [PATCH 076/185] Added additional failing integration tests. --- tests/Avalonia.IntegrationTests.Appium/WindowTests.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index f3861ca267..ec24caa18c 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -63,9 +63,9 @@ namespace Avalonia.IntegrationTests.Appium [Theory] [MemberData(nameof(WindowStateData))] - public void WindowState(Size? size, ShowWindowMode mode, WindowState state) + public void WindowState(Size? size, ShowWindowMode mode, WindowState state, bool canResize) { - using var window = OpenWindow(size, mode, state: state); + using var window = OpenWindow(size, mode, state: state, canResize: canResize); try { @@ -253,10 +253,10 @@ namespace Avalonia.IntegrationTests.Appium return data; } - public static TheoryData WindowStateData() + public static TheoryData WindowStateData() { var sizes = new Size?[] { null, new Size(400, 300) }; - var data = new TheoryData(); + var data = new TheoryData(); foreach (var size in sizes) { @@ -274,7 +274,8 @@ namespace Avalonia.IntegrationTests.Appium mode != ShowWindowMode.NonOwned) continue; - data.Add(size, mode, state); + data.Add(size, mode, state, true); + data.Add(size, mode, state, false); } } } From 39fe9b17fb346ee5e63d6bfa508ed8d775eda355 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Mon, 6 Feb 2023 19:26:11 +0100 Subject: [PATCH 077/185] Nullability fixes for Avalonia.Base --- src/Avalonia.Base/Animation/KeySpline.cs | 11 ++- src/Avalonia.Base/AvaloniaObject.cs | 38 ++++------- .../Diagnostics/AvaloniaObjectExtensions.cs | 3 - src/Avalonia.Base/Input/DragEventArgs.cs | 18 ++--- src/Avalonia.Base/Input/KeyGesture.cs | 2 +- .../Input/KeyboardNavigationHandler.cs | 44 ++++++------ .../Input/Navigation/TabNavigation.cs | 48 ++++--------- .../LogicalTree/LogicalExtensions.cs | 6 +- src/Avalonia.Base/Media/Color.cs | 9 +-- src/Avalonia.Base/Media/DrawingContext.cs | 2 +- src/Avalonia.Base/Media/DrawingGroup.cs | 26 +++---- src/Avalonia.Base/Media/DrawingImage.cs | 6 +- src/Avalonia.Base/Media/FontFamily.cs | 4 +- .../Media/Fonts/FontFamilyKey.cs | 5 +- src/Avalonia.Base/Media/FormattedText.cs | 6 +- src/Avalonia.Base/Media/GeometryDrawing.cs | 6 +- src/Avalonia.Base/Media/GlyphRunDrawing.cs | 12 ++-- src/Avalonia.Base/Media/HslColor.cs | 2 +- src/Avalonia.Base/Media/HsvColor.cs | 2 +- src/Avalonia.Base/Media/IVisualBrush.cs | 3 +- .../Media/Immutable/ImmutableDashStyle.cs | 24 ++----- .../Media/Immutable/ImmutableVisualBrush.cs | 7 +- src/Avalonia.Base/Media/TextDecoration.cs | 14 ++-- src/Avalonia.Base/Media/VisualBrush.cs | 7 +- .../Platform/IDrawingContextImpl.cs | 4 +- .../Platform/Internal/AssemblyDescriptor.cs | 28 ++++---- .../Animations/CompositionAnimation.cs | 2 +- .../Animations/CompositionAnimationGroup.cs | 2 +- .../Animations/ImplicitAnimationCollection.cs | 2 +- .../Composition/CompositionDrawingSurface.cs | 3 +- .../Composition/CompositionObject.cs | 4 +- .../Composition/CompositionPropertySet.cs | 2 +- .../Drawing/CompositionDrawingContext.cs | 14 +++- .../Composition/Expressions/Expression.cs | 2 - .../ExpressionEvaluationContext.cs | 1 - .../Composition/Server/DrawingContextProxy.cs | 4 +- .../Rendering/ImmediateRenderer.cs | 6 +- .../SceneGraph/ExperimentalAcrylicNode.cs | 7 +- src/Avalonia.Base/Utilities/TypeUtilities.cs | 11 +-- src/Avalonia.Base/Utilities/WeakEvent.cs | 68 ++----------------- .../Utilities/WeakEventHandlerManager.cs | 45 ++++++------ src/Avalonia.Base/Visual.cs | 24 +++---- .../VisualTree/VisualExtensions.cs | 19 +++--- .../HeadlessPlatformRenderInterface.cs | 4 +- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 12 ++++ .../Avalonia.Direct2D1/Media/GlyphRunImpl.cs | 7 +- tests/Avalonia.UnitTests/MockGlyphRun.cs | 8 +-- 47 files changed, 226 insertions(+), 358 deletions(-) diff --git a/src/Avalonia.Base/Animation/KeySpline.cs b/src/Avalonia.Base/Animation/KeySpline.cs index 6ca5b2e759..ed6adb79b8 100644 --- a/src/Avalonia.Base/Animation/KeySpline.cs +++ b/src/Avalonia.Base/Animation/KeySpline.cs @@ -79,15 +79,12 @@ namespace Avalonia.Animation /// culture of the string /// Thrown if the string does not have 4 values /// A with the appropriate values set - public static KeySpline Parse(string value, CultureInfo culture) + public static KeySpline Parse(string value, CultureInfo? culture) { - if (culture is null) - culture = CultureInfo.InvariantCulture; + culture ??= CultureInfo.InvariantCulture; - using (var tokenizer = new StringTokenizer((string)value, culture, exceptionMessage: $"Invalid KeySpline string: \"{value}\".")) - { - return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble()); - } + using var tokenizer = new StringTokenizer(value, culture, exceptionMessage: $"Invalid KeySpline string: \"{value}\"."); + return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble()); } /// diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 1946d4ba5c..50a7a5c831 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -152,7 +152,7 @@ namespace Avalonia property = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); - _values?.ClearLocalValue(property); + _values.ClearLocalValue(property); } /// @@ -242,7 +242,14 @@ namespace Avalonia return registered.InvokeGetter(this); } - /// + /// + /// Gets an base value. + /// + /// The property. + /// + /// Gets the value of the property excluding animated values, otherwise . + /// Note that this method does not return property values that come from inherited or default values. + /// public Optional GetBaseValue(StyledProperty property) { _ = property ?? throw new ArgumentNullException(nameof(property)); @@ -261,7 +268,7 @@ namespace Avalonia VerifyAccess(); - return _values?.IsAnimating(property) ?? false; + return _values.IsAnimating(property); } /// @@ -279,7 +286,7 @@ namespace Avalonia VerifyAccess(); - return _values?.IsSet(property) ?? false; + return _values.IsSet(property); } /// @@ -515,14 +522,12 @@ namespace Avalonia /// The property. public void CoerceValue(AvaloniaProperty property) => _values.CoerceValue(property); - /// internal void AddInheritanceChild(AvaloniaObject child) { _inheritanceChildren ??= new List(); _inheritanceChildren.Add(child); } - - /// + internal void RemoveInheritanceChild(AvaloniaObject child) { _inheritanceChildren?.Remove(child); @@ -541,24 +546,11 @@ namespace Avalonia return new AvaloniaPropertyValue( property, GetValue(property), - BindingPriority.Unset, - "Local Value"); - } - else if (_values != null) - { - var result = _values.GetDiagnostic(property); - - if (result != null) - { - return result; - } + BindingPriority.LocalValue, + null); } - return new AvaloniaPropertyValue( - property, - GetValue(property), - BindingPriority.Unset, - "Unset"); + return _values.GetDiagnostic(property); } internal ValueStore GetValueStore() => _values; diff --git a/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs index d7b1f2e053..270cac95f2 100644 --- a/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs @@ -1,6 +1,3 @@ -using System; -using Avalonia.Data; - namespace Avalonia.Diagnostics { /// diff --git a/src/Avalonia.Base/Input/DragEventArgs.cs b/src/Avalonia.Base/Input/DragEventArgs.cs index 403dd6f23e..8d7cc2b9a1 100644 --- a/src/Avalonia.Base/Input/DragEventArgs.cs +++ b/src/Avalonia.Base/Input/DragEventArgs.cs @@ -1,36 +1,28 @@ using System; using Avalonia.Interactivity; using Avalonia.Metadata; -using Avalonia.VisualTree; namespace Avalonia.Input { public class DragEventArgs : RoutedEventArgs { - private Interactive _target; - private Point _targetLocation; + private readonly Interactive _target; + private readonly Point _targetLocation; public DragDropEffects DragEffects { get; set; } - public IDataObject Data { get; private set; } + public IDataObject Data { get; } - public KeyModifiers KeyModifiers { get; private set; } + public KeyModifiers KeyModifiers { get; } public Point GetPosition(Visual relativeTo) { - var point = new Point(0, 0); - if (relativeTo == null) { throw new ArgumentNullException(nameof(relativeTo)); } - if (_target != null) - { - point = _target.TranslatePoint(_targetLocation, relativeTo) ?? point; - } - - return point; + return _target.TranslatePoint(_targetLocation, relativeTo) ?? new Point(0, 0); } [Unstable] diff --git a/src/Avalonia.Base/Input/KeyGesture.cs b/src/Avalonia.Base/Input/KeyGesture.cs index c6618fd550..9ee8ae9711 100644 --- a/src/Avalonia.Base/Input/KeyGesture.cs +++ b/src/Avalonia.Base/Input/KeyGesture.cs @@ -136,7 +136,7 @@ namespace Avalonia.Input return StringBuilderCache.GetStringAndRelease(s); } - public bool Matches(KeyEventArgs keyEvent) => + public bool Matches(KeyEventArgs? keyEvent) => keyEvent != null && keyEvent.KeyModifiers == KeyModifiers && ResolveNumPadOperationKey(keyEvent.Key) == ResolveNumPadOperationKey(Key); diff --git a/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs b/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs index b05d8f30bb..ba909de60f 100644 --- a/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Avalonia.Input.Navigation; using Avalonia.VisualTree; @@ -51,7 +50,7 @@ namespace Avalonia.Input // If there's a custom keyboard navigation handler as an ancestor, use that. var custom = (element as Visual)?.FindAncestorOfType(true); - if (custom is object && HandlePreCustomNavigation(custom, element, direction, out var ce)) + if (custom is not null && HandlePreCustomNavigation(custom, element, direction, out var ce)) return ce; var result = direction switch @@ -117,32 +116,27 @@ namespace Avalonia.Input NavigationDirection direction, [NotNullWhen(true)] out IInputElement? result) { - if (customHandler != null) + var (handled, next) = customHandler.GetNext(element, direction); + + if (handled) { - var (handled, next) = customHandler.GetNext(element, direction); + if (next is not null) + { + result = next; + return true; + } - if (handled) + var r = direction switch { - if (next != null) - { - result = next; - return true; - } - else if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous) - { - var r = direction switch - { - NavigationDirection.Next => TabNavigation.GetNextTabOutside(customHandler), - NavigationDirection.Previous => TabNavigation.GetPrevTabOutside(customHandler), - _ => throw new NotSupportedException(), - }; - - if (r is object) - { - result = r; - return true; - } - } + NavigationDirection.Next => TabNavigation.GetNextTabOutside(customHandler), + NavigationDirection.Previous => TabNavigation.GetPrevTabOutside(customHandler), + _ => null + }; + + if (r is not null) + { + result = r; + return true; } } diff --git a/src/Avalonia.Base/Input/Navigation/TabNavigation.cs b/src/Avalonia.Base/Input/Navigation/TabNavigation.cs index d218867cf2..c460ecf3b3 100644 --- a/src/Avalonia.Base/Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Base/Input/Navigation/TabNavigation.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; using Avalonia.VisualTree; namespace Avalonia.Input.Navigation @@ -54,8 +52,7 @@ namespace Avalonia.Input.Navigation // Avoid the endless loop here for Cycle groups if (loopStartElement == nextTabElement) break; - if (loopStartElement == null) - loopStartElement = nextTabElement; + loopStartElement ??= nextTabElement; var firstTabElementInside = GetNextTab(null, nextTabElement, true); if (firstTabElementInside != null) @@ -80,12 +77,9 @@ namespace Avalonia.Input.Navigation public static IInputElement? GetNextTabOutside(ICustomKeyboardNavigation e) { - if (e is IInputElement container) + if (e is IInputElement container && GetLastInTree(container) is { } last) { - var last = GetLastInTree(container); - - if (last is object) - return GetNextTab(last, false); + return GetNextTab(last, false); } return null; @@ -93,11 +87,8 @@ namespace Avalonia.Input.Navigation public static IInputElement? GetPrevTab(IInputElement? e, IInputElement? container, bool goDownOnly) { - if (e is null && container is null) - throw new InvalidOperationException("Either 'e' or 'container' must be non-null."); - - if (container is null) - container = GetGroupParent(e!); + container ??= + GetGroupParent(e ?? throw new InvalidOperationException("Either 'e' or 'container' must be non-null.")); KeyboardNavigationMode tabbingType = GetKeyNavigationMode(container); @@ -163,8 +154,7 @@ namespace Avalonia.Input.Navigation // Avoid the endless loop here if (loopStartElement == nextTabElement) break; - if (loopStartElement == null) - loopStartElement = nextTabElement; + loopStartElement ??= nextTabElement; // At this point nextTabElement is TabGroup var lastTabElementInside = GetPrevTab(null, nextTabElement, true); @@ -189,22 +179,18 @@ namespace Avalonia.Input.Navigation public static IInputElement? GetPrevTabOutside(ICustomKeyboardNavigation e) { - if (e is IInputElement container) + if (e is IInputElement container && GetFirstChild(container) is { } first) { - var first = GetFirstChild(container); - - if (first is object) - return GetPrevTab(first, null, false); + return GetPrevTab(first, null, false); } return null; } - private static IInputElement? FocusedElement(IInputElement e) + private static IInputElement? FocusedElement(IInputElement? e) { - var iie = e; // Focus delegation is enabled only if keyboard focus is outside the container - if (iie != null && !iie.IsKeyboardFocusWithin) + if (e != null && !e.IsKeyboardFocusWithin) { var focusedElement = (FocusManager.Instance as FocusManager)?.GetFocusedElement(e); if (focusedElement != null) @@ -229,13 +215,11 @@ namespace Avalonia.Input.Navigation private static IInputElement? GetFirstChild(IInputElement e) { // If the element has a FocusedElement it should be its first child - if (FocusedElement(e) is IInputElement focusedElement) + if (FocusedElement(e) is { } focusedElement) return focusedElement; // Return the first visible element. - var uiElement = e as InputElement; - - if (uiElement is null || IsVisibleAndEnabled(uiElement)) + if (e is not InputElement uiElement || IsVisibleAndEnabled(uiElement)) { if (e is Visual elementAsVisual) { @@ -265,7 +249,7 @@ namespace Avalonia.Input.Navigation private static IInputElement? GetLastChild(IInputElement e) { // If the element has a FocusedElement it should be its last child - if (FocusedElement(e) is IInputElement focusedElement) + if (FocusedElement(e) is { } focusedElement) return focusedElement; // Return the last visible element. @@ -273,9 +257,7 @@ namespace Avalonia.Input.Navigation if (uiElement == null || IsVisibleAndEnabled(uiElement)) { - var elementAsVisual = e as Visual; - - if (elementAsVisual != null) + if (e is Visual elementAsVisual) { var children = elementAsVisual.VisualChildren; var count = children.Count; @@ -322,7 +304,7 @@ namespace Avalonia.Input.Navigation return firstTabElement; } - private static IInputElement? GetLastInTree(IInputElement container) + private static IInputElement GetLastInTree(IInputElement container) { IInputElement? result; IInputElement? c = container; diff --git a/src/Avalonia.Base/LogicalTree/LogicalExtensions.cs b/src/Avalonia.Base/LogicalTree/LogicalExtensions.cs index 74720c0a77..9ab7da3ff7 100644 --- a/src/Avalonia.Base/LogicalTree/LogicalExtensions.cs +++ b/src/Avalonia.Base/LogicalTree/LogicalExtensions.cs @@ -48,7 +48,7 @@ namespace Avalonia.LogicalTree /// The logical. /// If given logical should be included in search. /// First ancestor of given type. - public static T? FindLogicalAncestorOfType(this ILogical logical, bool includeSelf = false) where T : class + public static T? FindLogicalAncestorOfType(this ILogical? logical, bool includeSelf = false) where T : class { if (logical is null) { @@ -120,7 +120,7 @@ namespace Avalonia.LogicalTree /// The logical. /// If given logical should be included in search. /// First descendant of given type. - public static T? FindLogicalDescendantOfType(this ILogical logical, bool includeSelf = false) where T : class + public static T? FindLogicalDescendantOfType(this ILogical? logical, bool includeSelf = false) where T : class { if (logical is null) { @@ -185,7 +185,7 @@ namespace Avalonia.LogicalTree /// True if is an ancestor of ; /// otherwise false. /// - public static bool IsLogicalAncestorOf(this ILogical logical, ILogical target) + public static bool IsLogicalAncestorOf(this ILogical? logical, ILogical? target) { var current = target?.LogicalParent; diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs index 5470a735b3..ab89177295 100644 --- a/src/Avalonia.Base/Media/Color.cs +++ b/src/Avalonia.Base/Media/Color.cs @@ -147,16 +147,11 @@ namespace Avalonia.Media /// The color string. /// The parsed color /// The status of the operation. - public static bool TryParse(string s, out Color color) + public static bool TryParse(string? s, out Color color) { color = default; - if (s is null) - { - return false; - } - - if (s.Length == 0) + if (string.IsNullOrEmpty(s)) { return false; } diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs index d295111d72..622181dba0 100644 --- a/src/Avalonia.Base/Media/DrawingContext.cs +++ b/src/Avalonia.Base/Media/DrawingContext.cs @@ -240,7 +240,7 @@ namespace Avalonia.Media /// /// The foreground brush. /// The glyph run. - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) { _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun)); diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index 481329c20c..b7abda2c61 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -13,14 +13,14 @@ namespace Avalonia.Media public static readonly StyledProperty OpacityProperty = AvaloniaProperty.Register(nameof(Opacity), 1); - public static readonly StyledProperty TransformProperty = - AvaloniaProperty.Register(nameof(Transform)); + public static readonly StyledProperty TransformProperty = + AvaloniaProperty.Register(nameof(Transform)); - public static readonly StyledProperty ClipGeometryProperty = - AvaloniaProperty.Register(nameof(ClipGeometry)); + public static readonly StyledProperty ClipGeometryProperty = + AvaloniaProperty.Register(nameof(ClipGeometry)); - public static readonly StyledProperty OpacityMaskProperty = - AvaloniaProperty.Register(nameof(OpacityMask)); + public static readonly StyledProperty OpacityMaskProperty = + AvaloniaProperty.Register(nameof(OpacityMask)); public static readonly DirectProperty ChildrenProperty = AvaloniaProperty.RegisterDirect( @@ -36,19 +36,19 @@ namespace Avalonia.Media set => SetValue(OpacityProperty, value); } - public Transform Transform + public Transform? Transform { get => GetValue(TransformProperty); set => SetValue(TransformProperty, value); } - public Geometry ClipGeometry + public Geometry? ClipGeometry { get => GetValue(ClipGeometryProperty); set => SetValue(ClipGeometryProperty, value); } - public IBrush OpacityMask + public IBrush? OpacityMask { get => GetValue(OpacityMaskProperty); set => SetValue(OpacityMaskProperty, value); @@ -159,7 +159,7 @@ namespace Avalonia.Media public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) { - if (((brush == null) && (pen == null)) || (geometry == null)) + if ((brush == null) && (pen == null)) { return; } @@ -167,9 +167,9 @@ namespace Avalonia.Media AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); } - public void DrawGlyphRun(IBrush foreground, IRef glyphRun) + public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) { - if (foreground == null || glyphRun == null) + if (foreground == null) { return; } @@ -184,7 +184,7 @@ namespace Avalonia.Media AddDrawing(glyphRunDrawing); } - public void DrawLine(IPen pen, Point p1, Point p2) + public void DrawLine(IPen? pen, Point p1, Point p2) { if (pen == null) { diff --git a/src/Avalonia.Base/Media/DrawingImage.cs b/src/Avalonia.Base/Media/DrawingImage.cs index 38ddbdfaed..1b22a1ee69 100644 --- a/src/Avalonia.Base/Media/DrawingImage.cs +++ b/src/Avalonia.Base/Media/DrawingImage.cs @@ -20,8 +20,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty DrawingProperty = - AvaloniaProperty.Register(nameof(Drawing)); + public static readonly StyledProperty DrawingProperty = + AvaloniaProperty.Register(nameof(Drawing)); /// public event EventHandler? Invalidated; @@ -30,7 +30,7 @@ namespace Avalonia.Media /// Gets or sets the drawing content. /// [Content] - public Drawing Drawing + public Drawing? Drawing { get => GetValue(DrawingProperty); set => SetValue(DrawingProperty, value); diff --git a/src/Avalonia.Base/Media/FontFamily.cs b/src/Avalonia.Base/Media/FontFamily.cs index da84861668..f4406bd010 100644 --- a/src/Avalonia.Base/Media/FontFamily.cs +++ b/src/Avalonia.Base/Media/FontFamily.cs @@ -119,7 +119,7 @@ namespace Avalonia.Media case 2: { - var source = segments[0].StartsWith("/") + var source = segments[0].StartsWith("/", StringComparison.Ordinal) ? new Uri(segments[0], UriKind.Relative) : new Uri(segments[0], UriKind.RelativeOrAbsolute); @@ -188,7 +188,7 @@ namespace Avalonia.Media { unchecked { - return ((FamilyNames != null ? FamilyNames.GetHashCode() : 0) * 397) ^ (Key != null ? Key.GetHashCode() : 0); + return (FamilyNames.GetHashCode() * 397) ^ (Key is not null ? Key.GetHashCode() : 0); } } diff --git a/src/Avalonia.Base/Media/Fonts/FontFamilyKey.cs b/src/Avalonia.Base/Media/Fonts/FontFamilyKey.cs index f607c67fed..12bb7e77e7 100644 --- a/src/Avalonia.Base/Media/Fonts/FontFamilyKey.cs +++ b/src/Avalonia.Base/Media/Fonts/FontFamilyKey.cs @@ -41,10 +41,7 @@ namespace Avalonia.Media.Fonts { var hash = (int)2166136261; - if (Source != null) - { - hash = (hash * 16777619) ^ Source.GetHashCode(); - } + hash = (hash * 16777619) ^ Source.GetHashCode(); if (BaseUri != null) { diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs index 28757b1a1d..3b63a98720 100644 --- a/src/Avalonia.Base/Media/FormattedText.cs +++ b/src/Avalonia.Base/Media/FormattedText.cs @@ -1354,7 +1354,7 @@ namespace Avalonia.Media { var highlightBounds = currentLine.GetTextBounds(x0,x1 - x0); - if (highlightBounds != null) + if (highlightBounds.Count > 0) { foreach (var bound in highlightBounds) { @@ -1365,7 +1365,7 @@ namespace Avalonia.Media // Convert logical units (which extend leftward from the right edge // of the paragraph) to physical units. // - // Note that since rect is in logical units, rect.Right corresponds to + // Note that since rect is in logical units, rect.Right corresponds to // the visual *left* edge of the rectangle in the RTL case. Specifically, // is the distance leftward from the right edge of the formatting rectangle // whose width is the paragraph width passed to FormatLine. @@ -1384,7 +1384,7 @@ namespace Avalonia.Media else { accumulatedBounds = Geometry.Combine(accumulatedBounds, rectangleGeometry, GeometryCombineMode.Union); - } + } } } } diff --git a/src/Avalonia.Base/Media/GeometryDrawing.cs b/src/Avalonia.Base/Media/GeometryDrawing.cs index 26cc2c3cab..ac2dce1e42 100644 --- a/src/Avalonia.Base/Media/GeometryDrawing.cs +++ b/src/Avalonia.Base/Media/GeometryDrawing.cs @@ -15,8 +15,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty GeometryProperty = - AvaloniaProperty.Register(nameof(Geometry)); + public static readonly StyledProperty GeometryProperty = + AvaloniaProperty.Register(nameof(Geometry)); /// /// Defines the property. @@ -34,7 +34,7 @@ namespace Avalonia.Media /// Gets or sets the that describes the shape of this . /// [Content] - public Geometry Geometry + public Geometry? Geometry { get => GetValue(GeometryProperty); set => SetValue(GeometryProperty, value); diff --git a/src/Avalonia.Base/Media/GlyphRunDrawing.cs b/src/Avalonia.Base/Media/GlyphRunDrawing.cs index 242b9913fa..06d92fd81c 100644 --- a/src/Avalonia.Base/Media/GlyphRunDrawing.cs +++ b/src/Avalonia.Base/Media/GlyphRunDrawing.cs @@ -2,19 +2,19 @@ { public class GlyphRunDrawing : Drawing { - public static readonly StyledProperty ForegroundProperty = - AvaloniaProperty.Register(nameof(Foreground)); + public static readonly StyledProperty ForegroundProperty = + AvaloniaProperty.Register(nameof(Foreground)); - public static readonly StyledProperty GlyphRunProperty = - AvaloniaProperty.Register(nameof(GlyphRun)); + public static readonly StyledProperty GlyphRunProperty = + AvaloniaProperty.Register(nameof(GlyphRun)); - public IBrush Foreground + public IBrush? Foreground { get => GetValue(ForegroundProperty); set => SetValue(ForegroundProperty, value); } - public GlyphRun GlyphRun + public GlyphRun? GlyphRun { get => GetValue(GlyphRunProperty); set => SetValue(GlyphRunProperty, value); diff --git a/src/Avalonia.Base/Media/HslColor.cs b/src/Avalonia.Base/Media/HslColor.cs index 425a3138c3..b4bf6fd217 100644 --- a/src/Avalonia.Base/Media/HslColor.cs +++ b/src/Avalonia.Base/Media/HslColor.cs @@ -254,7 +254,7 @@ namespace Avalonia.Media /// The HSL color string to parse. /// The parsed . /// True if parsing was successful; otherwise, false. - public static bool TryParse(string s, out HslColor hslColor) + public static bool TryParse(string? s, out HslColor hslColor) { bool prefixMatched = false; diff --git a/src/Avalonia.Base/Media/HsvColor.cs b/src/Avalonia.Base/Media/HsvColor.cs index 9f95b31518..f97457c54d 100644 --- a/src/Avalonia.Base/Media/HsvColor.cs +++ b/src/Avalonia.Base/Media/HsvColor.cs @@ -254,7 +254,7 @@ namespace Avalonia.Media /// The HSV color string to parse. /// The parsed . /// True if parsing was successful; otherwise, false. - public static bool TryParse(string s, out HsvColor hsvColor) + public static bool TryParse(string? s, out HsvColor hsvColor) { bool prefixMatched = false; diff --git a/src/Avalonia.Base/Media/IVisualBrush.cs b/src/Avalonia.Base/Media/IVisualBrush.cs index 6662613ff4..a7d3e4da10 100644 --- a/src/Avalonia.Base/Media/IVisualBrush.cs +++ b/src/Avalonia.Base/Media/IVisualBrush.cs @@ -1,5 +1,4 @@ using Avalonia.Metadata; -using Avalonia.VisualTree; namespace Avalonia.Media { @@ -12,6 +11,6 @@ namespace Avalonia.Media /// /// Gets the visual to draw. /// - Visual Visual { get; } + Visual? Visual { get; } } } diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs b/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs index 1f53f06955..6dff006045 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs @@ -39,17 +39,8 @@ namespace Avalonia.Media.Immutable { return true; } - else if (other is null) - { - return false; - } - if (Offset != other.Offset) - { - return false; - } - - return SequenceEqual(Dashes, other.Dashes); + return other is not null && Offset == other.Offset && SequenceEqual(_dashes, other.Dashes); } /// @@ -58,30 +49,27 @@ namespace Avalonia.Media.Immutable var hashCode = 717868523; hashCode = hashCode * -1521134295 + Offset.GetHashCode(); - if (_dashes != null) + foreach (var i in _dashes) { - foreach (var i in _dashes) - { - hashCode = hashCode * -1521134295 + i.GetHashCode(); - } + hashCode = hashCode * -1521134295 + i.GetHashCode(); } return hashCode; } - private static bool SequenceEqual(IReadOnlyList left, IReadOnlyList? right) + private static bool SequenceEqual(double[] left, IReadOnlyList? right) { if (ReferenceEquals(left, right)) { return true; } - if (left == null || right == null || left.Count != right.Count) + if (right is null || left.Length != right.Count) { return false; } - for (var c = 0; c < left.Count; c++) + for (var c = 0; c < left.Length; c++) { if (left[c] != right[c]) { diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs index 9b443391c5..0b625080e3 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs @@ -1,5 +1,4 @@ using Avalonia.Media.Imaging; -using Avalonia.VisualTree; namespace Avalonia.Media.Immutable { @@ -31,11 +30,11 @@ namespace Avalonia.Media.Immutable RelativeRect? destinationRect = null, double opacity = 1, ImmutableTransform? transform = null, - RelativePoint transformOrigin = new RelativePoint(), + RelativePoint transformOrigin = default, RelativeRect? sourceRect = null, Stretch stretch = Stretch.Uniform, TileMode tileMode = TileMode.None, - Imaging.BitmapInterpolationMode bitmapInterpolationMode = Imaging.BitmapInterpolationMode.Default) + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) : base( alignmentX, alignmentY, @@ -62,6 +61,6 @@ namespace Avalonia.Media.Immutable } /// - public Visual Visual { get; } + public Visual? Visual { get; } } } diff --git a/src/Avalonia.Base/Media/TextDecoration.cs b/src/Avalonia.Base/Media/TextDecoration.cs index dc9e5cb907..b74b7df9c5 100644 --- a/src/Avalonia.Base/Media/TextDecoration.cs +++ b/src/Avalonia.Base/Media/TextDecoration.cs @@ -22,8 +22,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty StrokeProperty = - AvaloniaProperty.Register(nameof(Stroke)); + public static readonly StyledProperty StrokeProperty = + AvaloniaProperty.Register(nameof(Stroke)); /// /// Defines the property. @@ -34,8 +34,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty> StrokeDashArrayProperty = - AvaloniaProperty.Register>(nameof(StrokeDashArray)); + public static readonly StyledProperty?> StrokeDashArrayProperty = + AvaloniaProperty.Register?>(nameof(StrokeDashArray)); /// /// Defines the property. @@ -82,7 +82,7 @@ namespace Avalonia.Media /// /// Gets or sets the that specifies how the is painted. /// - public IBrush Stroke + public IBrush? Stroke { get { return GetValue(StrokeProperty); } set { SetValue(StrokeProperty, value); } @@ -101,7 +101,7 @@ namespace Avalonia.Media /// Gets or sets a collection of values that indicate the pattern of dashes and gaps /// that is used to draw the . /// - public AvaloniaList StrokeDashArray + public AvaloniaList? StrokeDashArray { get { return GetValue(StrokeDashArrayProperty); } set { SetValue(StrokeDashArrayProperty, value); } @@ -220,7 +220,7 @@ namespace Avalonia.Media var intersections = glyphRun.PlatformImpl.Item.GetIntersections((float)(thickness * 0.5d - offsetY), (float)(thickness * 1.5d - offsetY)); - if (intersections != null && intersections.Count > 0) + if (intersections.Count > 0) { var last = baselineOrigin.X; var finalPos = last + glyphRun.Size.Width; diff --git a/src/Avalonia.Base/Media/VisualBrush.cs b/src/Avalonia.Base/Media/VisualBrush.cs index 1261d233ac..2be3e9a94e 100644 --- a/src/Avalonia.Base/Media/VisualBrush.cs +++ b/src/Avalonia.Base/Media/VisualBrush.cs @@ -1,5 +1,4 @@ using Avalonia.Media.Immutable; -using Avalonia.VisualTree; namespace Avalonia.Media { @@ -11,8 +10,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty VisualProperty = - AvaloniaProperty.Register(nameof(Visual)); + public static readonly StyledProperty VisualProperty = + AvaloniaProperty.Register(nameof(Visual)); static VisualBrush() { @@ -38,7 +37,7 @@ namespace Avalonia.Media /// /// Gets or sets the visual to draw. /// - public Visual Visual + public Visual? Visual { get { return GetValue(VisualProperty); } set { SetValue(VisualProperty, value); } diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index c05c04c22e..8509067cd0 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -49,7 +49,7 @@ namespace Avalonia.Platform /// The stroke pen. /// The first point of the line. /// The second point of the line. - void DrawLine(IPen pen, Point p1, Point p2); + void DrawLine(IPen? pen, Point p1, Point p2); /// /// Draws a geometry. @@ -91,7 +91,7 @@ namespace Avalonia.Platform /// /// The foreground. /// The glyph run. - void DrawGlyphRun(IBrush foreground, IRef glyphRun); + void DrawGlyphRun(IBrush? foreground, IRef glyphRun); /// /// Creates a new that can be used as a render layer diff --git a/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs b/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs index 467cd530fc..d1a803fefb 100644 --- a/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs +++ b/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs @@ -19,25 +19,20 @@ internal class AssemblyDescriptor : IAssemblyDescriptor public AssemblyDescriptor(Assembly assembly) { Assembly = assembly; + Resources = assembly.GetManifestResourceNames() + .ToDictionary(n => n, n => (IAssetDescriptor)new AssemblyResourceDescriptor(assembly, n)); + Name = assembly.GetName().Name; - if (assembly != null) + using var resources = assembly.GetManifestResourceStream(Constants.AvaloniaResourceName); + if (resources != null) { - Resources = assembly.GetManifestResourceNames() - .ToDictionary(n => n, n => (IAssetDescriptor)new AssemblyResourceDescriptor(assembly, n)); - Name = assembly.GetName().Name; - using (var resources = assembly.GetManifestResourceStream(Constants.AvaloniaResourceName)) - { - if (resources != null) - { - Resources.Remove(Constants.AvaloniaResourceName); + Resources.Remove(Constants.AvaloniaResourceName); - var indexLength = new BinaryReader(resources).ReadInt32(); - var index = AvaloniaResourcesIndexReaderWriter.ReadIndex(new SlicedStream(resources, 4, indexLength)); - var baseOffset = indexLength + 4; - AvaloniaResources = index.ToDictionary(r => GetPathRooted(r), r => (IAssetDescriptor) - new AvaloniaResourceDescriptor(assembly, baseOffset + r.Offset, r.Size)); - } - } + var indexLength = new BinaryReader(resources).ReadInt32(); + var index = AvaloniaResourcesIndexReaderWriter.ReadIndex(new SlicedStream(resources, 4, indexLength)); + var baseOffset = indexLength + 4; + AvaloniaResources = index.ToDictionary(GetPathRooted, r => (IAssetDescriptor) + new AvaloniaResourceDescriptor(assembly, baseOffset + r.Offset, r.Size)); } } @@ -45,6 +40,7 @@ internal class AssemblyDescriptor : IAssemblyDescriptor public Dictionary? Resources { get; } public Dictionary? AvaloniaResources { get; } public string? Name { get; } + private static string GetPathRooted(AvaloniaResourcesIndexEntry r) => r.Path![0] == '/' ? r.Path : '/' + r.Path; } diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs index a6db4330a3..455e9ebb5f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs @@ -23,7 +23,7 @@ namespace Avalonia.Rendering.Composition.Animations public abstract class CompositionAnimation : CompositionObject, ICompositionAnimationBase { private readonly CompositionPropertySet _propertySet; - internal CompositionAnimation(Compositor compositor) : base(compositor, null!) + internal CompositionAnimation(Compositor compositor) : base(compositor, null) { _propertySet = new CompositionPropertySet(compositor); } diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs index bad3991f43..1500e88abe 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.Composition.Animations public void Remove(CompositionAnimation value) => Animations.Remove(value); public void RemoveAll() => Animations.Clear(); - public CompositionAnimationGroup(Compositor compositor) : base(compositor, null!) + public CompositionAnimationGroup(Compositor compositor) : base(compositor, null) { } } diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs index 72be4edd07..d9adf261f8 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs @@ -23,7 +23,7 @@ namespace Avalonia.Rendering.Composition.Animations { private Dictionary _inner = new Dictionary(); private IDictionary _innerface; - internal ImplicitAnimationCollection(Compositor compositor) : base(compositor, null!) + internal ImplicitAnimationCollection(Compositor compositor) : base(compositor, null) { _innerface = _inner; } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs index ab4329df62..bfe70d593d 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs @@ -1,4 +1,3 @@ -using System; using System.Threading.Tasks; using Avalonia.Rendering.Composition.Server; using Avalonia.Threading; @@ -7,7 +6,7 @@ namespace Avalonia.Rendering.Composition; public class CompositionDrawingSurface : CompositionSurface { - internal new ServerCompositionDrawingSurface Server => (ServerCompositionDrawingSurface)base.Server; + internal new ServerCompositionDrawingSurface Server => (ServerCompositionDrawingSurface)base.Server!; internal CompositionDrawingSurface(Compositor compositor) : base(compositor, new ServerCompositionDrawingSurface(compositor.Server)) { } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs index 50332926ad..8c21b534db 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs @@ -22,7 +22,7 @@ namespace Avalonia.Rendering.Composition public ImplicitAnimationCollection? ImplicitAnimations { get; set; } private protected InlineDictionary PendingAnimations; - internal CompositionObject(Compositor compositor, ServerObject server) + internal CompositionObject(Compositor compositor, ServerObject? server) { Compositor = compositor; Server = server; @@ -32,7 +32,7 @@ namespace Avalonia.Rendering.Composition /// The associated Compositor /// public Compositor Compositor { get; } - internal ServerObject Server { get; } + internal ServerObject? Server { get; } public bool IsDisposed { get; private set; } private bool _registeredForSerialization; diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs index 7d794af9a2..efd89951bb 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs @@ -23,7 +23,7 @@ namespace Avalonia.Rendering.Composition private readonly Dictionary _variants = new Dictionary(); private readonly Dictionary _objects = new Dictionary(); - internal CompositionPropertySet(Compositor compositor) : base(compositor, null!) + internal CompositionPropertySet(Compositor compositor) : base(compositor, null) { } diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs index 05488a558f..b75d080cfd 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -88,8 +88,13 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } /// - public void DrawLine(IPen pen, Point p1, Point p2) + public void DrawLine(IPen? pen, Point p1, Point p2) { + if (pen is null) + { + return; + } + var next = NextDrawAs(); if (next == null || !next.Item.Equals(Transform, pen, p1, p2)) @@ -159,8 +164,13 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW public object? GetFeature(Type t) => null; /// - public void DrawGlyphRun(IBrush foreground, IRef glyphRun) + public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) { + if (foreground is null) + { + return; + } + var next = NextDrawAs(); if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs index 560ee05c10..b15da5d05d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs @@ -165,8 +165,6 @@ namespace Avalonia.Rendering.Composition.Expressions public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) { - if (context.ForeignFunctionInterface == null) - return default; var args = new List(); foreach (var expr in Parameters) args.Add(expr.Evaluate(ref context)); diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs index 9086c59aad..f268364b54 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using Avalonia.Rendering.Composition.Server; // Special license applies License.md diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index c58beebe7f..50df8bd32b 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -66,7 +66,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont _impl.DrawBitmap(source, opacityMask, opacityMaskRect, destRect); } - public void DrawLine(IPen pen, Point p1, Point p2) + public void DrawLine(IPen? pen, Point p1, Point p2) { _impl.DrawLine(pen, p1, p2); } @@ -86,7 +86,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont _impl.DrawEllipse(brush, pen, rect); } - public void DrawGlyphRun(IBrush foreground, IRef glyphRun) + public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) { _impl.DrawGlyphRun(foreground, glyphRun); } diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index c67ac7057d..8e5dc38317 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -48,8 +48,10 @@ namespace Avalonia.Rendering /// void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) { - var visual = brush.Visual; - Render(new DrawingContext(context), visual, visual.Bounds); + if (brush.Visual is { } visual) + { + Render(new DrawingContext(context), visual, visual.Bounds); + } } internal static void Render(Visual visual, DrawingContext context, bool updateTransformedBounds) diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs index 12b67105e9..82f8fc2d56 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs @@ -80,11 +80,8 @@ namespace Avalonia.Rendering.SceneGraph { p *= Transform.Invert(); - if (Material != null) - { - var rect = Rect.Rect; - return rect.ContainsExclusive(p); - } + var rect = Rect.Rect; + return rect.ContainsExclusive(p); } return false; diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index 3c44dd63ce..fafafabd82 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -212,7 +212,7 @@ namespace Avalonia.Utilities var toTypeConverter = TypeDescriptor.GetConverter(toUnderl); - if (toTypeConverter.CanConvertFrom(from) == true) + if (toTypeConverter.CanConvertFrom(from)) { result = toTypeConverter.ConvertFrom(null, culture, value); return true; @@ -220,7 +220,7 @@ namespace Avalonia.Utilities var fromTypeConverter = TypeDescriptor.GetConverter(from); - if (fromTypeConverter.CanConvertTo(toUnderl) == true) + if (fromTypeConverter.CanConvertTo(toUnderl)) { result = fromTypeConverter.ConvertTo(null, culture, value, toUnderl); return true; @@ -329,7 +329,7 @@ namespace Avalonia.Utilities } [RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConvertionRequiresUnreferencedCodeMessage)] - public static T ConvertImplicit(object value) + public static T ConvertImplicit(object? value) { if (TryConvertImplicit(typeof(T), value, out var result)) { @@ -369,11 +369,6 @@ namespace Avalonia.Utilities /// public static bool IsNumeric(Type type) { - if (type == null) - { - return false; - } - var underlyingType = Nullable.GetUnderlyingType(type); if (underlyingType != null) diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index e72606bf70..237a491615 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; using System.Runtime.CompilerServices; using Avalonia.Threading; @@ -15,7 +11,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event { private readonly Func, Action> _subscribe; - readonly ConditionalWeakTable _subscriptions = new(); + private readonly ConditionalWeakTable _subscriptions = new(); internal WeakEvent( Action> subscribe, @@ -51,56 +47,6 @@ public class WeakEvent : WeakEvent where TEventArgs : Event private readonly WeakEvent _ev; private readonly TSender _target; private readonly Action _compact; - - 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 true; - if (_reference.TryGetTarget(out _)) - return false; - _reference = null; - return true; - } - } - - 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 readonly Action _unsubscribe; private readonly WeakHashList> _list = new(); private bool _compactScheduled; @@ -114,7 +60,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event _unsubscribe = ev._subscribe(target, OnEvent); } - void Destroy() + private void Destroy() { if(_destroyed) return; @@ -134,15 +80,15 @@ public class WeakEvent : WeakEvent where TEventArgs : Event ScheduleCompact(); } - void ScheduleCompact() + private void ScheduleCompact() { if(_compactScheduled || _destroyed) return; _compactScheduled = true; Dispatcher.UIThread.Post(_compact, DispatcherPriority.Background); } - - void Compact() + + private void Compact() { if(!_compactScheduled) return; @@ -152,7 +98,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event Destroy(); } - void OnEvent(object? sender, TEventArgs eventArgs) + private void OnEvent(object? sender, TEventArgs eventArgs) { var alive = _list.GetAlive(); if(alive == null) @@ -196,4 +142,4 @@ public class WeakEvent return () => unsubscribe(s, handler); }); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs index 020ba7a6d9..ef143144e6 100644 --- a/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs +++ b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs @@ -60,8 +60,7 @@ namespace Avalonia.Utilities private static class SubscriptionTypeStorage where TArgs : EventArgs where TSubscriber : class { - public static readonly ConditionalWeakTable> Subscribers - = new ConditionalWeakTable>(); + public static readonly ConditionalWeakTable> Subscribers = new(); } private class SubscriptionDic : Dictionary> @@ -69,8 +68,7 @@ namespace Avalonia.Utilities { } - private static readonly Dictionary> Accessors - = new Dictionary>(); + private static readonly Dictionary> s_accessors = new(); private class Subscription where T : EventArgs where TSubscriber : class { @@ -81,18 +79,17 @@ namespace Avalonia.Utilities private readonly Delegate _delegate; private Descriptor[] _data = new Descriptor[2]; - private int _count = 0; + private int _count; - delegate void CallerDelegate(TSubscriber s, object sender, T args); - - struct Descriptor + private delegate void CallerDelegate(TSubscriber s, object? sender, T args); + + private struct Descriptor { - public WeakReference Subscriber; - public CallerDelegate Caller; + public WeakReference? Subscriber; + public CallerDelegate? Caller; } - private static Dictionary s_Callers = - new Dictionary(); + private static readonly Dictionary s_callers = new(); public Subscription(SubscriptionDic sdic, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents)] Type targetType, @@ -101,8 +98,8 @@ namespace Avalonia.Utilities _sdic = sdic; _target = target; _eventName = eventName; - if (!Accessors.TryGetValue(targetType, out var evDic)) - Accessors[targetType] = evDic = new Dictionary(); + if (!s_accessors.TryGetValue(targetType, out var evDic)) + s_accessors[targetType] = evDic = new Dictionary(); if (evDic.TryGetValue(eventName, out var info)) { @@ -123,12 +120,12 @@ namespace Avalonia.Utilities var del = new Action(OnEvent); _delegate = del.GetMethodInfo().CreateDelegate(_info.EventHandlerType!, del.Target); - _info.AddMethod!.Invoke(target, new[] { _delegate }); + _info.AddMethod!.Invoke(target, new object?[] { _delegate }); } - void Destroy() + private void Destroy() { - _info.RemoveMethod!.Invoke(_target, new[] { _delegate }); + _info.RemoveMethod!.Invoke(_target, new object?[] { _delegate }); _sdic.Remove(_eventName); } @@ -146,8 +143,8 @@ namespace Avalonia.Utilities MethodInfo method = s.Method; var subscriber = (TSubscriber)s.Target!; - if (!s_Callers.TryGetValue(method, out var caller)) - s_Callers[method] = caller = + if (!s_callers.TryGetValue(method, out var caller)) + s_callers[method] = caller = (CallerDelegate)Delegate.CreateDelegate(typeof(CallerDelegate), null, method); _data[_count] = new Descriptor { @@ -178,7 +175,7 @@ namespace Avalonia.Utilities } } - void Compact(bool preventDestroy = false) + private void Compact(bool preventDestroy = false) { int empty = -1; for (int c = 0; c < _count; c++) @@ -206,15 +203,15 @@ namespace Avalonia.Utilities Destroy(); } - void OnEvent(object sender, T eventArgs) + private void OnEvent(object? sender, T eventArgs) { var needCompact = false; - for(var c=0; c<_count; c++) + for (var c = 0; c < _count; c++) { - var r = _data[c].Subscriber; + var r = _data[c].Subscriber!; if (r.TryGetTarget(out var sub)) { - _data[c].Caller(sub, sender, eventArgs); + _data[c].Caller!(sub, sender, eventArgs); } else needCompact = true; diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index e6d7492c51..58e84f29d8 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -483,17 +483,13 @@ namespace Avalonia parent.HasNonUniformZIndexChildren = true; var visualChildren = VisualChildren; + var visualChildrenCount = visualChildren.Count; - if (visualChildren != null) + for (var i = 0; i < visualChildrenCount; i++) { - var visualChildrenCount = visualChildren.Count; - - for (var i = 0; i < visualChildrenCount; i++) + if (visualChildren[i] is { } child) { - if (visualChildren[i] is Visual child) - { - child.OnAttachedToVisualTreeCore(e); - } + child.OnAttachedToVisualTreeCore(e); } } } @@ -543,17 +539,13 @@ namespace Avalonia e.Root?.Renderer?.AddDirty(this); var visualChildren = VisualChildren; + var visualChildrenCount = visualChildren.Count; - if (visualChildren != null) + for (var i = 0; i < visualChildrenCount; i++) { - var visualChildrenCount = visualChildren.Count; - - for (var i = 0; i < visualChildrenCount; i++) + if (visualChildren[i] is { } child) { - if (visualChildren[i] is Visual child) - { - child.OnDetachedFromVisualTreeCore(e); - } + child.OnDetachedFromVisualTreeCore(e); } } } diff --git a/src/Avalonia.Base/VisualTree/VisualExtensions.cs b/src/Avalonia.Base/VisualTree/VisualExtensions.cs index b58db3b276..9e38c6e7f2 100644 --- a/src/Avalonia.Base/VisualTree/VisualExtensions.cs +++ b/src/Avalonia.Base/VisualTree/VisualExtensions.cs @@ -46,7 +46,7 @@ namespace Avalonia.VisualTree Visual? v = visual ?? throw new ArgumentNullException(nameof(visual)); var result = 0; - v = v?.VisualParent; + v = v.VisualParent; while (v != null) { @@ -64,17 +64,13 @@ namespace Avalonia.VisualTree /// The first visual. /// The second visual. /// The common ancestor, or null if not found. - public static Visual? FindCommonVisualAncestor(this Visual visual, Visual target) + public static Visual? FindCommonVisualAncestor(this Visual? visual, Visual? target) { - Visual? v = visual ?? throw new ArgumentNullException(nameof(visual)); - - if (target is null) + if (visual is null || target is null) { return null; } - Visual? t = target; - void GoUpwards(ref Visual? node, int count) { for (int i = 0; i < count; ++i) @@ -83,6 +79,9 @@ namespace Avalonia.VisualTree } } + Visual? v = visual; + Visual? t = target; + // We want to find lowest node first, then make sure that both nodes are at the same height. // By doing that we can sometimes find out that other node is our lowest common ancestor. var firstHeight = CalculateDistanceFromRoot(v); @@ -144,7 +143,7 @@ namespace Avalonia.VisualTree /// The visual. /// If given visual should be included in search. /// First ancestor of given type. - public static T? FindAncestorOfType(this Visual visual, bool includeSelf = false) where T : class + public static T? FindAncestorOfType(this Visual? visual, bool includeSelf = false) where T : class { if (visual is null) { @@ -173,7 +172,7 @@ namespace Avalonia.VisualTree /// The visual. /// If given visual should be included in search. /// First descendant of given type. - public static T? FindDescendantOfType(this Visual visual, bool includeSelf = false) where T : class + public static T? FindDescendantOfType(this Visual? visual, bool includeSelf = false) where T : class { if (visual is null) { @@ -392,7 +391,7 @@ namespace Avalonia.VisualTree /// True if is an ancestor of ; /// otherwise false. /// - public static bool IsVisualAncestorOf(this Visual visual, Visual target) + public static bool IsVisualAncestorOf(this Visual? visual, Visual? target) { Visual? current = target?.VisualParent; diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 225e846390..68466fe381 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -141,9 +141,7 @@ namespace Avalonia.Headless } public IReadOnlyList GetIntersections(float lowerBound, float upperBound) - { - return null; - } + => Array.Empty(); } class HeadlessGeometryStub : IGeometryImpl diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index dcb20d2a44..ba646c64ee 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -208,6 +208,12 @@ namespace Avalonia.Skia public void DrawLine(IPen pen, Point p1, Point p2) { CheckLease(); + + if (pen is null) + { + return; + } + using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) { if (paint.Paint is object) @@ -495,6 +501,12 @@ namespace Avalonia.Skia public void DrawGlyphRun(IBrush foreground, IRef glyphRun) { CheckLease(); + + if (foreground is null) + { + return; + } + using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Item.Size)) { var glyphRunImpl = (GlyphRunImpl)glyphRun.Item; diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs index 24b8fc04b3..446db47d92 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Platform; using SharpDX.DirectWrite; @@ -25,8 +26,6 @@ namespace Avalonia.Direct2D1.Media } public IReadOnlyList GetIntersections(float lowerBound, float upperBound) - { - return null; - } + => Array.Empty(); } } diff --git a/tests/Avalonia.UnitTests/MockGlyphRun.cs b/tests/Avalonia.UnitTests/MockGlyphRun.cs index 477f34565f..0319803a5e 100644 --- a/tests/Avalonia.UnitTests/MockGlyphRun.cs +++ b/tests/Avalonia.UnitTests/MockGlyphRun.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media.TextFormatting; using Avalonia.Platform; @@ -24,12 +25,9 @@ namespace Avalonia.UnitTests public void Dispose() { - } public IReadOnlyList GetIntersections(float lowerBound, float upperBound) - { - return null; - } + => Array.Empty(); } } From d99b795504cedec99428e2038bf1df2408215122 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Tue, 7 Feb 2023 11:54:32 +0800 Subject: [PATCH 078/185] fix: fix ListBoxItem generation. --- src/Avalonia.Controls/ListBox.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 8b1a307182..80d1677c2f 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -104,6 +104,7 @@ namespace Avalonia.Controls public void UnselectAll() => Selection.Clear(); protected internal override Control CreateContainerForItemOverride() => new ListBoxItem(); + protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is ListBoxItem; /// protected override void OnGotFocus(GotFocusEventArgs e) From 38aaadf92db4e4957a3ccab2365937c215e3c89a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 6 Feb 2023 15:16:59 +0100 Subject: [PATCH 079/185] Use custom zoom logic when !_canResize. `[NSWindow setIsZoomed]` requires that the window is resizable by the user in order to work; when `canResize == false` this is not that case. --- native/Avalonia.Native/src/OSX/WindowImpl.mm | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index ce82f7d83f..af4f92524b 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -225,16 +225,12 @@ bool WindowImpl::IsZoomed() { } void WindowImpl::DoZoom() { - switch (_decorations) { - case SystemDecorationsNone: - case SystemDecorationsBorderOnly: - [Window setFrame:[Window screen].visibleFrame display:true]; - break; - - - case SystemDecorationsFull: - [Window performZoom:Window]; - break; + if (_decorations == SystemDecorationsNone || + _decorations == SystemDecorationsBorderOnly || + _canResize == false) { + [Window setFrame:[Window screen].visibleFrame display:true]; + } else { + [Window performZoom:Window]; } } From 5346344d12fa314c7317f90f03c8e798ee4ef6cc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 6 Feb 2023 17:32:55 +0100 Subject: [PATCH 080/185] Don't call virtual method from ctor. And remove unneeded checks for already-existing `Window` (this method is always called from ctor). --- .../Avalonia.Native/src/OSX/WindowBaseImpl.mm | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 038e9a048c..4d3768a4a8 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -580,21 +580,12 @@ void WindowBaseImpl::CleanNSWindow() { } } -void WindowBaseImpl::CreateNSWindow(bool isDialog) { - if (isDialog) { - if (![Window isKindOfClass:[AvnPanel class]]) { - CleanNSWindow(); - - Window = [[AvnPanel alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()]; - - [Window setHidesOnDeactivate:false]; - } +void WindowBaseImpl::CreateNSWindow(bool usePanel) { + if (usePanel) { + Window = [[AvnPanel alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:NSWindowStyleMaskBorderless]; + [Window setHidesOnDeactivate:false]; } else { - if (![Window isKindOfClass:[AvnWindow class]]) { - CleanNSWindow(); - - Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()]; - } + Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:NSWindowStyleMaskBorderless]; } } From b609ba58a8e445ff68ddbcd64d9ab3d9d4f6cb6c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 6 Feb 2023 17:43:02 +0100 Subject: [PATCH 081/185] Disable zoom button when CanResize == false. To do this more easily, merged `HideOrShowTrafficLights` into a virtual `UpdateStyle`. They were always called together, and really _must_ be called together; this enforces that. --- .../Avalonia.Native/src/OSX/WindowBaseImpl.h | 3 +- native/Avalonia.Native/src/OSX/WindowImpl.h | 3 +- native/Avalonia.Native/src/OSX/WindowImpl.mm | 37 +++++++++---------- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index 4c2758f6c6..afa3a2956f 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -106,8 +106,7 @@ BEGIN_INTERFACE_MAP() protected: virtual NSWindowStyleMask GetStyle(); - - void UpdateStyle(); + virtual void UpdateStyle(); private: void CreateNSWindow (bool isDialog); diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index 3861aaf170..e7e9b7f1d8 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -41,8 +41,6 @@ BEGIN_INTERFACE_MAP() WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl); - void HideOrShowTrafficLights (); - virtual HRESULT Show (bool activate, bool isDialog) override; virtual HRESULT SetEnabled (bool enable) override; @@ -101,6 +99,7 @@ BEGIN_INTERFACE_MAP() protected: virtual NSWindowStyleMask GetStyle() override; + void UpdateStyle () override; private: void OnInitialiseNSWindow(); diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index af4f92524b..ae3e1eaf1d 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -30,19 +30,6 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBase OnInitialiseNSWindow(); } -void WindowImpl::HideOrShowTrafficLights() { - if (Window == nil) { - return; - } - - bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); - bool hasTrafficLights = _isClientAreaExtended ? wantsChrome : _decorations == SystemDecorationsFull; - - [[Window standardWindowButton:NSWindowCloseButton] setHidden:!hasTrafficLights]; - [[Window standardWindowButton:NSWindowMiniaturizeButton] setHidden:!hasTrafficLights]; - [[Window standardWindowButton:NSWindowZoomButton] setHidden:!hasTrafficLights]; -} - void WindowImpl::OnInitialiseNSWindow(){ [GetWindowProtocol() setCanBecomeKeyWindow:true]; @@ -67,8 +54,6 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) { WindowBaseImpl::Show(activate, isDialog); GetWindowState(&_actualWindowState); - HideOrShowTrafficLights(); - return SetWindowState(_lastWindowState); } } @@ -257,8 +242,6 @@ HRESULT WindowImpl::SetDecorations(SystemDecorations value) { UpdateStyle(); - HideOrShowTrafficLights(); - switch (_decorations) { case SystemDecorationsNone: [Window setHasShadow:NO]; @@ -415,9 +398,6 @@ HRESULT WindowImpl::SetExtendClientArea(bool enable) { } [GetWindowProtocol() setIsExtended:enable]; - - HideOrShowTrafficLights(); - UpdateStyle(); } @@ -608,3 +588,20 @@ NSWindowStyleMask WindowImpl::GetStyle() { } return s; } + +void WindowImpl::UpdateStyle() { + WindowBaseImpl::UpdateStyle(); + + if (Window == nil) { + return; + } + + bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); + bool hasTrafficLights = _isClientAreaExtended ? wantsChrome : _decorations == SystemDecorationsFull; + + [[Window standardWindowButton:NSWindowCloseButton] setHidden:!hasTrafficLights]; + [[Window standardWindowButton:NSWindowMiniaturizeButton] setHidden:!hasTrafficLights]; + [[Window standardWindowButton:NSWindowZoomButton] setHidden:!hasTrafficLights]; + [[Window standardWindowButton:NSWindowZoomButton] setEnabled:_canResize]; +} + From d40041f02dd8ae5e53074e3b4c3e47b420ff9457 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 6 Feb 2023 19:13:59 +0100 Subject: [PATCH 082/185] GetStyle => abstract CalculateStyleMask. - Make naming more clear - it's not getting the style mask, it's calculating what it should be in order to update the mask - Make it abstract to prevent it being called from the ctor in future --- native/Avalonia.Native/src/OSX/PopupImpl.mm | 2 +- native/Avalonia.Native/src/OSX/WindowBaseImpl.h | 2 +- native/Avalonia.Native/src/OSX/WindowBaseImpl.mm | 10 +--------- native/Avalonia.Native/src/OSX/WindowImpl.h | 2 +- native/Avalonia.Native/src/OSX/WindowImpl.mm | 2 +- 5 files changed, 5 insertions(+), 13 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/PopupImpl.mm b/native/Avalonia.Native/src/OSX/PopupImpl.mm index 9820a9f052..972d03d08c 100644 --- a/native/Avalonia.Native/src/OSX/PopupImpl.mm +++ b/native/Avalonia.Native/src/OSX/PopupImpl.mm @@ -29,7 +29,7 @@ private: [Window setLevel:NSPopUpMenuWindowLevel]; } protected: - virtual NSWindowStyleMask GetStyle() override + virtual NSWindowStyleMask CalculateStyleMask() override { return NSWindowStyleMaskBorderless; } diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index afa3a2956f..93decef136 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -105,7 +105,7 @@ BEGIN_INTERFACE_MAP() virtual void BringToFront (); protected: - virtual NSWindowStyleMask GetStyle(); + virtual NSWindowStyleMask CalculateStyleMask() = 0; virtual void UpdateStyle(); private: diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 4d3768a4a8..59102e15a6 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -35,18 +35,14 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl, lastSize = NSSize { 100, 100 }; lastMaxSize = NSSize { CGFLOAT_MAX, CGFLOAT_MAX}; lastMinSize = NSSize { 0, 0 }; - lastMenu = nullptr; CreateNSWindow(usePanel); [Window setContentView:StandardContainer]; - [Window setStyleMask:NSWindowStyleMaskBorderless]; [Window setBackingType:NSBackingStoreBuffered]; - [Window setContentMinSize:lastMinSize]; [Window setContentMaxSize:lastMaxSize]; - [Window setOpaque:false]; } @@ -564,12 +560,8 @@ bool WindowBaseImpl::IsModal() { return false; } -NSWindowStyleMask WindowBaseImpl::GetStyle() { - return NSWindowStyleMaskBorderless; -} - void WindowBaseImpl::UpdateStyle() { - [Window setStyleMask:GetStyle()]; + [Window setStyleMask:CalculateStyleMask()]; } void WindowBaseImpl::CleanNSWindow() { diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index e7e9b7f1d8..9c684c77c4 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -98,7 +98,7 @@ BEGIN_INTERFACE_MAP() bool CanBecomeKeyWindow (); protected: - virtual NSWindowStyleMask GetStyle() override; + virtual NSWindowStyleMask CalculateStyleMask() override; void UpdateStyle () override; private: diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index ae3e1eaf1d..afd9c1a5ea 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -553,7 +553,7 @@ bool WindowImpl::IsOwned() { return _parent != nullptr; } -NSWindowStyleMask WindowImpl::GetStyle() { +NSWindowStyleMask WindowImpl::CalculateStyleMask() { unsigned long s = NSWindowStyleMaskBorderless; if(_actualWindowState == FullScreen) From 959b09c2434a285cead448fa8f22a6d282afb90d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 7 Feb 2023 00:18:15 +0100 Subject: [PATCH 083/185] Skip flaky test for now. --- tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 0839cbf183..1933d10919 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -151,7 +151,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("Normal", windowState.Text); } - [PlatformFact(TestPlatforms.MacOS)] + [PlatformFact(TestPlatforms.MacOS, Skip = "Flaky test, skip for now")] public void Does_Not_Switch_Space_From_FullScreen_To_Main_Desktop_When_FullScreen_Window_Clicked() { // Issue #9565 From 6a1910172c2a060f7ef67d875af03064db768d5f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 7 Feb 2023 10:26:45 +0100 Subject: [PATCH 084/185] Move traffic lights logic into one place. --- native/Avalonia.Native/src/OSX/AvnWindow.mm | 4 ---- native/Avalonia.Native/src/OSX/WindowImpl.mm | 15 ++++++++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index 784072221d..23abf1d53f 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -274,10 +274,6 @@ -(void) setEnabled:(bool)enable { _isEnabled = enable; - - [[self standardWindowButton:NSWindowCloseButton] setEnabled:enable]; - [[self standardWindowButton:NSWindowMiniaturizeButton] setEnabled:enable]; - [[self standardWindowButton:NSWindowZoomButton] setEnabled:enable]; } -(void)becomeKeyWindow diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index afd9c1a5ea..e4bbe24cb8 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -599,9 +599,14 @@ void WindowImpl::UpdateStyle() { bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); bool hasTrafficLights = _isClientAreaExtended ? wantsChrome : _decorations == SystemDecorationsFull; - [[Window standardWindowButton:NSWindowCloseButton] setHidden:!hasTrafficLights]; - [[Window standardWindowButton:NSWindowMiniaturizeButton] setHidden:!hasTrafficLights]; - [[Window standardWindowButton:NSWindowZoomButton] setHidden:!hasTrafficLights]; - [[Window standardWindowButton:NSWindowZoomButton] setEnabled:_canResize]; -} + NSButton* closeButton = [Window standardWindowButton:NSWindowCloseButton]; + NSButton* miniaturizeButton = [Window standardWindowButton:NSWindowMiniaturizeButton]; + NSButton* zoomButton = [Window standardWindowButton:NSWindowZoomButton]; + [closeButton setHidden:!hasTrafficLights]; + [closeButton setEnabled:_isEnabled]; + [miniaturizeButton setHidden:!hasTrafficLights]; + [miniaturizeButton setEnabled:_isEnabled]; + [zoomButton setHidden:!hasTrafficLights]; + [zoomButton setEnabled:_isEnabled && _canResize]; +} From a705f546bba1f3fb973aae2cdeb9ed8d08cf4f16 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 7 Feb 2023 11:46:18 +0100 Subject: [PATCH 085/185] Enable resizing during fullscreen transition. macOS seems to tie resizing of the NSView inside the window to the resizable style mask of the window somehow. If we programmatically transition a non-resizable window to fullscreen, the inner NSView's size isn't changed, so we need to make the window resizable during the fullscreen transition. Makes the final two failing `WindowState` integration tests pass. --- native/Avalonia.Native/src/OSX/WindowImpl.mm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index e4bbe24cb8..47e83f8d56 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -146,11 +146,13 @@ bool WindowImpl::CanBecomeKeyWindow() void WindowImpl::StartStateTransition() { _transitioningWindowState = true; + UpdateStyle(); } void WindowImpl::EndStateTransition() { _transitioningWindowState = false; - + UpdateStyle(); + // Ensure correct order of child windows after fullscreen transition. BringToFront(); } @@ -573,7 +575,7 @@ NSWindowStyleMask WindowImpl::CalculateStyleMask() { case SystemDecorationsFull: s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable; - if (_canResize && _isEnabled) { + if ((_canResize && _isEnabled) || _transitioningWindowState) { s = s | NSWindowStyleMaskResizable; } break; From 50a368eaa15e7a5756eeda2ec2fdf69d43e9f6da Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 7 Feb 2023 13:05:05 +0100 Subject: [PATCH 086/185] Skip flaky test for now. --- tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 1933d10919..47a471fafd 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -56,7 +56,7 @@ namespace Avalonia.IntegrationTests.Appium } } - [PlatformFact(TestPlatforms.MacOS)] + [PlatformFact(TestPlatforms.MacOS, Skip = "Flaky test, skip for now")] public void WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_Clicking_Resize_Grip() { var mainWindow = GetWindow("MainWindow"); From 6dae36ead9858385e710a117c8a2bc4b002e960c Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 7 Feb 2023 14:01:39 +0000 Subject: [PATCH 087/185] Revert "Only set pointer events to handled in button if click is triggered" --- src/Avalonia.Controls/Button.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 9627f200df..1ec6f8dabc 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -394,10 +394,10 @@ namespace Avalonia.Controls if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { IsPressed = true; + e.Handled = true; if (ClickMode == ClickMode.Press) { - e.Handled = true; OnClick(); } } @@ -411,11 +411,11 @@ namespace Avalonia.Controls if (IsPressed && e.InitialPressMouseButton == MouseButton.Left) { IsPressed = false; + e.Handled = true; if (ClickMode == ClickMode.Release && this.GetVisualsAt(e.GetPosition(this)).Any(c => this == c || this.IsVisualAncestorOf(c))) { - e.Handled = true; OnClick(); } } From 784c380c604b6fd6a139abd7233aaf668661bfd0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 7 Feb 2023 16:10:31 +0100 Subject: [PATCH 088/185] Don't change z-order of window exiting fullscreen. When a window exits fullscreen, its child windows need to be ordered, but we shouldn't touch the z-order of the window itself as this sometimes seemed to result in the parent window being shown over the child windows. Fixes flaky integration tests (hopefully). --- native/Avalonia.Native/src/OSX/WindowImpl.h | 1 + native/Avalonia.Native/src/OSX/WindowImpl.mm | 21 ++++++++++++------- .../WindowTests_MacOS.cs | 4 ++-- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index 9c684c77c4..29bb659039 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -102,6 +102,7 @@ protected: void UpdateStyle () override; private: + void ZOrderChildWindows(); void OnInitialiseNSWindow(); NSString *_lastTitle; }; diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 47e83f8d56..4510d135dc 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -119,14 +119,19 @@ void WindowImpl::BringToFront() } [Window invalidateShadow]; + ZOrderChildWindows(); + } +} + +void WindowImpl::ZOrderChildWindows() +{ + for(auto iterator = _children.begin(); iterator != _children.end(); iterator++) + { + auto window = (*iterator)->Window; - for(auto iterator = _children.begin(); iterator != _children.end(); iterator++) - { - auto window = (*iterator)->Window; - - // #9565: Only bring window to front if it's on the currently active space - if ([window isOnActiveSpace]) - (*iterator)->BringToFront(); + // #9565: Only bring window to front if it's on the currently active space + if ([window isOnActiveSpace]) { + (*iterator)->BringToFront(); } } } @@ -154,7 +159,7 @@ void WindowImpl::EndStateTransition() { UpdateStyle(); // Ensure correct order of child windows after fullscreen transition. - BringToFront(); + ZOrderChildWindows(); } SystemDecorations WindowImpl::Decorations() { diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 47a471fafd..0839cbf183 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -56,7 +56,7 @@ namespace Avalonia.IntegrationTests.Appium } } - [PlatformFact(TestPlatforms.MacOS, Skip = "Flaky test, skip for now")] + [PlatformFact(TestPlatforms.MacOS)] public void WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_Clicking_Resize_Grip() { var mainWindow = GetWindow("MainWindow"); @@ -151,7 +151,7 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("Normal", windowState.Text); } - [PlatformFact(TestPlatforms.MacOS, Skip = "Flaky test, skip for now")] + [PlatformFact(TestPlatforms.MacOS)] public void Does_Not_Switch_Space_From_FullScreen_To_Main_Desktop_When_FullScreen_Window_Clicked() { // Issue #9565 From e1138f2cb6a393802b235a073d28e85a64690ffe Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Tue, 7 Feb 2023 16:53:49 +0100 Subject: [PATCH 089/185] Nullability fixes for Avalonia.Controls --- .../Input/Platform/IClipboard.cs | 6 +- src/Avalonia.Base/Media/Color.cs | 2 +- .../Platform/Internal/AssemblyDescriptor.cs | 5 +- .../ClassicDesktopStyleApplicationLifetime.cs | 28 +- ...IClassicDesktopStyleApplicationLifetime.cs | 5 +- .../AutoCompleteBox/AutoCompleteBox.cs | 2 +- .../Automation/AutomationProperties.cs | 282 +++++------------- src/Avalonia.Controls/Calendar/Calendar.cs | 18 +- .../CalendarBlackoutDatesCollection.cs | 14 +- .../Calendar/CalendarItem.cs | 112 +++---- .../Calendar/DateTimeHelper.cs | 33 +- .../CalendarDatePicker/CalendarDatePicker.cs | 20 +- src/Avalonia.Controls/Chrome/TitleBar.cs | 40 +-- .../MenuScrollingVisibilityConverter.cs | 1 - src/Avalonia.Controls/DefinitionBase.cs | 90 +++--- src/Avalonia.Controls/DockPanel.cs | 9 +- src/Avalonia.Controls/Documents/Inline.cs | 9 +- src/Avalonia.Controls/Documents/Span.cs | 14 +- src/Avalonia.Controls/Grid.cs | 230 +++++++------- src/Avalonia.Controls/Image.cs | 7 +- src/Avalonia.Controls/ItemsControl.cs | 12 +- .../LayoutTransformControl.cs | 20 +- src/Avalonia.Controls/MaskedTextBox.cs | 12 +- src/Avalonia.Controls/NativeControlHost.cs | 22 +- src/Avalonia.Controls/NativeMenu.Export.cs | 20 +- .../Primitives/AdornerLayer.cs | 12 +- src/Avalonia.Controls/Primitives/Popup.cs | 12 +- .../Primitives/SelectingItemsControl.cs | 31 +- .../Primitives/TemplatedControl.cs | 7 +- .../Primitives/TextSearch.cs | 8 +- src/Avalonia.Controls/Primitives/Track.cs | 19 +- .../Primitives/VisualLayerManager.cs | 16 +- src/Avalonia.Controls/RelativePanel.cs | 7 +- .../Remote/Server/RemoteServerTopLevelImpl.cs | 44 ++- src/Avalonia.Controls/Slider.cs | 13 +- src/Avalonia.Controls/StackPanel.cs | 9 +- src/Avalonia.Controls/TickBar.cs | 18 +- src/Avalonia.Controls/TrayIcon.cs | 9 +- src/Avalonia.Controls/TreeView.cs | 16 +- src/Avalonia.Controls/Viewbox.cs | 36 +-- src/Avalonia.Controls/WrapPanel.cs | 107 +++---- src/Browser/Avalonia.Browser/ClipboardImpl.cs | 10 +- 42 files changed, 580 insertions(+), 807 deletions(-) diff --git a/src/Avalonia.Base/Input/Platform/IClipboard.cs b/src/Avalonia.Base/Input/Platform/IClipboard.cs index bf2a5a8602..3de352fc4f 100644 --- a/src/Avalonia.Base/Input/Platform/IClipboard.cs +++ b/src/Avalonia.Base/Input/Platform/IClipboard.cs @@ -6,9 +6,9 @@ namespace Avalonia.Input.Platform [NotClientImplementable] public interface IClipboard { - Task GetTextAsync(); + Task GetTextAsync(); - Task SetTextAsync(string text); + Task SetTextAsync(string? text); Task ClearAsync(); @@ -16,6 +16,6 @@ namespace Avalonia.Input.Platform Task GetFormatsAsync(); - Task GetDataAsync(string format); + Task GetDataAsync(string format); } } diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs index ab89177295..74e70b2a14 100644 --- a/src/Avalonia.Base/Media/Color.cs +++ b/src/Avalonia.Base/Media/Color.cs @@ -331,7 +331,7 @@ namespace Avalonia.Media /// /// Parses the given string representing a CSS color value into a new . /// - private static bool TryParseCssFormat(string s, out Color color) + private static bool TryParseCssFormat(string? s, out Color color) { bool prefixMatched = false; diff --git a/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs b/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs index d1a803fefb..6a577c204c 100644 --- a/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs +++ b/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -18,7 +19,7 @@ internal class AssemblyDescriptor : IAssemblyDescriptor { public AssemblyDescriptor(Assembly assembly) { - Assembly = assembly; + Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); Resources = assembly.GetManifestResourceNames() .ToDictionary(n => n, n => (IAssetDescriptor)new AssemblyResourceDescriptor(assembly, n)); Name = assembly.GetName().Name; diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index fde401fb01..ada0b94124 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; @@ -17,32 +16,34 @@ namespace Avalonia.Controls.ApplicationLifetimes private int _exitCode; private CancellationTokenSource? _cts; private bool _isShuttingDown; - private HashSet _windows = new HashSet(); + private readonly HashSet _windows = new(); + + private static ClassicDesktopStyleApplicationLifetime? s_activeLifetime; - private static ClassicDesktopStyleApplicationLifetime? _activeLifetime; static ClassicDesktopStyleApplicationLifetime() { Window.WindowOpenedEvent.AddClassHandler(typeof(Window), OnWindowOpened); - Window.WindowClosedEvent.AddClassHandler(typeof(Window), WindowClosedEvent); + Window.WindowClosedEvent.AddClassHandler(typeof(Window), OnWindowClosed); } - private static void WindowClosedEvent(object? sender, RoutedEventArgs e) + private static void OnWindowClosed(object? sender, RoutedEventArgs e) { - _activeLifetime?._windows.Remove((Window)sender!); - _activeLifetime?.HandleWindowClosed((Window)sender!); + var window = (Window)sender!; + s_activeLifetime?._windows.Remove(window); + s_activeLifetime?.HandleWindowClosed(window); } private static void OnWindowOpened(object? sender, RoutedEventArgs e) { - _activeLifetime?._windows.Add((Window)sender!); + s_activeLifetime?._windows.Add((Window)sender!); } public ClassicDesktopStyleApplicationLifetime() { - if (_activeLifetime != null) + if (s_activeLifetime != null) throw new InvalidOperationException( "Can not have multiple active ClassicDesktopStyleApplicationLifetime instances and the previously created one was not disposed"); - _activeLifetime = this; + s_activeLifetime = this; } /// @@ -65,9 +66,10 @@ namespace Avalonia.Controls.ApplicationLifetimes /// public Window? MainWindow { get; set; } + /// public IReadOnlyList Windows => _windows.ToArray(); - private void HandleWindowClosed(Window window) + private void HandleWindowClosed(Window? window) { if (window == null) return; @@ -130,8 +132,8 @@ namespace Avalonia.Controls.ApplicationLifetimes public void Dispose() { - if (_activeLifetime == this) - _activeLifetime = null; + if (s_activeLifetime == this) + s_activeLifetime = null; } private bool DoShutdown( diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs index 22b5f8236d..b9a372f935 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs @@ -40,7 +40,10 @@ namespace Avalonia.Controls.ApplicationLifetimes /// The main window. /// Window? MainWindow { get; set; } - + + /// + /// Gets the list of all open windows in the application. + /// IReadOnlyList Windows { get; } /// diff --git a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs index 55649660f7..9a949e31d4 100644 --- a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs @@ -1711,7 +1711,7 @@ namespace Avalonia.Controls /// The predicate to use for the partial or /// exact match. /// Returns the object or null. - private object? TryGetMatch(string? searchText, AvaloniaList view, AutoCompleteFilterPredicate? predicate) + private object? TryGetMatch(string? searchText, AvaloniaList? view, AutoCompleteFilterPredicate? predicate) { if (predicate is null) return null; diff --git a/src/Avalonia.Controls/Automation/AutomationProperties.cs b/src/Avalonia.Controls/Automation/AutomationProperties.cs index 35f94722ce..3ea9c170ff 100644 --- a/src/Avalonia.Controls/Automation/AutomationProperties.cs +++ b/src/Avalonia.Controls/Automation/AutomationProperties.cs @@ -38,8 +38,8 @@ namespace Avalonia.Automation /// /// Defines the AutomationProperties.AcceleratorKey attached property. /// - public static readonly AttachedProperty AcceleratorKeyProperty = - AvaloniaProperty.RegisterAttached( + public static readonly AttachedProperty AcceleratorKeyProperty = + AvaloniaProperty.RegisterAttached( "AcceleratorKey", typeof(AutomationProperties)); @@ -54,16 +54,16 @@ namespace Avalonia.Automation /// /// Defines the AutomationProperties.AccessKey attached property /// - public static readonly AttachedProperty AccessKeyProperty = - AvaloniaProperty.RegisterAttached( + public static readonly AttachedProperty AccessKeyProperty = + AvaloniaProperty.RegisterAttached( "AccessKey", typeof(AutomationProperties)); /// /// Defines the AutomationProperties.AutomationId attached property. /// - public static readonly AttachedProperty AutomationIdProperty = - AvaloniaProperty.RegisterAttached( + public static readonly AttachedProperty AutomationIdProperty = + AvaloniaProperty.RegisterAttached( "AutomationId", typeof(AutomationProperties)); @@ -78,8 +78,8 @@ namespace Avalonia.Automation /// /// Defines the AutomationProperties.HelpText attached property. /// - public static readonly AttachedProperty HelpTextProperty = - AvaloniaProperty.RegisterAttached( + public static readonly AttachedProperty HelpTextProperty = + AvaloniaProperty.RegisterAttached( "HelpText", typeof(AutomationProperties)); @@ -122,16 +122,16 @@ namespace Avalonia.Automation /// /// Defines the AutomationProperties.ItemStatus attached property. /// - public static readonly AttachedProperty ItemStatusProperty = - AvaloniaProperty.RegisterAttached( + public static readonly AttachedProperty ItemStatusProperty = + AvaloniaProperty.RegisterAttached( "ItemStatus", typeof(AutomationProperties)); /// /// Defines the AutomationProperties.ItemType attached property. /// - public static readonly AttachedProperty ItemTypeProperty = - AvaloniaProperty.RegisterAttached( + public static readonly AttachedProperty ItemTypeProperty = + AvaloniaProperty.RegisterAttached( "ItemType", typeof(AutomationProperties)); @@ -155,8 +155,8 @@ namespace Avalonia.Automation /// /// Defines the AutomationProperties.Name attached attached property. /// - public static readonly AttachedProperty NameProperty = - AvaloniaProperty.RegisterAttached( + public static readonly AttachedProperty NameProperty = + AvaloniaProperty.RegisterAttached( "Name", typeof(AutomationProperties)); @@ -193,25 +193,17 @@ namespace Avalonia.Automation /// public static void SetAcceleratorKey(StyledElement element, string value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(AcceleratorKeyProperty, value); } /// /// Helper for reading AcceleratorKey property from a StyledElement. /// - public static string GetAcceleratorKey(StyledElement element) + public static string? GetAcceleratorKey(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((string)element.GetValue(AcceleratorKeyProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(AcceleratorKeyProperty); } /// @@ -219,11 +211,7 @@ namespace Avalonia.Automation /// public static void SetAccessibilityView(StyledElement element, AccessibilityView value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(AccessibilityViewProperty, value); } @@ -232,11 +220,7 @@ namespace Avalonia.Automation /// public static AccessibilityView GetAccessibilityView(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); return element.GetValue(AccessibilityViewProperty); } @@ -245,50 +229,34 @@ namespace Avalonia.Automation /// public static void SetAccessKey(StyledElement element, string value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(AccessKeyProperty, value); } /// /// Helper for reading AccessKey property from a StyledElement. /// - public static string GetAccessKey(StyledElement element) + public static string? GetAccessKey(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((string)element.GetValue(AccessKeyProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(AccessKeyProperty); } /// /// Helper for setting AutomationId property on a StyledElement. /// - public static void SetAutomationId(StyledElement element, string value) + public static void SetAutomationId(StyledElement element, string? value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(AutomationIdProperty, value); } /// /// Helper for reading AutomationId property from a StyledElement. /// - public static string GetAutomationId(StyledElement element) + public static string? GetAutomationId(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); return element.GetValue(AutomationIdProperty); } @@ -297,11 +265,7 @@ namespace Avalonia.Automation /// public static void SetControlTypeOverride(StyledElement element, AutomationControlType? value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(ControlTypeOverrideProperty, value); } @@ -310,38 +274,26 @@ namespace Avalonia.Automation /// public static AutomationControlType? GetControlTypeOverride(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); return element.GetValue(ControlTypeOverrideProperty); } /// /// Helper for setting HelpText property on a StyledElement. /// - public static void SetHelpText(StyledElement element, string value) + public static void SetHelpText(StyledElement element, string? value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(HelpTextProperty, value); } /// /// Helper for reading HelpText property from a StyledElement. /// - public static string GetHelpText(StyledElement element) + public static string? GetHelpText(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((string)element.GetValue(HelpTextProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(HelpTextProperty); } /// @@ -349,11 +301,7 @@ namespace Avalonia.Automation /// public static void SetIsColumnHeader(StyledElement element, bool value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(IsColumnHeaderProperty, value); } @@ -362,12 +310,8 @@ namespace Avalonia.Automation /// public static bool GetIsColumnHeader(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((bool)element.GetValue(IsColumnHeaderProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(IsColumnHeaderProperty); } /// @@ -375,11 +319,7 @@ namespace Avalonia.Automation /// public static void SetIsRequiredForForm(StyledElement element, bool value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(IsRequiredForFormProperty, value); } @@ -388,12 +328,8 @@ namespace Avalonia.Automation /// public static bool GetIsRequiredForForm(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((bool)element.GetValue(IsRequiredForFormProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(IsRequiredForFormProperty); } /// @@ -401,12 +337,8 @@ namespace Avalonia.Automation /// public static bool GetIsRowHeader(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((bool)element.GetValue(IsRowHeaderProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(IsRowHeaderProperty); } /// @@ -414,11 +346,7 @@ namespace Avalonia.Automation /// public static void SetIsRowHeader(StyledElement element, bool value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(IsRowHeaderProperty, value); } @@ -427,11 +355,7 @@ namespace Avalonia.Automation /// public static void SetIsOffscreenBehavior(StyledElement element, IsOffscreenBehavior value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(IsOffscreenBehaviorProperty, value); } @@ -440,64 +364,44 @@ namespace Avalonia.Automation /// public static IsOffscreenBehavior GetIsOffscreenBehavior(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((IsOffscreenBehavior)element.GetValue(IsOffscreenBehaviorProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(IsOffscreenBehaviorProperty); } /// /// Helper for setting ItemStatus property on a StyledElement. /// - public static void SetItemStatus(StyledElement element, string value) + public static void SetItemStatus(StyledElement element, string? value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(ItemStatusProperty, value); } /// /// Helper for reading ItemStatus property from a StyledElement. /// - public static string GetItemStatus(StyledElement element) + public static string? GetItemStatus(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((string)element.GetValue(ItemStatusProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(ItemStatusProperty); } /// /// Helper for setting ItemType property on a StyledElement. /// - public static void SetItemType(StyledElement element, string value) + public static void SetItemType(StyledElement element, string? value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(ItemTypeProperty, value); } /// /// Helper for reading ItemType property from a StyledElement. /// - public static string GetItemType(StyledElement element) + public static string? GetItemType(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((string)element.GetValue(ItemTypeProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(ItemTypeProperty); } /// @@ -505,11 +409,7 @@ namespace Avalonia.Automation /// public static void SetLabeledBy(StyledElement element, Control value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(LabeledByProperty, value); } @@ -518,11 +418,7 @@ namespace Avalonia.Automation /// public static Control GetLabeledBy(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); return element.GetValue(LabeledByProperty); } @@ -531,11 +427,7 @@ namespace Avalonia.Automation /// public static void SetLiveSetting(StyledElement element, AutomationLiveSetting value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(LiveSettingProperty, value); } @@ -544,38 +436,26 @@ namespace Avalonia.Automation /// public static AutomationLiveSetting GetLiveSetting(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((AutomationLiveSetting)element.GetValue(LiveSettingProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(LiveSettingProperty); } /// /// Helper for setting Name property on a StyledElement. /// - public static void SetName(StyledElement element, string value) + public static void SetName(StyledElement element, string? value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(NameProperty, value); } /// /// Helper for reading Name property from a StyledElement. /// - public static string GetName(StyledElement element) + public static string? GetName(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((string)element.GetValue(NameProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(NameProperty); } /// @@ -583,11 +463,7 @@ namespace Avalonia.Automation /// public static void SetPositionInSet(StyledElement element, int value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(PositionInSetProperty, value); } @@ -596,12 +472,8 @@ namespace Avalonia.Automation /// public static int GetPositionInSet(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((int)element.GetValue(PositionInSetProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(PositionInSetProperty); } /// @@ -609,11 +481,7 @@ namespace Avalonia.Automation /// public static void SetSizeOfSet(StyledElement element, int value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(SizeOfSetProperty, value); } @@ -622,12 +490,8 @@ namespace Avalonia.Automation /// public static int GetSizeOfSet(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((int)element.GetValue(SizeOfSetProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(SizeOfSetProperty); } } } diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index 9c88bae5f6..3300292857 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -237,11 +237,11 @@ namespace Avalonia.Controls private DateTime _selectedYear; private DateTime _displayDate = DateTime.Today; - private DateTime? _displayDateStart = null; - private DateTime? _displayDateEnd = null; + private DateTime? _displayDateStart; + private DateTime? _displayDateEnd; private bool _isShiftPressed; - private bool _displayDateIsChanging = false; + private bool _displayDateIsChanging; internal CalendarDayButton? FocusButton { get; set; } internal CalendarButton? FocusCalendarButton { get; set; } @@ -291,7 +291,7 @@ namespace Avalonia.Controls } else { - throw new ArgumentOutOfRangeException("d", "Invalid DayOfWeek"); + throw new ArgumentOutOfRangeException(nameof(e), "Invalid DayOfWeek"); } } @@ -346,10 +346,10 @@ namespace Avalonia.Controls } } - public static readonly StyledProperty HeaderBackgroundProperty = - AvaloniaProperty.Register(nameof(HeaderBackground)); + public static readonly StyledProperty HeaderBackgroundProperty = + AvaloniaProperty.Register(nameof(HeaderBackground)); - public IBrush HeaderBackground + public IBrush? HeaderBackground { get { return GetValue(HeaderBackgroundProperty); } set { SetValue(HeaderBackgroundProperty, value); } @@ -478,7 +478,7 @@ namespace Avalonia.Controls } else { - throw new ArgumentOutOfRangeException("d", "Invalid SelectionMode"); + throw new ArgumentOutOfRangeException(nameof(e), "Invalid SelectionMode"); } } @@ -574,7 +574,7 @@ namespace Avalonia.Controls } else { - throw new ArgumentOutOfRangeException("d", "SelectedDate value is not valid."); + throw new ArgumentOutOfRangeException(nameof(e), "SelectedDate value is not valid."); } } else diff --git a/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs b/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs index fe8b616e02..8fb9b66f3d 100644 --- a/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs +++ b/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs @@ -15,7 +15,7 @@ namespace Avalonia.Controls.Primitives /// /// The Calendar whose dates this object represents. /// - private Calendar _owner; + private readonly Calendar _owner; /// /// Initializes a new instance of the @@ -79,13 +79,13 @@ namespace Avalonia.Controls.Primitives if (DateTime.Compare(end, start) > -1) { - rangeStart = DateTimeHelper.DiscardTime(start).Value; - rangeEnd = DateTimeHelper.DiscardTime(end).Value; + rangeStart = DateTimeHelper.DiscardTime(start); + rangeEnd = DateTimeHelper.DiscardTime(end); } else { - rangeStart = DateTimeHelper.DiscardTime(end).Value; - rangeEnd = DateTimeHelper.DiscardTime(start).Value; + rangeStart = DateTimeHelper.DiscardTime(end); + rangeEnd = DateTimeHelper.DiscardTime(start); } int count = Count; @@ -144,7 +144,7 @@ namespace Avalonia.Controls.Primitives if (!IsValid(item)) { - throw new ArgumentOutOfRangeException("Value is not valid."); + throw new ArgumentOutOfRangeException(nameof(item), "Value is not valid."); } base.InsertItem(index, item); @@ -186,7 +186,7 @@ namespace Avalonia.Controls.Primitives if (!IsValid(item)) { - throw new ArgumentOutOfRangeException("Value is not valid."); + throw new ArgumentOutOfRangeException(nameof(item), "Value is not valid."); } base.SetItem(index, item); diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index 032f452111..3d436b4485 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -44,30 +44,30 @@ namespace Avalonia.Controls.Primitives private ITemplate? _dayTitleTemplate; private DateTime _currentMonth; - private bool _isMouseLeftButtonDown = false; - private bool _isMouseLeftButtonDownYearView = false; - private bool _isControlPressed = false; + private bool _isMouseLeftButtonDown; + private bool _isMouseLeftButtonDownYearView; + private bool _isControlPressed; - private System.Globalization.Calendar _calendar = new System.Globalization.GregorianCalendar(); - - private PointerPressedEventArgs? _downEventArg; - private PointerPressedEventArgs? _downEventArgYearView; + private readonly System.Globalization.Calendar _calendar = new GregorianCalendar(); internal Calendar? Owner { get; set; } internal CalendarDayButton? CurrentButton { get; set; } - public static readonly StyledProperty HeaderBackgroundProperty = Calendar.HeaderBackgroundProperty.AddOwner(); - public IBrush HeaderBackground + public static readonly StyledProperty HeaderBackgroundProperty = Calendar.HeaderBackgroundProperty.AddOwner(); + + public IBrush? HeaderBackground { get { return GetValue(HeaderBackgroundProperty); } set { SetValue(HeaderBackgroundProperty, value); } } + public static readonly DirectProperty?> DayTitleTemplateProperty = AvaloniaProperty.RegisterDirect?>( nameof(DayTitleTemplate), o => o.DayTitleTemplate, (o,v) => o.DayTitleTemplate = v, defaultBindingMode: BindingMode.OneTime); + public ITemplate? DayTitleTemplate { get { return _dayTitleTemplate; } @@ -178,7 +178,7 @@ namespace Avalonia.Controls.Primitives { if (_dayTitleTemplate != null) { - var cell = (Control) _dayTitleTemplate.Build(); + var cell = _dayTitleTemplate.Build(); cell.DataContext = string.Empty; cell.SetValue(Grid.RowProperty, 0); cell.SetValue(Grid.ColumnProperty, i); @@ -308,16 +308,13 @@ namespace Avalonia.Controls.Primitives for (int childIndex = 0; childIndex < Calendar.ColumnsPerMonth; childIndex++) { var daytitle = MonthView!.Children[childIndex]; - if (daytitle != null) + if (Owner != null) { - if (Owner != null) - { - daytitle.DataContext = DateTimeHelper.GetCurrentDateFormat().ShortestDayNames[(childIndex + (int)Owner.FirstDayOfWeek) % NumberOfDaysPerWeek]; - } - else - { - daytitle.DataContext = DateTimeHelper.GetCurrentDateFormat().ShortestDayNames[(childIndex + (int)DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek) % NumberOfDaysPerWeek]; - } + daytitle.DataContext = DateTimeHelper.GetCurrentDateFormat().ShortestDayNames[(childIndex + (int)Owner.FirstDayOfWeek) % NumberOfDaysPerWeek]; + } + else + { + daytitle.DataContext = DateTimeHelper.GetCurrentDateFormat().ShortestDayNames[(childIndex + (int)DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek) % NumberOfDaysPerWeek]; } } } @@ -527,7 +524,7 @@ namespace Avalonia.Controls.Primitives childButton.Content = dateToAdd.Day.ToString(DateTimeHelper.GetCurrentDateFormat()); childButton.DataContext = dateToAdd; - if (DateTime.Compare((DateTime)DateTimeHelper.DiscardTime(DateTime.MaxValue), dateToAdd) > 0) + if (DateTime.Compare(DateTimeHelper.DiscardTime(DateTime.MaxValue), dateToAdd) > 0) { // Since we are sure DisplayDate is not equal to // DateTime.MaxValue, it is safe to use AddDays @@ -587,7 +584,7 @@ namespace Avalonia.Controls.Primitives { if (Owner != null) { - _currentMonth = (DateTime)Owner.SelectedMonth; + _currentMonth = Owner.SelectedMonth; } else { @@ -676,7 +673,7 @@ namespace Avalonia.Controls.Primitives if (Owner != null) { selectedYear = Owner.SelectedYear; - _currentMonth = (DateTime)Owner.SelectedMonth; + _currentMonth = Owner.SelectedMonth; } else { @@ -696,9 +693,9 @@ namespace Avalonia.Controls.Primitives SetYearButtons(decade, decadeEnd); } } - internal void UpdateYearViewSelection(CalendarButton calendarButton) + internal void UpdateYearViewSelection(CalendarButton? calendarButton) { - if (Owner != null && calendarButton != null && calendarButton.DataContext != null) + if (Owner != null && calendarButton?.DataContext is DateTime selectedDate) { Owner.FocusCalendarButton!.IsCalendarButtonFocused = false; Owner.FocusCalendarButton = calendarButton; @@ -706,11 +703,11 @@ namespace Avalonia.Controls.Primitives if (Owner.DisplayMode == CalendarMode.Year) { - Owner.SelectedMonth = (DateTime)calendarButton.DataContext; + Owner.SelectedMonth = selectedDate; } else { - Owner.SelectedYear = (DateTime)calendarButton.DataContext; + Owner.SelectedYear = selectedDate; } } } @@ -719,7 +716,7 @@ namespace Avalonia.Controls.Primitives { int year; int count = -1; - foreach (object child in YearView!.Children) + foreach (var child in YearView!.Children) { CalendarButton childButton = (CalendarButton)child; year = decade + count; @@ -859,7 +856,8 @@ namespace Avalonia.Controls.Primitives { if (Owner != null) { - if (_isMouseLeftButtonDown && sender is CalendarDayButton b && b.IsEnabled && !b.IsBlackout) + if (_isMouseLeftButtonDown + && sender is CalendarDayButton { IsEnabled: true, IsBlackout: false, DataContext: DateTime selectedDate } b) { // Update the states of all buttons to be selected starting // from HoverStart to b @@ -867,7 +865,6 @@ namespace Avalonia.Controls.Primitives { case CalendarSelectionMode.SingleDate: { - DateTime selectedDate = (DateTime)b.DataContext!; Owner.CalendarDatePickerDisplayDateFlag = true; if (Owner.SelectedDates.Count == 0) { @@ -882,10 +879,9 @@ namespace Avalonia.Controls.Primitives case CalendarSelectionMode.SingleRange: case CalendarSelectionMode.MultipleRange: { - Debug.Assert(b.DataContext != null, "The DataContext should not be null!"); Owner.UnHighlightDays(); Owner.HoverEndIndex = b.Index; - Owner.HoverEnd = (DateTime?)b.DataContext; + Owner.HoverEnd = selectedDate; // Update the States of the buttons Owner.HighlightDays(); return; @@ -904,22 +900,14 @@ namespace Avalonia.Controls.Primitives Owner.Focus(); } - bool ctrl, shift; - CalendarExtensions.GetMetaKeyState(e.KeyModifiers, out ctrl, out shift); - CalendarDayButton b = (CalendarDayButton)sender!; + CalendarExtensions.GetMetaKeyState(e.KeyModifiers, out var ctrl, out var shift); - if (b != null) + if (sender is CalendarDayButton b) { _isControlPressed = ctrl; - if (b.IsEnabled && !b.IsBlackout) + if (b.IsEnabled && !b.IsBlackout && b.DataContext is DateTime selectedDate) { - DateTime selectedDate = (DateTime)b.DataContext!; _isMouseLeftButtonDown = true; - // null check is added for unit tests - if (e != null) - { - _downEventArg = e; - } switch (Owner.SelectionMode) { @@ -1010,12 +998,12 @@ namespace Avalonia.Controls.Primitives } } } - private void AddSelection(CalendarDayButton b) + private void AddSelection(CalendarDayButton b, DateTime selectedDate) { if (Owner != null) { Owner.HoverEndIndex = b.Index; - Owner.HoverEnd = (DateTime)b.DataContext!; + Owner.HoverEnd = selectedDate; if (Owner.HoverEnd != null && Owner.HoverStart != null) { @@ -1025,7 +1013,7 @@ namespace Avalonia.Controls.Primitives // SelectionMode Owner.IsMouseSelection = true; Owner.SelectedDates.AddRange(Owner.HoverStart.Value, Owner.HoverEnd.Value); - Owner.OnDayClick((DateTime)b.DataContext); + Owner.OnDayClick(selectedDate); } } } @@ -1039,11 +1027,11 @@ namespace Avalonia.Controls.Primitives Owner.OnDayButtonMouseUp(e); } _isMouseLeftButtonDown = false; - if (b != null && b.DataContext != null) + if (b != null && b.DataContext is DateTime selectedDate) { if (Owner.SelectionMode == CalendarSelectionMode.None || Owner.SelectionMode == CalendarSelectionMode.SingleDate) { - Owner.OnDayClick((DateTime)b.DataContext); + Owner.OnDayClick(selectedDate); return; } if (Owner.HoverStart.HasValue) @@ -1058,14 +1046,14 @@ namespace Avalonia.Controls.Primitives Owner.RemovedItems.Add(item); } Owner.SelectedDates.ClearInternal(); - AddSelection(b); + AddSelection(b, selectedDate); return; } case CalendarSelectionMode.MultipleRange: { // add the selection (either single day or // SingleRange day) - AddSelection(b); + AddSelection(b, selectedDate); return; } } @@ -1076,7 +1064,7 @@ namespace Avalonia.Controls.Primitives // be able to switch months if (b.IsInactive && b.IsBlackout) { - Owner.OnDayClick((DateTime)b.DataContext); + Owner.OnDayClick(selectedDate); } } } @@ -1095,9 +1083,9 @@ namespace Avalonia.Controls.Primitives Owner.HoverStart = null; _isMouseLeftButtonDown = false; b.IsSelected = false; - if (b.DataContext != null) + if (b.DataContext is DateTime selectedDate) { - Owner.SelectedDates.Remove((DateTime)b.DataContext); + Owner.SelectedDates.Remove(selectedDate); } } } @@ -1107,35 +1095,26 @@ namespace Avalonia.Controls.Primitives private void Month_CalendarButtonMouseDown(object? sender, PointerPressedEventArgs e) { - CalendarButton b = (CalendarButton)sender!; - _isMouseLeftButtonDownYearView = true; - if (e != null) - { - _downEventArgYearView = e; - } - - UpdateYearViewSelection(b); + UpdateYearViewSelection(sender as CalendarButton); } internal void Month_CalendarButtonMouseUp(object? sender, PointerReleasedEventArgs e) { _isMouseLeftButtonDownYearView = false; - if (Owner != null) + if (Owner != null && (sender as CalendarButton)?.DataContext is DateTime newMonth) { - DateTime newmonth = (DateTime)((CalendarButton)sender!).DataContext!; - if (Owner.DisplayMode == CalendarMode.Year) { - Owner.DisplayDate = newmonth; + Owner.DisplayDate = newMonth; Owner.DisplayMode = CalendarMode.Month; } else { Debug.Assert(Owner.DisplayMode == CalendarMode.Decade, "The owning Calendar should be in decade mode!"); - Owner.SelectedMonth = newmonth; + Owner.SelectedMonth = newMonth; Owner.DisplayMode = CalendarMode.Year; } } @@ -1145,8 +1124,7 @@ namespace Avalonia.Controls.Primitives { if (_isMouseLeftButtonDownYearView) { - CalendarButton b = (CalendarButton)sender!; - UpdateYearViewSelection(b); + UpdateYearViewSelection(sender as CalendarButton); } } diff --git a/src/Avalonia.Controls/Calendar/DateTimeHelper.cs b/src/Avalonia.Controls/Calendar/DateTimeHelper.cs index bfff03a926..570f05cfe8 100644 --- a/src/Avalonia.Controls/Calendar/DateTimeHelper.cs +++ b/src/Avalonia.Controls/Calendar/DateTimeHelper.cs @@ -53,7 +53,7 @@ namespace Avalonia.Controls public static int CompareDays(DateTime dt1, DateTime dt2) { - return DateTime.Compare(DiscardTime(dt1).Value, DiscardTime(dt2).Value); + return DateTime.Compare(DiscardTime(dt1), DiscardTime(dt2)); } public static int CompareYearMonth(DateTime dt1, DateTime dt2) @@ -71,14 +71,9 @@ namespace Avalonia.Controls return new DateTime(d.Year, d.Month, 1, 0, 0, 0); } - [return: NotNullIfNotNull("d")] - public static DateTime? DiscardTime(DateTime? d) + public static DateTime DiscardTime(DateTime d) { - if (d == null) - { - return null; - } - return d.Value.Date; + return d.Date; } public static int EndOfDecade(DateTime date) @@ -127,28 +122,14 @@ namespace Avalonia.Controls public static string ToYearMonthPatternString(DateTime date) { - string result = string.Empty; - DateTimeFormatInfo format = GetCurrentDateFormat(); - - if (format != null) - { - result = date.ToString(format.YearMonthPattern, format); - } - - return result; + var format = GetCurrentDateFormat(); + return date.ToString(format.YearMonthPattern, format); } public static string ToYearString(DateTime date) { - string result = string.Empty; - DateTimeFormatInfo format = GetCurrentDateFormat(); - - if (format != null) - { - result = date.Year.ToString(format); - } - - return result; + var format = GetCurrentDateFormat(); + return date.Year.ToString(format); } } } diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs index b17648f5bb..869bdeabea 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs @@ -51,11 +51,11 @@ namespace Avalonia.Controls private bool _isDropDownOpen; private DateTime? _selectedDate; private string? _text; - private bool _suspendTextChangeHandler = false; - private bool _isPopupClosing = false; - private bool _ignoreButtonClick = false; - private bool _isFlyoutOpen = false; - private bool _isPressed = false; + private bool _suspendTextChangeHandler; + private bool _isPopupClosing; + private bool _ignoreButtonClick; + private bool _isFlyoutOpen; + private bool _isPressed; /// /// Occurs when the drop-down @@ -185,7 +185,7 @@ namespace Avalonia.Controls { _textBox.KeyDown += TextBox_KeyDown; _textBox.GotFocus += TextBox_GotFocus; - _textBoxTextChangedSubscription = _textBox.GetObservable(TextBox.TextProperty).Subscribe(txt => TextBox_TextChanged()); + _textBoxTextChangedSubscription = _textBox.GetObservable(TextBox.TextProperty).Subscribe(_ => TextBox_TextChanged()); if(SelectedDate.HasValue) { @@ -292,7 +292,7 @@ namespace Avalonia.Controls // Text else if (change.Property == TextProperty) { - var (oldValue, newValue) = change.GetOldAndNewValue(); + var (_, newValue) = change.GetOldAndNewValue(); if (!_suspendTextChangeHandler) { @@ -595,9 +595,9 @@ namespace Avalonia.Controls private void Calendar_KeyDown(object? sender, KeyEventArgs e) { - Calendar? c = sender as Calendar ?? throw new ArgumentException("Sender must be Calendar.", nameof(sender)); - - if (!e.Handled && (e.Key == Key.Enter || e.Key == Key.Space || e.Key == Key.Escape) && c.DisplayMode == CalendarMode.Month) + if (!e.Handled + && sender is Calendar { DisplayMode: CalendarMode.Month } + && (e.Key == Key.Enter || e.Key == Key.Space || e.Key == Key.Escape)) { Focus(); IsDropDownOpen = false; diff --git a/src/Avalonia.Controls/Chrome/TitleBar.cs b/src/Avalonia.Controls/Chrome/TitleBar.cs index 47b0bb6e2d..368c9d4c2f 100644 --- a/src/Avalonia.Controls/Chrome/TitleBar.cs +++ b/src/Avalonia.Controls/Chrome/TitleBar.cs @@ -17,28 +17,26 @@ namespace Avalonia.Controls.Chrome private void UpdateSize(Window window) { - if (window != null) + Margin = new Thickness( + window.OffScreenMargin.Left, + window.OffScreenMargin.Top, + window.OffScreenMargin.Right, + window.OffScreenMargin.Bottom); + + if (window.WindowState != WindowState.FullScreen) { - Margin = new Thickness( - window.OffScreenMargin.Left, - window.OffScreenMargin.Top, - window.OffScreenMargin.Right, - window.OffScreenMargin.Bottom); + Height = window.WindowDecorationMargin.Top; - if (window.WindowState != WindowState.FullScreen) + if (_captionButtons != null) { - Height = window.WindowDecorationMargin.Top; - - if (_captionButtons != null) - { - _captionButtons.Height = Height; - } + _captionButtons.Height = Height; } - - IsVisible = window.PlatformImpl?.NeedsManagedDecorations ?? false; } + + IsVisible = window.PlatformImpl?.NeedsManagedDecorations ?? false; } + /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); @@ -55,6 +53,7 @@ namespace Avalonia.Controls.Chrome } } + /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); @@ -64,13 +63,13 @@ namespace Avalonia.Controls.Chrome _disposables = new CompositeDisposable(6) { window.GetObservable(Window.WindowDecorationMarginProperty) - .Subscribe(x => UpdateSize(window)), + .Subscribe(_ => UpdateSize(window)), window.GetObservable(Window.ExtendClientAreaTitleBarHeightHintProperty) - .Subscribe(x => UpdateSize(window)), + .Subscribe(_ => UpdateSize(window)), window.GetObservable(Window.OffScreenMarginProperty) - .Subscribe(x => UpdateSize(window)), + .Subscribe(_ => UpdateSize(window)), window.GetObservable(Window.ExtendClientAreaChromeHintsProperty) - .Subscribe(x => UpdateSize(window)), + .Subscribe(_ => UpdateSize(window)), window.GetObservable(Window.WindowStateProperty) .Subscribe(x => { @@ -80,11 +79,12 @@ namespace Avalonia.Controls.Chrome PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen); }), window.GetObservable(Window.IsExtendedIntoWindowDecorationsProperty) - .Subscribe(x => UpdateSize(window)) + .Subscribe(_ => UpdateSize(window)) }; } } + /// protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); diff --git a/src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs b/src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs index 9d859a753a..18d668e9a4 100644 --- a/src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs +++ b/src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs @@ -14,7 +14,6 @@ namespace Avalonia.Controls.Converters public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) { if (parameter == null || - values == null || values.Count != 4 || !(values[0] is ScrollBarVisibility visibility) || !(values[1] is double offset) || diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 5c35a09f1c..eb587fb157 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -21,9 +21,9 @@ namespace Avalonia.Controls /// /// SharedSizeGroup property. /// - public string SharedSizeGroup + public string? SharedSizeGroup { - get { return (string)GetValue(SharedSizeGroupProperty); } + get { return GetValue(SharedSizeGroupProperty); } set { SetValue(SharedSizeGroupProperty, value); } } @@ -32,20 +32,15 @@ namespace Avalonia.Controls /// internal void OnEnterParentTree() { - this.InheritanceParent = Parent; + InheritanceParent = Parent; if (_sharedState == null) { // start with getting SharedSizeGroup value. // this property is NOT inherited which should result in better overall perf. - string sharedSizeGroupId = SharedSizeGroup; - if (sharedSizeGroupId != null) + if (SharedSizeGroup is { } sharedSizeGroupId && PrivateSharedSizeScope is { } privateSharedSizeScope) { - SharedSizeScope? privateSharedSizeScope = PrivateSharedSizeScope; - if (privateSharedSizeScope != null) - { - _sharedState = privateSharedSizeScope.EnsureSharedState(sharedSizeGroupId); - _sharedState.AddMember(this); - } + _sharedState = privateSharedSizeScope.EnsureSharedState(sharedSizeGroupId); + _sharedState.AddMember(this); } } @@ -321,13 +316,12 @@ namespace Avalonia.Controls return ((_flags & flags) == flags); } - private static void OnSharedSizeGroupPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) + private static void OnSharedSizeGroupPropertyChanged(DefinitionBase definition, + AvaloniaPropertyChangedEventArgs e) { - DefinitionBase definition = (DefinitionBase)d; - if (definition.Parent != null) { - string sharedSizeGroupId = (string)e.NewValue!; + string? sharedSizeGroupId = e.NewValue.Value; if (definition._sharedState != null) { @@ -337,16 +331,14 @@ namespace Avalonia.Controls definition._sharedState = null; } - if ((definition._sharedState == null) && (sharedSizeGroupId != null)) + if (definition._sharedState == null + && sharedSizeGroupId != null + && definition.PrivateSharedSizeScope is { } privateSharedSizeScope) { - SharedSizeScope? privateSharedSizeScope = definition.PrivateSharedSizeScope; - if (privateSharedSizeScope != null) - { - // if definition is not registered and both: shared size group id AND private shared scope - // are available, then register definition. - definition._sharedState = privateSharedSizeScope.EnsureSharedState(sharedSizeGroupId); - definition._sharedState.AddMember(definition); - } + // if definition is not registered and both: shared size group id AND private shared scope + // are available, then register definition. + definition._sharedState = privateSharedSizeScope.EnsureSharedState(sharedSizeGroupId); + definition._sharedState.AddMember(definition); } } } @@ -357,17 +349,15 @@ namespace Avalonia.Controls /// b) contains only letters, digits and underscore ('_'). /// c) does not start with a digit. /// - private static bool SharedSizeGroupPropertyValueValid(string value) + private static bool SharedSizeGroupPropertyValueValid(string? id) { // null is default value - if (value == null) + if (id == null) { return true; } - string id = (string)value; - - if (!string.IsNullOrEmpty(id)) + if (id.Length > 0) { int i = -1; while (++i < id.Length) @@ -397,14 +387,11 @@ namespace Avalonia.Controls /// existing scope just left. In both cases if the DefinitionBase object is already registered /// in SharedSizeState, it should un-register and register itself in a new one. /// - private static void OnPrivateSharedSizeScopePropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) + private static void OnPrivateSharedSizeScopePropertyChanged(DefinitionBase definition, + AvaloniaPropertyChangedEventArgs e) { - DefinitionBase definition = (DefinitionBase)d; - if (definition.Parent != null) { - SharedSizeScope privateSharedSizeScope = (SharedSizeScope)e.NewValue!; - if (definition._sharedState != null) { // if definition is already registered And shared size scope is changing, @@ -413,16 +400,14 @@ namespace Avalonia.Controls definition._sharedState = null; } - if ((definition._sharedState == null) && (privateSharedSizeScope != null)) + if (definition._sharedState == null + && e.NewValue.Value is { } privateSharedSizeScope + && definition.SharedSizeGroup is { } sharedSizeGroup) { - string sharedSizeGroup = definition.SharedSizeGroup; - if (sharedSizeGroup != null) - { - // if definition is not registered and both: shared size group id AND private shared scope - // are available, then register definition. - definition._sharedState = privateSharedSizeScope.EnsureSharedState(definition.SharedSizeGroup); - definition._sharedState.AddMember(definition); - } + // if definition is not registered and both: shared size group id AND private shared scope + // are available, then register definition. + definition._sharedState = privateSharedSizeScope.EnsureSharedState(sharedSizeGroup); + definition._sharedState.AddMember(definition); } } } @@ -432,7 +417,7 @@ namespace Avalonia.Controls /// private SharedSizeScope? PrivateSharedSizeScope { - get { return (SharedSizeScope?)GetValue(PrivateSharedSizeScopeProperty); } + get { return GetValue(PrivateSharedSizeScopeProperty); } } /// @@ -465,7 +450,7 @@ namespace Avalonia.Controls private SharedSizeState? _sharedState; // reference to shared state object this instance is registered with - [System.Flags] + [Flags] private enum Flags : byte { // @@ -520,11 +505,10 @@ namespace Avalonia.Controls /// internal SharedSizeState(SharedSizeScope sharedSizeScope, string sharedSizeGroupId) { - Debug.Assert(sharedSizeScope != null && sharedSizeGroupId != null); _sharedSizeScope = sharedSizeScope; _sharedSizeGroupId = sharedSizeGroupId; _registry = new List(); - _layoutUpdated = new EventHandler(OnLayoutUpdated); + _layoutUpdated = OnLayoutUpdated; _broadcastInvalidation = true; } @@ -568,7 +552,7 @@ namespace Avalonia.Controls { for (int i = 0, count = _registry.Count; i < count; ++i) { - Grid parentGrid = (Grid)(_registry[i].Parent!); + Grid parentGrid = _registry[i].Parent!; parentGrid.Invalidate(); } _broadcastInvalidation = false; @@ -703,7 +687,7 @@ namespace Avalonia.Controls // measure is invalid - it used the old shared size, // which is larger than d's (possibly changed) minSize measureIsValid = (definitionBase.LayoutWasUpdated && - MathUtilities.GreaterThanOrClose(definitionBase._minSize, this.MinSize)); + MathUtilities.GreaterThanOrClose(definitionBase._minSize, MinSize)); } if(!measureIsValid) @@ -786,8 +770,8 @@ namespace Avalonia.Controls /// /// /// - public static readonly AttachedProperty SharedSizeGroupProperty = - AvaloniaProperty.RegisterAttached( + public static readonly AttachedProperty SharedSizeGroupProperty = + AvaloniaProperty.RegisterAttached( "SharedSizeGroup", validate: SharedSizeGroupPropertyValueValid); @@ -796,8 +780,8 @@ namespace Avalonia.Controls /// static DefinitionBase() { - SharedSizeGroupProperty.Changed.AddClassHandler(OnSharedSizeGroupPropertyChanged); - PrivateSharedSizeScopeProperty.Changed.AddClassHandler(OnPrivateSharedSizeScopePropertyChanged); + SharedSizeGroupProperty.Changed.AddClassHandler(OnSharedSizeGroupPropertyChanged); + PrivateSharedSizeScopeProperty.Changed.AddClassHandler(OnPrivateSharedSizeScopePropertyChanged); } /// diff --git a/src/Avalonia.Controls/DockPanel.cs b/src/Avalonia.Controls/DockPanel.cs index 3e3ed509b5..1a0cf1644a 100644 --- a/src/Avalonia.Controls/DockPanel.cs +++ b/src/Avalonia.Controls/DockPanel.cs @@ -101,9 +101,6 @@ namespace Avalonia.Controls Size childConstraint; // Contains the suggested input constraint for this child. Size childDesiredSize; // Contains the return size from child measure. - if (child == null) - { continue; } - // Child constraint is the remaining size; this is total size minus size consumed by previous children. childConstraint = new Size(Math.Max(0.0, constraint.Width - accumulatedWidth), Math.Max(0.0, constraint.Height - accumulatedHeight)); @@ -122,7 +119,7 @@ namespace Avalonia.Controls // will deal with computing our minimum size (parentSize) due to that accumulation. // Therefore, we only need to compute our minimum size (parentSize) in dimensions that this child does // not accumulate: Width for Top/Bottom, Height for Left/Right. - switch (DockPanel.GetDock((Control)child)) + switch (GetDock(child)) { case Dock.Left: case Dock.Right: @@ -164,8 +161,6 @@ namespace Avalonia.Controls for (int i = 0; i < totalChildrenCount; ++i) { var child = children[i]; - if (child == null) - { continue; } Size childDesiredSize = child.DesiredSize; Rect rcChild = new Rect( @@ -176,7 +171,7 @@ namespace Avalonia.Controls if (i < nonFillChildrenCount) { - switch (DockPanel.GetDock((Control)child)) + switch (GetDock(child)) { case Dock.Left: accumulatedLeft += childDesiredSize.Width; diff --git a/src/Avalonia.Controls/Documents/Inline.cs b/src/Avalonia.Controls/Documents/Inline.cs index 47581e87f1..23b806583e 100644 --- a/src/Avalonia.Controls/Documents/Inline.cs +++ b/src/Avalonia.Controls/Documents/Inline.cs @@ -13,8 +13,8 @@ namespace Avalonia.Controls.Documents /// /// AvaloniaProperty for property. /// - public static readonly StyledProperty TextDecorationsProperty = - AvaloniaProperty.Register( + public static readonly StyledProperty TextDecorationsProperty = + AvaloniaProperty.Register( nameof(TextDecorations)); /// @@ -28,7 +28,7 @@ namespace Avalonia.Controls.Documents /// /// The TextDecorations property specifies decorations that are added to the text of an element. /// - public TextDecorationCollection TextDecorations + public TextDecorationCollection? TextDecorations { get { return GetValue(TextDecorationsProperty); } set { SetValue(TextDecorationsProperty, value); } @@ -83,7 +83,8 @@ namespace Avalonia.Controls.Documents return new GenericTextRunProperties(new Typeface(FontFamily, fontStyle, fontWeight), FontSize, textDecorations, Foreground, background, BaselineAlignment); } - + + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); diff --git a/src/Avalonia.Controls/Documents/Span.cs b/src/Avalonia.Controls/Documents/Span.cs index a7a702ceae..d3565cbdd5 100644 --- a/src/Avalonia.Controls/Documents/Span.cs +++ b/src/Avalonia.Controls/Documents/Span.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Text; using Avalonia.Media.TextFormatting; @@ -51,6 +52,7 @@ namespace Avalonia.Controls.Documents } } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -68,26 +70,26 @@ namespace Avalonia.Controls.Documents { base.OnInlineHostChanged(oldValue, newValue); - if (Inlines is not null) - { - Inlines.InlineHost = newValue; - } + Inlines.InlineHost = newValue; } private void OnInlinesChanged(InlineCollection? oldValue, InlineCollection? newValue) { + void OnInlinesInvalidated(object? sender, EventArgs e) + => InlineHost?.Invalidate(); + if (oldValue is not null) { oldValue.LogicalChildren = null; oldValue.InlineHost = null; - oldValue.Invalidated -= (s, e) => InlineHost?.Invalidate(); + oldValue.Invalidated -= OnInlinesInvalidated; } if (newValue is not null) { newValue.LogicalChildren = LogicalChildren; newValue.InlineHost = InlineHost; - newValue.Invalidated += (s, e) => InlineHost?.Invalidate(); + newValue.Invalidated += OnInlinesInvalidated; } } } diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 7737fdac2e..2501440ff2 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -164,20 +164,21 @@ namespace Avalonia.Controls /// /// Returns a ColumnDefinitions of column definitions. /// + [MemberNotNull(nameof(_extData))] public ColumnDefinitions ColumnDefinitions { get { - if (_data == null) { _data = new ExtendedData(); } - if (_data.ColumnDefinitions == null) { _data.ColumnDefinitions = new ColumnDefinitions() { Parent = this }; } + if (_extData == null) { _extData = new ExtendedData(); } + if (_extData.ColumnDefinitions == null) { _extData.ColumnDefinitions = new ColumnDefinitions() { Parent = this }; } - return (_data.ColumnDefinitions); + return (_extData.ColumnDefinitions); } set { - if (_data == null) { _data = new ExtendedData(); } - _data.ColumnDefinitions = value; - _data.ColumnDefinitions.Parent = this; + if (_extData == null) { _extData = new ExtendedData(); } + _extData.ColumnDefinitions = value; + _extData.ColumnDefinitions.Parent = this; InvalidateMeasure(); } } @@ -185,20 +186,21 @@ namespace Avalonia.Controls /// /// Returns a RowDefinitions of row definitions. /// + [MemberNotNull(nameof(_extData))] public RowDefinitions RowDefinitions { get { - if (_data == null) { _data = new ExtendedData(); } - if (_data.RowDefinitions == null) { _data.RowDefinitions = new RowDefinitions() { Parent = this }; } + if (_extData == null) { _extData = new ExtendedData(); } + if (_extData.RowDefinitions == null) { _extData.RowDefinitions = new RowDefinitions() { Parent = this }; } - return (_data.RowDefinitions); + return (_extData.RowDefinitions); } set { - if (_data == null) { _data = new ExtendedData(); } - _data.RowDefinitions = value; - _data.RowDefinitions.Parent = this; + if (_extData == null) { _extData = new ExtendedData(); } + _extData.RowDefinitions = value; + _extData.RowDefinitions.Parent = this; InvalidateMeasure(); } } @@ -211,7 +213,7 @@ namespace Avalonia.Controls protected override Size MeasureOverride(Size constraint) { Size gridDesiredSize; - ExtendedData extData = ExtData; + var extData = _extData; try { @@ -221,17 +223,14 @@ namespace Avalonia.Controls if (extData == null) { gridDesiredSize = new Size(); - var children = this.Children; + var children = Children; for (int i = 0, count = children.Count; i < count; ++i) { var child = children[i]; - if (child != null) - { - child.Measure(constraint); - gridDesiredSize = new Size(Math.Max(gridDesiredSize.Width, child.DesiredSize.Width), - Math.Max(gridDesiredSize.Height, child.DesiredSize.Height)); - } + child.Measure(constraint); + gridDesiredSize = new Size(Math.Max(gridDesiredSize.Width, child.DesiredSize.Width), + Math.Max(gridDesiredSize.Height, child.DesiredSize.Height)); } } else @@ -512,17 +511,14 @@ namespace Avalonia.Controls { ArrangeOverrideInProgress = true; - if (_data == null) + if (_extData is null) { - var children = this.Children; + var children = Children; for (int i = 0, count = children.Count; i < count; ++i) { var child = children[i]; - if (child != null) - { - child.Arrange(new Rect(arrangeSize)); - } + child.Arrange(new Rect(arrangeSize)); } } else @@ -532,15 +528,11 @@ namespace Avalonia.Controls SetFinalSize(DefinitionsU, arrangeSize.Width, true); SetFinalSize(DefinitionsV, arrangeSize.Height, false); - var children = this.Children; + var children = Children; for (int currentCell = 0; currentCell < PrivateCells.Length; ++currentCell) { var cell = children[currentCell]; - if (cell == null) - { - continue; - } int columnIndex = PrivateCells[currentCell].ColumnIndex; int rowIndex = PrivateCells[currentCell].RowIndex; @@ -599,7 +591,7 @@ namespace Avalonia.Controls { double value = 0.0; - Debug.Assert(_data != null); + Debug.Assert(_extData != null); // actual value calculations require structure to be up-to-date if (!ColumnDefinitionsDirty) @@ -621,7 +613,7 @@ namespace Avalonia.Controls { double value = 0.0; - Debug.Assert(_data != null); + Debug.Assert(_extData != null); // actual value calculations require structure to be up-to-date if (!RowDefinitionsDirty) @@ -654,18 +646,20 @@ namespace Avalonia.Controls /// /// Convenience accessor to ValidDefinitionsUStructure bit flag. /// + [MemberNotNull(nameof(_extData))] internal bool ColumnDefinitionsDirty { - get => ColumnDefinitions?.IsDirty ?? false; + get => ColumnDefinitions.IsDirty; set => ColumnDefinitions.IsDirty = value; } /// /// Convenience accessor to ValidDefinitionsVStructure bit flag. /// + [MemberNotNull(nameof(_extData))] internal bool RowDefinitionsDirty { - get => RowDefinitions?.IsDirty ?? false; + get => RowDefinitions.IsDirty; set => RowDefinitions.IsDirty = value; } @@ -686,8 +680,10 @@ namespace Avalonia.Controls /// private void ValidateCellsCore() { - var children = this.Children; - ExtendedData extData = ExtData; + Debug.Assert(_extData is not null); + + var children = Children; + var extData = _extData!; extData.CellCachesCollection = new CellCache[children.Count]; extData.CellGroup1 = int.MaxValue; @@ -702,10 +698,6 @@ namespace Avalonia.Controls for (int i = PrivateCells.Length - 1; i >= 0; --i) { var child = children[i]; - if (child == null) - { - continue; - } CellCache cell = new CellCache(); @@ -713,19 +705,19 @@ namespace Avalonia.Controls // Read indices from the corresponding properties: // clamp to value < number_of_columns // column >= 0 is guaranteed by property value validation callback - cell.ColumnIndex = Math.Min(GetColumn((Control)child), DefinitionsU.Count - 1); + cell.ColumnIndex = Math.Min(GetColumn(child), DefinitionsU.Count - 1); // clamp to value < number_of_rows // row >= 0 is guaranteed by property value validation callback - cell.RowIndex = Math.Min(GetRow((Control)child), DefinitionsV.Count - 1); + cell.RowIndex = Math.Min(GetRow(child), DefinitionsV.Count - 1); // Read span properties: // clamp to not exceed beyond right side of the grid // column_span > 0 is guaranteed by property value validation callback - cell.ColumnSpan = Math.Min(GetColumnSpan((Control)child), DefinitionsU.Count - cell.ColumnIndex); + cell.ColumnSpan = Math.Min(GetColumnSpan(child), DefinitionsU.Count - cell.ColumnIndex); // clamp to not exceed beyond bottom side of the grid // row_span > 0 is guaranteed by property value validation callback - cell.RowSpan = Math.Min(GetRowSpan((Control)child), DefinitionsV.Count - cell.RowIndex); + cell.RowSpan = Math.Min(GetRowSpan(child), DefinitionsV.Count - cell.RowIndex); Debug.Assert(0 <= cell.ColumnIndex && cell.ColumnIndex < DefinitionsU.Count); Debug.Assert(0 <= cell.RowIndex && cell.RowIndex < DefinitionsV.Count); @@ -792,7 +784,7 @@ namespace Avalonia.Controls { if (ColumnDefinitionsDirty) { - ExtendedData extData = ExtData; + var extData = _extData; if (extData.ColumnDefinitions == null) { @@ -818,7 +810,7 @@ namespace Avalonia.Controls ColumnDefinitionsDirty = false; } - Debug.Assert(ExtData.DefinitionsU != null && ExtData.DefinitionsU.Count > 0); + Debug.Assert(_extData is { DefinitionsU.Count: > 0 }); } /// @@ -833,7 +825,7 @@ namespace Avalonia.Controls { if (RowDefinitionsDirty) { - ExtendedData extData = ExtData; + var extData = _extData; if (extData.RowDefinitions == null) { @@ -859,7 +851,7 @@ namespace Avalonia.Controls RowDefinitionsDirty = false; } - Debug.Assert(ExtData.DefinitionsV != null && ExtData.DefinitionsV.Count > 0); + Debug.Assert(_extData is { DefinitionsV.Count: > 0 }); } /// @@ -965,8 +957,7 @@ namespace Avalonia.Controls bool ignoreDesiredSizeU, bool forceInfinityV) { - bool unusedHasDesiredSizeUChanged; - MeasureCellsGroup(cellsHead, referenceSize, ignoreDesiredSizeU, forceInfinityV, out unusedHasDesiredSizeUChanged); + MeasureCellsGroup(cellsHead, referenceSize, ignoreDesiredSizeU, forceInfinityV, out _); } /// @@ -994,7 +985,7 @@ namespace Avalonia.Controls return; } - var children = this.Children; + var children = Children; Hashtable? spanStore = null; bool ignoreDesiredSizeV = forceInfinityV; @@ -1101,8 +1092,6 @@ namespace Avalonia.Controls int cell, bool forceInfinityV) { - - double cellMeasureWidth; double cellMeasureHeight; @@ -1144,15 +1133,9 @@ namespace Avalonia.Controls } - var child = this.Children[cell]; - if (child != null) - { - Size childConstraint = new Size(cellMeasureWidth, cellMeasureHeight); - child.Measure(childConstraint); - } - - - + var child = Children[cell]; + Size childConstraint = new Size(cellMeasureWidth, cellMeasureHeight); + child.Measure(childConstraint); } /// @@ -1230,7 +1213,7 @@ namespace Avalonia.Controls // avoid processing when asked to distribute "0" if (!MathUtilities.IsZero(requestedSize)) { - DefinitionBase[] tempDefinitions = TempDefinitions; // temp array used to remember definitions for sorting + DefinitionBase?[] tempDefinitions = TempDefinitions; // temp array used to remember definitions for sorting int end = start + count; int autoDefinitionsCount = 0; double rangeMinSize = 0; @@ -1288,20 +1271,24 @@ namespace Avalonia.Controls Array.Sort(tempDefinitions, 0, count, s_spanPreferredDistributionOrderComparer); for (i = 0, sizeToDistribute = requestedSize; i < autoDefinitionsCount; ++i) { + var tempDefinition = tempDefinitions[i]!; + // sanity check: only auto definitions allowed in this loop - Debug.Assert(tempDefinitions[i].UserSize.IsAuto); + Debug.Assert(tempDefinition.UserSize.IsAuto); // adjust sizeToDistribute value by subtracting auto definition min size - sizeToDistribute -= (tempDefinitions[i].MinSize); + sizeToDistribute -= (tempDefinition.MinSize); } for (; i < count; ++i) { + var tempDefinition = tempDefinitions[i]!; + // sanity check: no auto definitions allowed in this loop - Debug.Assert(!tempDefinitions[i].UserSize.IsAuto); + Debug.Assert(!tempDefinition.UserSize.IsAuto); - double newMinSize = Math.Min(sizeToDistribute / (count - i), tempDefinitions[i].PreferredSize); - if (newMinSize > tempDefinitions[i].MinSize) { tempDefinitions[i].UpdateMinSize(newMinSize); } + double newMinSize = Math.Min(sizeToDistribute / (count - i), tempDefinition.PreferredSize); + if (newMinSize > tempDefinition.MinSize) { tempDefinition.UpdateMinSize(newMinSize); } sizeToDistribute -= newMinSize; } @@ -1325,24 +1312,28 @@ namespace Avalonia.Controls Array.Sort(tempDefinitions, 0, count, s_spanMaxDistributionOrderComparer); for (i = 0, sizeToDistribute = requestedSize - rangePreferredSize; i < count - autoDefinitionsCount; ++i) { + var tempDefinition = tempDefinitions[i]!; + // sanity check: no auto definitions allowed in this loop - Debug.Assert(!tempDefinitions[i].UserSize.IsAuto); + Debug.Assert(!tempDefinition.UserSize.IsAuto); - double preferredSize = tempDefinitions[i].PreferredSize; + double preferredSize = tempDefinition.PreferredSize; double newMinSize = preferredSize + sizeToDistribute / (count - autoDefinitionsCount - i); - tempDefinitions[i].UpdateMinSize(Math.Min(newMinSize, tempDefinitions[i].SizeCache)); - sizeToDistribute -= (tempDefinitions[i].MinSize - preferredSize); + tempDefinition.UpdateMinSize(Math.Min(newMinSize, tempDefinition.SizeCache)); + sizeToDistribute -= (tempDefinition.MinSize - preferredSize); } for (; i < count; ++i) { + var tempDefinition = tempDefinitions[i]!; + // sanity check: only auto definitions allowed in this loop - Debug.Assert(tempDefinitions[i].UserSize.IsAuto); + Debug.Assert(tempDefinition.UserSize.IsAuto); - double preferredSize = tempDefinitions[i].MinSize; + double preferredSize = tempDefinition.MinSize; double newMinSize = preferredSize + sizeToDistribute / (count - i); - tempDefinitions[i].UpdateMinSize(Math.Min(newMinSize, tempDefinitions[i].SizeCache)); - sizeToDistribute -= (tempDefinitions[i].MinSize - preferredSize); + tempDefinition.UpdateMinSize(Math.Min(newMinSize, tempDefinition.SizeCache)); + sizeToDistribute -= (tempDefinition.MinSize - preferredSize); } // sanity check: requested size must all be distributed @@ -1376,8 +1367,10 @@ namespace Avalonia.Controls for (int i = 0; i < count; ++i) { - double deltaSize = (maxMaxSize - tempDefinitions[i].SizeCache) * sizeToDistribute / totalRemainingSize; - tempDefinitions[i].UpdateMinSize(tempDefinitions[i].SizeCache + deltaSize); + var tempDefinition = tempDefinitions[i]!; + + double deltaSize = (maxMaxSize - tempDefinition.SizeCache) * sizeToDistribute / totalRemainingSize; + tempDefinition.UpdateMinSize(tempDefinition.SizeCache + deltaSize); } } else @@ -1388,7 +1381,7 @@ namespace Avalonia.Controls // for (int i = 0; i < count; ++i) { - tempDefinitions[i].UpdateMinSize(equalSize); + tempDefinitions[i]!.UpdateMinSize(equalSize); } } } @@ -1429,7 +1422,7 @@ namespace Avalonia.Controls double availableSize) { int defCount = definitions.Count; - DefinitionBase[] tempDefinitions = TempDefinitions; + DefinitionBase?[] tempDefinitions = TempDefinitions; int minCount = 0, maxCount = 0; double takenSize = 0; double totalStarWeight = 0.0; @@ -1560,8 +1553,8 @@ namespace Avalonia.Controls remainingStarWeight = totalStarWeight - takenStarWeight; } - double minRatio = (minCount > 0) ? tempDefinitions[minCount - 1].MeasureSize : Double.PositiveInfinity; - double maxRatio = (maxCount > 0) ? tempDefinitions[defCount + maxCount - 1].SizeCache : -1.0; + double minRatio = (minCount > 0) ? tempDefinitions[minCount - 1]!.MeasureSize : Double.PositiveInfinity; + double maxRatio = (maxCount > 0) ? tempDefinitions[defCount + maxCount - 1]!.SizeCache : -1.0; // choose the def with larger ratio to the current proportion ("max discrepancy") double proportion = remainingStarWeight / remainingAvailableSize; @@ -1579,13 +1572,13 @@ namespace Avalonia.Controls double resolvedSize; if (chooseMin == true) { - resolvedDef = tempDefinitions[minCount - 1]; + resolvedDef = tempDefinitions[minCount - 1]!; resolvedSize = resolvedDef.MinSize; --minCount; } else { - resolvedDef = tempDefinitions[defCount + maxCount - 1]; + resolvedDef = tempDefinitions[defCount + maxCount - 1]!; resolvedSize = Math.Max(resolvedDef.MinSize, resolvedDef.UserMaxSize); --maxCount; } @@ -1603,12 +1596,12 @@ namespace Avalonia.Controls // advance to the next candidate defs, removing ones that have been resolved. // Both counts are advanced, as a def might appear in both lists. - while (minCount > 0 && tempDefinitions[minCount - 1].MeasureSize < 0.0) + while (minCount > 0 && tempDefinitions[minCount - 1]!.MeasureSize < 0.0) { --minCount; tempDefinitions[minCount] = null!; } - while (maxCount > 0 && tempDefinitions[defCount + maxCount - 1].MeasureSize < 0.0) + while (maxCount > 0 && tempDefinitions[defCount + maxCount - 1]!.MeasureSize < 0.0) { --maxCount; tempDefinitions[defCount + maxCount] = null!; @@ -1637,8 +1630,7 @@ namespace Avalonia.Controls // resolved as 'min'. Their allocation can be increased to make up the gap. for (int i = minCount; i < minCountPhase2; ++i) { - DefinitionBase def = tempDefinitions[i]; - if (def != null) + if (tempDefinitions[i] is { } def) { def.MeasureSize = 1.0; // mark as 'not yet resolved' ++starCount; @@ -1653,8 +1645,7 @@ namespace Avalonia.Controls // resolved as 'max'. Their allocation can be decreased to make up the gap. for (int i = maxCount; i < maxCountPhase2; ++i) { - DefinitionBase def = tempDefinitions[defCount + i]; - if (def != null) + if (tempDefinitions[defCount + i] is { } def) { def.MeasureSize = 1.0; // mark as 'not yet resolved' ++starCount; @@ -1695,7 +1686,7 @@ namespace Avalonia.Controls totalStarWeight = 0.0; for (int i = 0; i < starCount; ++i) { - DefinitionBase def = tempDefinitions[i]; + DefinitionBase def = tempDefinitions[i]!; totalStarWeight += def.MeasureSize; def.SizeCache = totalStarWeight; } @@ -1703,7 +1694,7 @@ namespace Avalonia.Controls // resolve the defs, in decreasing order of weight for (int i = starCount - 1; i >= 0; --i) { - DefinitionBase def = tempDefinitions[i]; + DefinitionBase def = tempDefinitions[i]!; double resolvedSize = (def.MeasureSize > 0.0) ? Math.Max(availableSize - takenSize, 0.0) * (def.MeasureSize / def.SizeCache) : 0.0; // min and max should have no effect by now, but just in case... @@ -2095,7 +2086,7 @@ namespace Avalonia.Controls { // DpiScale dpiScale = GetDpi(); // double dpi = columns ? dpiScale.DpiScaleX : dpiScale.DpiScaleY; - var dpi = (VisualRoot as Layout.ILayoutRoot)?.LayoutScaling ?? 1.0; + var dpi = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1.0; double[] roundingErrors = RoundingErrors; double roundedTakenSize = 0.0; @@ -2302,8 +2293,7 @@ namespace Avalonia.Controls /// private void SetValid() { - ExtendedData extData = ExtData; - if (extData != null) + if (_extData is { } extData) { // for (int i = 0; i < PrivateColumnCount; ++i) DefinitionsU[i].SetValid (); // for (int i = 0; i < PrivateRowCount; ++i) DefinitionsV[i].SetValid (); @@ -2330,12 +2320,12 @@ namespace Avalonia.Controls if (ShowGridLines && (_gridLinesRenderer == null)) { _gridLinesRenderer = new GridLinesRenderer(); - this.VisualChildren.Add(_gridLinesRenderer); + VisualChildren.Add(_gridLinesRenderer); } if ((!ShowGridLines) && (_gridLinesRenderer != null)) { - this.VisualChildren.Add(_gridLinesRenderer); + VisualChildren.Add(_gridLinesRenderer); _gridLinesRenderer = null; } @@ -2364,7 +2354,7 @@ namespace Avalonia.Controls { Grid grid = (Grid)d; - if (grid.ExtData != null // trivial grid is 1 by 1. there is no grid lines anyway + if (grid._extData != null // trivial grid is 1 by 1. there is no grid lines anyway && grid.ListenToNotifications) { grid.InvalidateVisual(); @@ -2375,13 +2365,11 @@ namespace Avalonia.Controls private static void OnCellAttachedPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) { - Visual? child = d as Visual; - - if (child != null) + if (d is Visual child) { Grid? grid = child.GetVisualParent(); if (grid != null - && grid.ExtData != null + && grid._extData != null && grid.ListenToNotifications) { grid.CellsStructureDirty = true; @@ -2427,7 +2415,7 @@ namespace Avalonia.Controls /// private IReadOnlyList DefinitionsU { - get { return (ExtData.DefinitionsU!); } + get { return _extData!.DefinitionsU!; } } /// @@ -2435,17 +2423,19 @@ namespace Avalonia.Controls /// private IReadOnlyList DefinitionsV { - get { return (ExtData.DefinitionsV!); } + get { return _extData!.DefinitionsV!; } } /// /// Helper accessor to layout time array of definitions. /// - private DefinitionBase[] TempDefinitions + private DefinitionBase?[] TempDefinitions { get { - ExtendedData extData = ExtData; + Debug.Assert(_extData is not null); + + var extData = _extData!; int requiredLength = Math.Max(DefinitionsU.Count, DefinitionsV.Count) * 2; if (extData.TempDefinitions == null @@ -2516,7 +2506,7 @@ namespace Avalonia.Controls /// private CellCache[] PrivateCells { - get { return (ExtData.CellCachesCollection!); } + get { return _extData!.CellCachesCollection!; } } /// @@ -2582,18 +2572,10 @@ namespace Avalonia.Controls set { SetFlags(value, Flags.HasGroup3CellsInAutoRows); } } - /// - /// Returns reference to extended data bag. - /// - private ExtendedData ExtData - { - get { return (_data!); } - } - /// /// Returns *-weight, adjusted for scale computed during Phase 1 /// - static double StarWeight(DefinitionBase def, double scale) + private static double StarWeight(DefinitionBase def, double scale) { if (scale < 0.0) { @@ -2609,17 +2591,17 @@ namespace Avalonia.Controls } // Extended data instantiated on demand, for non-trivial case handling only - private ExtendedData? _data; + private ExtendedData? _extData; // Grid validity / property caches dirtiness flags private Flags _flags; private GridLinesRenderer? _gridLinesRenderer; // Keeps track of definition indices. - int[]? _definitionIndices; + private int[]? _definitionIndices; // Stores unrounded values and rounding errors during layout rounding. - double[]? _roundingErrors; + private double[]? _roundingErrors; // 5 is an arbitrary constant chosen to end the measure loop private const int c_layoutLoopMaxCount = 5; @@ -2645,14 +2627,14 @@ namespace Avalonia.Controls internal int CellGroup2; // index of the first cell in second cell group internal int CellGroup3; // index of the first cell in third cell group internal int CellGroup4; // index of the first cell in forth cell group - internal DefinitionBase[]? TempDefinitions; // temporary array used during layout for various purposes + internal DefinitionBase?[]? TempDefinitions; // temporary array used during layout for various purposes // TempDefinitions.Length == Max(definitionsU.Length, definitionsV.Length) } /// /// Grid validity / property caches dirtiness flags /// - [System.Flags] + [Flags] private enum Flags { // @@ -2768,7 +2750,7 @@ namespace Avalonia.Controls /// /// LayoutTimeSizeType is used internally and reflects layout-time size type. /// - [System.Flags] + [Flags] internal enum LayoutTimeSizeType : byte { None = 0x00, @@ -3317,7 +3299,7 @@ namespace Avalonia.Controls internal void UpdateRenderBounds(Size arrangeSize) { _lastArrangeSize = arrangeSize; - this.InvalidateVisual(); + InvalidateVisual(); } private static Size _lastArrangeSize; diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index 7408bff902..2cf0fc3ec4 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -14,8 +14,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty SourceProperty = - AvaloniaProperty.Register(nameof(Source)); + public static readonly StyledProperty SourceProperty = + AvaloniaProperty.Register(nameof(Source)); /// /// Defines the property. @@ -42,7 +42,7 @@ namespace Avalonia.Controls /// Gets or sets the image that will be displayed. /// [Content] - public IImage Source + public IImage? Source { get { return GetValue(SourceProperty); } set { SetValue(SourceProperty, value); } @@ -66,6 +66,7 @@ namespace Avalonia.Controls set { SetValue(StretchDirectionProperty, value); } } + /// protected override bool BypassFlowDirectionPolicies => true; /// diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 59b5bf48a5..42674cb481 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -2,7 +2,6 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; -using System.Diagnostics.CodeAnalysis; using Avalonia.Automation.Peers; using Avalonia.Collections; using Avalonia.Controls.Generators; @@ -17,7 +16,6 @@ using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Metadata; using Avalonia.Styling; -using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -106,7 +104,6 @@ namespace Avalonia.Controls private Tuple? _containerBeingPrepared; private ScrollViewer? _scrollViewer; private ItemsPresenter? _itemsPresenter; - private IScrollSnapPointsInfo? _scrolSnapPointInfo; /// /// Initializes a new instance of the class. @@ -221,6 +218,7 @@ namespace Avalonia.Controls } + /// public event EventHandler HorizontalSnapPointsChanged { add @@ -240,6 +238,7 @@ namespace Avalonia.Controls } } + /// public event EventHandler VerticalSnapPointsChanged { add @@ -424,13 +423,12 @@ namespace Avalonia.Controls /// true if the item is (or is eligible to be) its own container; otherwise, false. protected internal virtual bool IsItemItsOwnContainerOverride(Control item) => true; + /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); _scrollViewer = e.NameScope.Find("PART_ScrollViewer"); _itemsPresenter = e.NameScope.Find("PART_ItemsPresenter"); - - _scrolSnapPointInfo = _itemsPresenter as IScrollSnapPointsInfo; } /// @@ -477,11 +475,13 @@ namespace Avalonia.Controls base.OnKeyDown(e); } + /// protected override AutomationPeer OnCreateAutomationPeer() { return new ItemsControlAutomationPeer(this); } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -748,11 +748,13 @@ namespace Avalonia.Controls return true; } + /// public IReadOnlyList GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment) { return _itemsPresenter?.GetIrregularSnapPoints(orientation, snapPointsAlignment) ?? new List(); } + /// public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset) { offset = 0; diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index ce254684b7..387dc27562 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -28,7 +28,7 @@ namespace Avalonia.Controls .AddClassHandler((x, e) => x.OnLayoutTransformChanged(e)); ChildProperty.Changed - .AddClassHandler((x, e) => x.OnChildChanged(e)); + .AddClassHandler((x, _) => x.OnChildChanged()); UseRenderTransformProperty.Changed .AddClassHandler((x, e) => x.OnUseRenderTransformPropertyChanged(e)); @@ -146,7 +146,7 @@ namespace Avalonia.Controls return transformedDesiredSize; } - IDisposable? _renderTransformChangedEvent; + private IDisposable? _renderTransformChangedEvent; private void OnUseRenderTransformPropertyChanged(AvaloniaPropertyChangedEventArgs e) { @@ -167,8 +167,7 @@ namespace Avalonia.Controls .Subscribe( (x) => { - var target2 = x.Sender as LayoutTransformControl; - if (target2 != null) + if (x.Sender is LayoutTransformControl target2) { target2.LayoutTransform = target2.RenderTransform; } @@ -182,7 +181,7 @@ namespace Avalonia.Controls } } - private void OnChildChanged(AvaloniaPropertyChangedEventArgs e) + private void OnChildChanged() { if (null != TransformRoot) { @@ -206,18 +205,18 @@ namespace Avalonia.Controls /// /// Actual DesiredSize of Child element (the value it returned from its MeasureOverride method). /// - private Size _childActualSize = default; + private Size _childActualSize; /// /// RenderTransform/MatrixTransform applied to TransformRoot. /// - private MatrixTransform _matrixTransform = new MatrixTransform(); + private readonly MatrixTransform _matrixTransform = new(); /// /// Transformation matrix corresponding to _matrixTransform. /// private Matrix _transformation; - private IDisposable? _transformChangedEvent = null; + private IDisposable? _transformChangedEvent; /// /// Returns true if Size a is smaller than Size b in either dimension. @@ -263,10 +262,7 @@ namespace Avalonia.Controls // Get the transform matrix and apply it _transformation = RoundMatrix(LayoutTransform.Value, DecimalsAfterRound); - if (null != _matrixTransform) - { - _matrixTransform.Matrix = _transformation; - } + _matrixTransform.Matrix = _transformation; // New transform means re-layout is necessary InvalidateMeasure(); diff --git a/src/Avalonia.Controls/MaskedTextBox.cs b/src/Avalonia.Controls/MaskedTextBox.cs index 080326606e..5a3eb47ce4 100644 --- a/src/Avalonia.Controls/MaskedTextBox.cs +++ b/src/Avalonia.Controls/MaskedTextBox.cs @@ -178,12 +178,11 @@ namespace Avalonia.Controls } } - - } Type IStyleable.StyleKey => typeof(TextBox); + /// protected override void OnGotFocus(GotFocusEventArgs e) { if (HidePromptOnLeave == true && MaskProvider != null) @@ -193,6 +192,7 @@ namespace Avalonia.Controls base.OnGotFocus(e); } + /// protected override async void OnKeyDown(KeyEventArgs e) { if (MaskProvider == null) @@ -271,15 +271,17 @@ namespace Avalonia.Controls } } + /// protected override void OnLostFocus(RoutedEventArgs e) { - if (HidePromptOnLeave == true && MaskProvider != null) + if (HidePromptOnLeave && MaskProvider != null) { Text = MaskProvider.ToString(!HidePromptOnLeave, true); } base.OnLostFocus(e); } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { void UpdateMaskProvider() @@ -357,6 +359,8 @@ namespace Avalonia.Controls } base.OnPropertyChanged(change); } + + /// protected override void OnTextInput(TextInputEventArgs e) { _ignoreTextChanges = true; @@ -423,7 +427,7 @@ namespace Avalonia.Controls return startPosition; } - private void RefreshText(MaskedTextProvider provider, int position) + private void RefreshText(MaskedTextProvider? provider, int position) { if (provider != null) { diff --git a/src/Avalonia.Controls/NativeControlHost.cs b/src/Avalonia.Controls/NativeControlHost.cs index 6b9e378d3d..a94a1ee983 100644 --- a/src/Avalonia.Controls/NativeControlHost.cs +++ b/src/Avalonia.Controls/NativeControlHost.cs @@ -16,19 +16,17 @@ namespace Avalonia.Controls private IPlatformHandle? _nativeControlHandle; private bool _queuedForDestruction; private bool _queuedForMoveResize; - private readonly List _propertyChangedSubscriptions = new List(); + private readonly List _propertyChangedSubscriptions = new(); + /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { _currentRoot = e.Root as TopLevel; var visual = (Visual)this; while (visual != null) { - if (visual is Visual v) - { - v.PropertyChanged += PropertyChangedHandler; - _propertyChangedSubscriptions.Add(v); - } + visual.PropertyChanged += PropertyChangedHandler; + _propertyChangedSubscriptions.Add(visual); visual = visual.GetVisualParent(); } @@ -42,15 +40,13 @@ namespace Avalonia.Controls EnqueueForMoveResize(); } + /// protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { _currentRoot = null; - if (_propertyChangedSubscriptions != null) - { - foreach (var v in _propertyChangedSubscriptions) - v.PropertyChanged -= PropertyChangedHandler; - _propertyChangedSubscriptions.Clear(); - } + foreach (var v in _propertyChangedSubscriptions) + v.PropertyChanged -= PropertyChangedHandler; + _propertyChangedSubscriptions.Clear(); UpdateHost(); } @@ -128,7 +124,7 @@ namespace Avalonia.Controls return new Rect(position.Value, bounds.Size); } - void EnqueueForMoveResize() + private void EnqueueForMoveResize() { if(_queuedForMoveResize) return; diff --git a/src/Avalonia.Controls/NativeMenu.Export.cs b/src/Avalonia.Controls/NativeMenu.Export.cs index 9c1fb93a48..ab64416a2c 100644 --- a/src/Avalonia.Controls/NativeMenu.Export.cs +++ b/src/Avalonia.Controls/NativeMenu.Export.cs @@ -12,10 +12,10 @@ namespace Avalonia.Controls public static bool GetIsNativeMenuExported(TopLevel tl) => tl.GetValue(IsNativeMenuExportedProperty); - private static readonly AttachedProperty s_nativeMenuInfoProperty = - AvaloniaProperty.RegisterAttached("___NativeMenuInfo"); - - class NativeMenuInfo + private static readonly AttachedProperty s_nativeMenuInfoProperty = + AvaloniaProperty.RegisterAttached("___NativeMenuInfo"); + + private sealed class NativeMenuInfo { public bool ChangingIsExported { get; set; } public ITopLevelNativeMenuExporter? Exporter { get; } @@ -33,7 +33,7 @@ namespace Avalonia.Controls } } - static NativeMenuInfo GetInfo(TopLevel target) + private static NativeMenuInfo GetInfo(TopLevel target) { var rv = target.GetValue(s_nativeMenuInfoProperty); if (rv == null) @@ -45,18 +45,18 @@ namespace Avalonia.Controls return rv; } - static void SetIsNativeMenuExported(TopLevel tl, bool value) + private static void SetIsNativeMenuExported(TopLevel tl, bool value) { GetInfo(tl).ChangingIsExported = true; tl.SetValue(IsNativeMenuExportedProperty, value); } - public static readonly AttachedProperty MenuProperty - = AvaloniaProperty.RegisterAttached("Menu"); + public static readonly AttachedProperty MenuProperty + = AvaloniaProperty.RegisterAttached("Menu"); - public static void SetMenu(AvaloniaObject o, NativeMenu menu) => o.SetValue(MenuProperty, menu); + public static void SetMenu(AvaloniaObject o, NativeMenu? menu) => o.SetValue(MenuProperty, menu); - public static NativeMenu GetMenu(AvaloniaObject o) => o.GetValue(MenuProperty); + public static NativeMenu? GetMenu(AvaloniaObject o) => o.GetValue(MenuProperty); static NativeMenu() { diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 3464857131..79719912ea 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -34,8 +34,8 @@ namespace Avalonia.Controls.Primitives public static readonly AttachedProperty AdornerProperty = AvaloniaProperty.RegisterAttached("Adorner"); - private static readonly AttachedProperty s_adornedElementInfoProperty = - AvaloniaProperty.RegisterAttached("AdornedElementInfo"); + private static readonly AttachedProperty s_adornedElementInfoProperty = + AvaloniaProperty.RegisterAttached("AdornedElementInfo"); private static readonly AttachedProperty s_savedAdornerLayerProperty = AvaloniaProperty.RegisterAttached("SavedAdornerLayer"); @@ -159,8 +159,8 @@ namespace Avalonia.Controls.Primitives return; } - AdornerLayer.SetAdornedElement(adorner, visual); - AdornerLayer.SetIsClipEnabled(adorner, false); + SetAdornedElement(adorner, visual); + SetIsClipEnabled(adorner, false); ((ISetLogicalParent) adorner).SetParent(visual); layer.Children.Add(adorner); @@ -177,6 +177,7 @@ namespace Avalonia.Controls.Primitives ((ISetLogicalParent) adorner).SetParent(null); } + /// protected override Size MeasureOverride(Size availableSize) { foreach (var child in Children) @@ -199,6 +200,7 @@ namespace Avalonia.Controls.Primitives return default; } + /// protected override Size ArrangeOverride(Size finalSize) { foreach (var child in Children) @@ -217,7 +219,7 @@ namespace Avalonia.Controls.Primitives } else { - ArrangeChild((Control) child, finalSize); + ArrangeChild(child, finalSize); } } } diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 3b68cd2ae8..d6cd71aedc 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -120,7 +120,7 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty TopmostProperty = AvaloniaProperty.Register(nameof(Topmost)); - private bool _isOpenRequested = false; + private bool _isOpenRequested; private bool _isOpen; private bool _ignoreIsOpenChanged; private PopupOpenState? _openState; @@ -377,9 +377,9 @@ namespace Avalonia.Controls.Primitives popupHost.SetChild(Child); ((ISetLogicalParent)popupHost).SetParent(this); - if (InheritsTransform && placementTarget is Control c) + if (InheritsTransform) { - TransformTrackingHelper.Track(c, PlacementTargetTransformChanged) + TransformTrackingHelper.Track(placementTarget, PlacementTargetTransformChanged) .DisposeWith(handlerCleanup); } else @@ -518,6 +518,7 @@ namespace Avalonia.Controls.Primitives Close(); } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -579,7 +580,7 @@ namespace Avalonia.Controls.Primitives var scaleX = 1.0; var scaleY = 1.0; - if (InheritsTransform && placementTarget.TransformToVisual(topLevel) is Matrix m) + if (InheritsTransform && placementTarget.TransformToVisual(topLevel) is { } m) { scaleX = Math.Sqrt(m.M11 * m.M11 + m.M12 * m.M12); scaleY = Math.Sqrt(m.M11 * m.M11 + m.M12 * m.M12); @@ -623,6 +624,7 @@ namespace Avalonia.Controls.Primitives } } + /// protected override AutomationPeer OnCreateAutomationPeer() { return new PopupAutomationPeer(this); @@ -850,7 +852,7 @@ namespace Avalonia.Controls.Primitives var popupHost = _openState.PopupHost; - return popupHost != null && ((Visual)popupHost).IsVisualAncestorOf(visual); + return ((Visual)popupHost).IsVisualAncestorOf(visual); } public bool IsPointerOverPopup => ((IInputElement?)_openState?.PopupHost)?.IsPointerOver ?? false; diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 5210362505..8d79d62dc4 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -3,16 +3,14 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Xml.Linq; -using Avalonia.Controls.Generators; using Avalonia.Controls.Selection; using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Threading; -using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives { @@ -255,6 +253,7 @@ namespace Avalonia.Controls.Primitives /// /// Gets or sets the model that holds the current selection. /// + [AllowNull] protected ISelectionModel Selection { get @@ -399,6 +398,7 @@ namespace Avalonia.Controls.Primitives return null; } + /// protected override void ItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { base.ItemsCollectionChanged(sender!, e); @@ -409,12 +409,14 @@ namespace Avalonia.Controls.Primitives } } + /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); AutoScrollToSelectedItemIfNecessary(); } + /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); @@ -431,6 +433,7 @@ namespace Avalonia.Controls.Primitives } } + /// protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index) { base.PrepareContainerForItemOverride(element, item, index); @@ -447,12 +450,14 @@ namespace Avalonia.Controls.Primitives } } + /// protected override void ContainerIndexChangedOverride(Control container, int oldIndex, int newIndex) { base.ContainerIndexChangedOverride(container, oldIndex, newIndex); MarkContainerSelected(container, Selection.IsSelected(newIndex)); } + /// protected internal override void ClearContainerForItemOverride(Control element) { base.ClearContainerForItemOverride(element); @@ -463,7 +468,7 @@ namespace Avalonia.Controls.Primitives KeyboardNavigation.SetTabOnceActiveElement(panel, null); } - if (element is ISelectable selectable) + if (element is ISelectable) MarkContainerSelected(element, false); } @@ -498,7 +503,8 @@ namespace Avalonia.Controls.Primitives DataValidationErrors.SetError(this, error); } } - + + /// protected override void OnInitialized() { base.OnInitialized(); @@ -509,6 +515,7 @@ namespace Avalonia.Controls.Primitives } } + /// protected override void OnTextInput(TextInputEventArgs e) { if (!e.Handled) @@ -551,6 +558,7 @@ namespace Avalonia.Controls.Primitives base.OnTextInput(e); } + /// protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); @@ -582,6 +590,7 @@ namespace Avalonia.Controls.Primitives } } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -592,7 +601,7 @@ namespace Avalonia.Controls.Primitives } if (change.Property == ItemsProperty && _updateState is null && _selection is object) { - var newValue = change.GetNewValue(); + var newValue = change.GetNewValue(); _selection.Source = newValue; if (newValue is null) @@ -695,7 +704,7 @@ namespace Avalonia.Controls.Primitives { if (multi) { - if (Selection.IsSelected(index) == true) + if (Selection.IsSelected(index)) { Selection.Deselect(index); } @@ -716,12 +725,10 @@ namespace Avalonia.Controls.Primitives Selection.Select(index); } - if (Presenter?.Panel != null) + if (Presenter?.Panel is { } panel) { var container = ContainerFromIndex(index); - KeyboardNavigation.SetTabOnceActiveElement( - (InputElement)Presenter.Panel, - container); + KeyboardNavigation.SetTabOnceActiveElement(panel, container); } } @@ -940,7 +947,7 @@ namespace Avalonia.Controls.Primitives private void UpdateContainerSelection() { - if (Presenter?.Panel is Panel panel) + if (Presenter?.Panel is { } panel) { foreach (var container in panel.Children) { diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 9a684c4534..d8874832bd 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -290,12 +290,6 @@ namespace Avalonia.Controls.Primitives ApplyTemplatedParent(child, this); ((ISetLogicalParent)child).SetParent(this); VisualChildren.Add(child); - - // Existing code kinda expect to see a NameScope even if it's empty - if (nameScope == null) - { - nameScope = new NameScope(); - } var e = new TemplateAppliedEventArgs(nameScope); OnApplyTemplate(e); @@ -320,6 +314,7 @@ namespace Avalonia.Controls.Primitives return this; } + /// protected sealed override void NotifyChildResourcesChanged(ResourcesChangedEventArgs e) { var count = VisualChildren.Count; diff --git a/src/Avalonia.Controls/Primitives/TextSearch.cs b/src/Avalonia.Controls/Primitives/TextSearch.cs index 949532cb16..962fba361e 100644 --- a/src/Avalonia.Controls/Primitives/TextSearch.cs +++ b/src/Avalonia.Controls/Primitives/TextSearch.cs @@ -11,15 +11,15 @@ namespace Avalonia.Controls.Primitives /// Defines the Text attached property. /// This text will be considered during text search in (such as ) /// - public static readonly AttachedProperty TextProperty - = AvaloniaProperty.RegisterAttached("Text", typeof(TextSearch)); + public static readonly AttachedProperty TextProperty + = AvaloniaProperty.RegisterAttached("Text", typeof(TextSearch)); /// /// Sets the for a control. /// /// The control /// The search text to set - public static void SetText(Control control, string text) + public static void SetText(Control control, string? text) { control.SetValue(TextProperty, text); } @@ -29,7 +29,7 @@ namespace Avalonia.Controls.Primitives /// /// The control /// The property value - public static string GetText(Control control) + public static string? GetText(Control control) { return control.GetValue(TextProperty); } diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index 14ec7a2849..9e8d1478fa 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -5,7 +5,6 @@ using System; using Avalonia.Controls.Metadata; -using Avalonia.Data; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Metadata; @@ -31,14 +30,14 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty OrientationProperty = ScrollBar.OrientationProperty.AddOwner(); - public static readonly StyledProperty ThumbProperty = - AvaloniaProperty.Register(nameof(Thumb)); + public static readonly StyledProperty ThumbProperty = + AvaloniaProperty.Register(nameof(Thumb)); - public static readonly StyledProperty