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/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 83776ec2c1..0695d9d17a 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -168,6 +168,9 @@ + + + @@ -201,14 +204,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"> - + 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 + { + } +} 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..5123803f6e 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 && theme != ThemeVariant.Default) + { + 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/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/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/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/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 new file mode 100644 index 0000000000..b01fb46aa3 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs @@ -0,0 +1,78 @@ +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 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) + { + 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); + _runs[index] = new GlyphRun(typeface, fontRenderingEmSize, chars.AsMemory(index, 1), new[] { glyph }); + } + } + + 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..d103b068a6 --- /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.GetMaxHeight() + 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..63ec8d756b 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; @@ -21,11 +21,12 @@ namespace Avalonia.Rendering.Composition.Server { private readonly ServerCompositor _compositor; private readonly Func> _surfaces; + private readonly DiagnosticTextRenderer _diagnosticTextRenderer; 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 FpsCounter? _fpsCounter; + private FrameTimeGraph? _renderTimeGraph; + private FrameTimeGraph? _layoutTimeGraph; private Rect _dirtyRect; private Random _random = new(); private Size _layerSize; @@ -35,18 +36,34 @@ 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; } - public ServerCompositionTarget(ServerCompositor compositor, Func> surfaces) : - base(compositor) + 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, + 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); + partial void OnIsEnabledChanged() { if (IsEnabled) @@ -62,7 +79,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 +135,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 +183,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); } + RenderedVisuals = 0; _dirtyRect = default; } } + private void DrawOverlays(IDrawingContextImpl targetContext) + { + 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(Size.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..b4b6d1d4f1 --- /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. + internal 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..0897cac62e --- /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. + /// + internal 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/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 6043175eee..5bf022cd51 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 RequestedThemeVariant 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..8218533f4f --- /dev/null +++ b/src/Avalonia.Base/Styling/ThemeVariant.cs @@ -0,0 +1,105 @@ +using System; +using System.ComponentModel; +using System.Text; +using Avalonia.Platform; + +namespace Avalonia.Styling; + +/// +/// Specifies a UI theme variant that should be used for the +/// +[TypeConverter(typeof(ThemeVariantTypeConverter))] +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) + { + 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)); + + /// + /// Use the Dark theme variant. + /// + public static ThemeVariant Dark { get; } = new(nameof(Dark)); + + 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 explicit operator ThemeVariant(PlatformThemeVariant themeVariant) + { + return themeVariant switch + { + PlatformThemeVariant.Light => Light, + PlatformThemeVariant.Dark => Dark, + _ => throw new ArgumentOutOfRangeException(nameof(themeVariant), themeVariant, null) + }; + } + + public static explicit operator PlatformThemeVariant?(ThemeVariant themeVariant) + { + if (themeVariant == Light) + { + return PlatformThemeVariant.Light; + } + else if (themeVariant == Dark) + { + return PlatformThemeVariant.Dark; + } + else if (themeVariant.InheritVariant is { } inheritVariant) + { + return (PlatformThemeVariant?)inheritVariant; + } + + return null; + } +} diff --git a/src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs b/src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs new file mode 100644 index 0000000000..acb2d7651b --- /dev/null +++ b/src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs @@ -0,0 +1,24 @@ +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.Default) => ThemeVariant.Default, + nameof(ThemeVariant.Light) => ThemeVariant.Light, + nameof(ThemeVariant.Dark) => ThemeVariant.Dark, + _ => 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.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..36fd9fe709 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/Application.cs b/src/Avalonia.Controls/Application.cs index 5b652cce19..6d3ba3cf8a 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)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 06a829c418..676fa1519a 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -1,9 +1,10 @@ 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.Data; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; @@ -17,7 +18,6 @@ using Avalonia.Platform.Storage; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Utilities; -using Avalonia.VisualTree; using Avalonia.Input.Platform; using System.Linq; @@ -96,6 +96,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; @@ -106,6 +107,7 @@ namespace Avalonia.Controls private Border? _transparencyFallbackBorder; private TargetWeakEventSubscriber? _resourcesChangesSubscriber; private IStorageProvider? _storageProvider; + private LayoutDiagnosticBridge? _layoutDiagnosticBridge; /// /// Initializes static members of the class. @@ -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,10 +184,15 @@ 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; - + this.GetObservable(PointerOverElementProperty) .Select( x => (x as InputElement)?.GetObservable(CursorProperty) ?? Observable.Empty()) @@ -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. /// @@ -328,8 +333,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; } } @@ -407,6 +421,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((PlatformThemeVariant?)change.GetNewValue() ?? PlatformThemeVariant.Light); + } + } + /// /// Creates the layout manager for this . /// @@ -431,10 +463,17 @@ namespace Avalonia.Controls _globalStyles.GlobalStylesAdded -= ((IStyleHost)this).StylesAdded; _globalStyles.GlobalStylesRemoved -= ((IStyleHost)this).StylesRemoved; } + if (_applicationThemeHost is { }) + { + _applicationThemeHost.ActualThemeVariantChanged -= GlobalActualThemeVariantChanged; + } Renderer?.Dispose(); Renderer = null!; + _layoutDiagnosticBridge?.Dispose(); + _layoutDiagnosticBridge = null; + _pointerOverPreProcessor?.OnCompleted(); _pointerOverPreProcessorSubscription?.Dispose(); _backGestureSubscription?.Dispose(); @@ -592,6 +631,11 @@ namespace Avalonia.Controls } } + 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); @@ -617,5 +661,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/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/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/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/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 - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index 925bf0a4fa..4068caac21 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -291,6 +291,19 @@ 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 = 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 c55e116cb3..92710e42ce 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -68,6 +68,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlConstructor FontFamilyConstructorUriName { get; } public IXamlType Thickness { get; } public IXamlConstructor ThicknessFullConstructor { get; } + public IXamlType ThemeVariant { get; } public IXamlType Point { get; } public IXamlConstructor PointFullConstructor { get; } public IXamlType Vector { get; } @@ -192,6 +193,7 @@ 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"); (IXamlType, IXamlConstructor) GetNumericTypeInfo(string name, IXamlType componentType, int componentCount) { 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; 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 aff533c443..2d0f351d58 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 + + + 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/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.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.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.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.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; } 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.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/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.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs new file mode 100644 index 0000000000..c5b62cdff2 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs @@ -0,0 +1,447 @@ +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.Xaml; + +public class ThemeDictionariesTests : XamlTestBase +{ + public static ThemeVariant Custom { get; } = new(nameof(Custom), ThemeVariant.Light); + + [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 = 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.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); } } 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); 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()