From a79ca4783e1f9f80b63c625dc06a3379826913d5 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 17 Jan 2023 23:12:07 -0500 Subject: [PATCH 01/63] 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 02/63] 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 03/63] 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 04/63] 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 05/63] 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 06/63] 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 07/63] 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 08/63] 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 09/63] 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 10/63] 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 11/63] 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 12/63] 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 13/63] 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 14/63] 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 15/63] 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 16/63] 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 17/63] 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 18/63] 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 19/63] 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 20/63] 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 21/63] 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 22/63] 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 23/63] 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 24/63] 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 25/63] 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 26/63] 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 27/63] 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 28/63] 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 29/63] 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 a22249b898aa621679bc79ea7fb1406a74dd0942 Mon Sep 17 00:00:00 2001 From: workgroupengineering Date: Thu, 2 Feb 2023 09:45:36 +0100 Subject: [PATCH 30/63] 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 31/63] 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 32/63] 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 33/63] 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 34/63] 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 35/63] 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 96bda931196bab8368969d0a1e1151a9427d618d Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Sat, 21 Jan 2023 11:43:14 +0100 Subject: [PATCH 36/63] 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 37/63] 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 38/63] 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 39/63] 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 07adf1d6dc82bf776b2adef730797fd585662def Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 3 Feb 2023 09:12:18 +0100 Subject: [PATCH 40/63] 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 42f8c58aef5930cb5ec3def93c981c39beab5143 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 3 Feb 2023 10:45:53 +0100 Subject: [PATCH 41/63] 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 42/63] 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 43/63] 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 44/63] 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 45/63] 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 46/63] 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 47/63] 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 48/63] 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 49/63] 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 50/63] 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 51/63] 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 52/63] 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 53/63] 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 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 54/63] 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 465d72f2903572c66a444f9f2b31392b7ad6cd69 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Sat, 4 Feb 2023 16:46:13 -0700 Subject: [PATCH 55/63] 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 56/63] 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 57/63] 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 58/63] 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 59/63] 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 60/63] 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 39fe9b17fb346ee5e63d6bfa508ed8d775eda355 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Mon, 6 Feb 2023 19:26:11 +0100 Subject: [PATCH 61/63] 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 62/63] 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 6dae36ead9858385e710a117c8a2bc4b002e960c Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 7 Feb 2023 14:01:39 +0000 Subject: [PATCH 63/63] 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(); } }