diff --git a/.editorconfig b/.editorconfig index eac5870f96..a144ec8843 100644 --- a/.editorconfig +++ b/.editorconfig @@ -55,16 +55,17 @@ dotnet_naming_symbols.constant_fields.required_modifiers = const dotnet_naming_style.pascal_case_style.capitalization = pascal_case -# static fields should have s_ prefix -dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion -dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields -dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style +# private static fields should have s_ prefix +dotnet_naming_rule.private_static_fields_should_have_prefix.severity = suggestion +dotnet_naming_rule.private_static_fields_should_have_prefix.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_should_have_prefix.style = private_static_prefix_style -dotnet_naming_symbols.static_fields.applicable_kinds = field -dotnet_naming_symbols.static_fields.required_modifiers = static +dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.required_modifiers = static +dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private -dotnet_naming_style.static_prefix_style.required_prefix = s_ -dotnet_naming_style.static_prefix_style.capitalization = camel_case +dotnet_naming_style.private_static_prefix_style.required_prefix = s_ +dotnet_naming_style.private_static_prefix_style.capitalization = camel_case # internal and private fields should be _camelCase dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion @@ -117,7 +118,7 @@ csharp_space_after_dot = false csharp_space_after_keywords_in_control_flow_statements = true csharp_space_after_semicolon_in_for_statement = true csharp_space_around_binary_operators = before_and_after -csharp_space_around_declaration_statements = do_not_ignore +csharp_space_around_declaration_statements = false csharp_space_before_colon_in_inheritance_clause = true csharp_space_before_comma = false csharp_space_before_dot = false @@ -145,10 +146,14 @@ dotnet_diagnostic.CS1591.severity = suggestion # CS0162: Remove unreachable code dotnet_diagnostic.CS0162.severity = error +# CA1018: Mark attributes with AttributeUsageAttribute +dotnet_diagnostic.CA1018.severity = error # CA1304: Specify CultureInfo dotnet_diagnostic.CA1304.severity = warning # CA1802: Use literals where appropriate dotnet_diagnostic.CA1802.severity = warning +# CA1813: Avoid unsealed attributes +dotnet_diagnostic.CA1813.severity = error # CA1815: Override equals and operator equals on value types dotnet_diagnostic.CA1815.severity = warning # CA1820: Test for empty strings using string length @@ -207,5 +212,5 @@ indent_size = 2 # Shell scripts [*.sh] end_of_line = lf -[*.{cmd, bat}] +[*.{cmd,bat}] end_of_line = crlf diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 2f034bd083..3acd4bf9f2 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -15,6 +15,7 @@ "src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj", "src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj", "src\\Avalonia.Controls.DataGrid\\Avalonia.Controls.DataGrid.csproj", + "src\\Avalonia.Controls.ItemsRepeater\\Avalonia.Controls.ItemsRepeater.csproj", "src\\Avalonia.Controls\\Avalonia.Controls.csproj", "src\\Avalonia.DesignerSupport\\Avalonia.DesignerSupport.csproj", "src\\Avalonia.Desktop\\Avalonia.Desktop.csproj", diff --git a/Avalonia.sln b/Avalonia.sln index ce9a37a3ce..525e01c891 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -233,6 +233,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\R EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater", "src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj", "{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater.UnitTests", "tests\Avalonia.Controls.ItemsRepeater.UnitTests\Avalonia.Controls.ItemsRepeater.UnitTests.csproj", "{F4E36AA8-814E-4704-BC07-291F70F45193}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -548,6 +552,14 @@ Global {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.Build.0 = Release|Any CPU + {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Release|Any CPU.Build.0 = Release|Any CPU + {F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -613,6 +625,7 @@ Global {90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098} {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props index 620ec58ff3..75d317be1a 100644 --- a/build/HarfBuzzSharp.props +++ b/build/HarfBuzzSharp.props @@ -1,7 +1,7 @@  - - - + + + diff --git a/build/ImageSharp.props b/build/ImageSharp.props index 178c274ac9..66e6580070 100644 --- a/build/ImageSharp.props +++ b/build/ImageSharp.props @@ -1,5 +1,5 @@ - + diff --git a/build/Moq.props b/build/Moq.props index 9e2fd1db5d..357f0c9a5f 100644 --- a/build/Moq.props +++ b/build/Moq.props @@ -1,5 +1,5 @@  - + diff --git a/build/SharedVersion.props b/build/SharedVersion.props index eca3ba37b0..2849262591 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -3,6 +3,7 @@ Avalonia 11.0.999 + Avalonia Team Copyright 2022 © The AvaloniaUI Project https://avaloniaui.net https://github.com/AvaloniaUI/Avalonia/ diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index 31619399f9..f45addaa2a 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,7 +1,7 @@  - - - + + + diff --git a/build/XUnit.props b/build/XUnit.props index 17ead91aa3..3c89c8b52b 100644 --- a/build/XUnit.props +++ b/build/XUnit.props @@ -1,13 +1,12 @@  - - - - - - - - + + + + + + + diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index f345043f61..ce82f7d83f 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -66,7 +66,7 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) { _isModal = isDialog; WindowBaseImpl::Show(activate, isDialog); - + GetWindowState(&_actualWindowState); HideOrShowTrafficLights(); return SetWindowState(_lastWindowState); 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.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj b/samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj index d0fb614840..733a4b7194 100644 --- a/samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj +++ b/samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index e4c83dca49..e465e9caf3 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -31,7 +31,6 @@ - diff --git a/samples/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/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 18f0dd16ba..c223bfe1a9 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -26,6 +26,7 @@ + diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 83776ec2c1..3681298a72 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -14,8 +14,8 @@ - - + + @@ -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/Animation/Animatable.cs b/src/Avalonia.Base/Animation/Animatable.cs index edaa76233e..5208c8b218 100644 --- a/src/Avalonia.Base/Animation/Animatable.cs +++ b/src/Avalonia.Base/Animation/Animatable.cs @@ -27,7 +27,11 @@ namespace Avalonia.Animation AvaloniaProperty.Register(nameof(Transitions)); private bool _transitionsEnabled = true; + private bool _isSubscribedToTransitionsCollection = false; private Dictionary? _transitionState; + private NotifyCollectionChangedEventHandler? _collectionChanged; + private NotifyCollectionChangedEventHandler TransitionsCollectionChangedHandler => + _collectionChanged ??= TransitionsCollectionChanged; /// /// Gets or sets the clock which controls the animations on the control. @@ -60,9 +64,14 @@ namespace Avalonia.Animation { _transitionsEnabled = true; - if (Transitions is object) + if (Transitions is Transitions transitions) { - AddTransitions(Transitions); + if (!_isSubscribedToTransitionsCollection) + { + _isSubscribedToTransitionsCollection = true; + transitions.CollectionChanged += TransitionsCollectionChangedHandler; + } + AddTransitions(transitions); } } } @@ -72,7 +81,7 @@ namespace Avalonia.Animation /// /// /// This method should not be called from user code, it will be called automatically by the framework - /// when a control is added to the visual tree. + /// when a control is removed from the visual tree. /// protected void DisableTransitions() { @@ -80,9 +89,14 @@ namespace Avalonia.Animation { _transitionsEnabled = false; - if (Transitions is object) + if (Transitions is Transitions transitions) { - RemoveTransitions(Transitions); + if (_isSubscribedToTransitionsCollection) + { + _isSubscribedToTransitionsCollection = false; + transitions.CollectionChanged -= TransitionsCollectionChangedHandler; + } + RemoveTransitions(transitions); } } } @@ -109,7 +123,8 @@ namespace Avalonia.Animation toAdd = newTransitions.Except(oldTransitions).ToList(); } - newTransitions.CollectionChanged += TransitionsCollectionChanged; + newTransitions.CollectionChanged += TransitionsCollectionChangedHandler; + _isSubscribedToTransitionsCollection = true; AddTransitions(toAdd); } @@ -122,19 +137,19 @@ namespace Avalonia.Animation toRemove = oldTransitions.Except(newTransitions).ToList(); } - oldTransitions.CollectionChanged -= TransitionsCollectionChanged; + oldTransitions.CollectionChanged -= TransitionsCollectionChangedHandler; RemoveTransitions(toRemove); } } else if (_transitionsEnabled && - Transitions is object && + Transitions is Transitions transitions && _transitionState is object && !change.Property.IsDirect && change.Priority > BindingPriority.Animation) { - for (var i = Transitions.Count -1; i >= 0; --i) + for (var i = transitions.Count - 1; i >= 0; --i) { - var transition = Transitions[i]; + var transition = transitions[i]; if (transition.Property == change.Property && _transitionState.TryGetValue(transition, out var state)) @@ -154,11 +169,11 @@ namespace Avalonia.Animation { oldValue = animatedValue; } - + var clock = Clock ?? AvaloniaLocator.Current.GetRequiredService(); state.Instance?.Dispose(); state.Instance = transition.Apply( this, - Clock ?? AvaloniaLocator.Current.GetRequiredService(), + clock, oldValue, newValue); return; diff --git a/src/Avalonia.Base/Animation/KeySpline.cs b/src/Avalonia.Base/Animation/KeySpline.cs index 6ca5b2e759..ed6adb79b8 100644 --- a/src/Avalonia.Base/Animation/KeySpline.cs +++ b/src/Avalonia.Base/Animation/KeySpline.cs @@ -79,15 +79,12 @@ namespace Avalonia.Animation /// culture of the string /// Thrown if the string does not have 4 values /// A with the appropriate values set - public static KeySpline Parse(string value, CultureInfo culture) + public static KeySpline Parse(string value, CultureInfo? culture) { - if (culture is null) - culture = CultureInfo.InvariantCulture; + culture ??= CultureInfo.InvariantCulture; - using (var tokenizer = new StringTokenizer((string)value, culture, exceptionMessage: $"Invalid KeySpline string: \"{value}\".")) - { - return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble()); - } + using var tokenizer = new StringTokenizer(value, culture, exceptionMessage: $"Invalid KeySpline string: \"{value}\"."); + return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble()); } /// diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 1946d4ba5c..50a7a5c831 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -152,7 +152,7 @@ namespace Avalonia property = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); - _values?.ClearLocalValue(property); + _values.ClearLocalValue(property); } /// @@ -242,7 +242,14 @@ namespace Avalonia return registered.InvokeGetter(this); } - /// + /// + /// Gets an base value. + /// + /// The property. + /// + /// Gets the value of the property excluding animated values, otherwise . + /// Note that this method does not return property values that come from inherited or default values. + /// public Optional GetBaseValue(StyledProperty property) { _ = property ?? throw new ArgumentNullException(nameof(property)); @@ -261,7 +268,7 @@ namespace Avalonia VerifyAccess(); - return _values?.IsAnimating(property) ?? false; + return _values.IsAnimating(property); } /// @@ -279,7 +286,7 @@ namespace Avalonia VerifyAccess(); - return _values?.IsSet(property) ?? false; + return _values.IsSet(property); } /// @@ -515,14 +522,12 @@ namespace Avalonia /// The property. public void CoerceValue(AvaloniaProperty property) => _values.CoerceValue(property); - /// internal void AddInheritanceChild(AvaloniaObject child) { _inheritanceChildren ??= new List(); _inheritanceChildren.Add(child); } - - /// + internal void RemoveInheritanceChild(AvaloniaObject child) { _inheritanceChildren?.Remove(child); @@ -541,24 +546,11 @@ namespace Avalonia return new AvaloniaPropertyValue( property, GetValue(property), - BindingPriority.Unset, - "Local Value"); - } - else if (_values != null) - { - var result = _values.GetDiagnostic(property); - - if (result != null) - { - return result; - } + BindingPriority.LocalValue, + null); } - return new AvaloniaPropertyValue( - property, - GetValue(property), - BindingPriority.Unset, - "Unset"); + return _values.GetDiagnostic(property); } internal ValueStore GetValueStore() => _values; diff --git a/src/Avalonia.Base/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/Data/InstancedBinding.cs b/src/Avalonia.Base/Data/InstancedBinding.cs index 00e5c3d8e6..c09c31632e 100644 --- a/src/Avalonia.Base/Data/InstancedBinding.cs +++ b/src/Avalonia.Base/Data/InstancedBinding.cs @@ -23,7 +23,7 @@ namespace Avalonia.Data /// The priority of the binding. /// /// This constructor can be used to create any type of binding and as such requires an - /// as the binding source because this is the only binding + /// as the binding source because this is the only binding /// source which can be used for all binding modes. If you wish to create an instance with /// something other than a subject, use one of the static creation methods on this class. /// diff --git a/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs index d7b1f2e053..270cac95f2 100644 --- a/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs @@ -1,6 +1,3 @@ -using System; -using Avalonia.Data; - namespace Avalonia.Diagnostics { /// diff --git a/src/Avalonia.Base/Input/DragEventArgs.cs b/src/Avalonia.Base/Input/DragEventArgs.cs index 403dd6f23e..8d7cc2b9a1 100644 --- a/src/Avalonia.Base/Input/DragEventArgs.cs +++ b/src/Avalonia.Base/Input/DragEventArgs.cs @@ -1,36 +1,28 @@ using System; using Avalonia.Interactivity; using Avalonia.Metadata; -using Avalonia.VisualTree; namespace Avalonia.Input { public class DragEventArgs : RoutedEventArgs { - private Interactive _target; - private Point _targetLocation; + private readonly Interactive _target; + private readonly Point _targetLocation; public DragDropEffects DragEffects { get; set; } - public IDataObject Data { get; private set; } + public IDataObject Data { get; } - public KeyModifiers KeyModifiers { get; private set; } + public KeyModifiers KeyModifiers { get; } public Point GetPosition(Visual relativeTo) { - var point = new Point(0, 0); - if (relativeTo == null) { throw new ArgumentNullException(nameof(relativeTo)); } - if (_target != null) - { - point = _target.TranslatePoint(_targetLocation, relativeTo) ?? point; - } - - return point; + return _target.TranslatePoint(_targetLocation, relativeTo) ?? new Point(0, 0); } [Unstable] diff --git a/src/Avalonia.Base/Input/KeyGesture.cs b/src/Avalonia.Base/Input/KeyGesture.cs index c6618fd550..9ee8ae9711 100644 --- a/src/Avalonia.Base/Input/KeyGesture.cs +++ b/src/Avalonia.Base/Input/KeyGesture.cs @@ -136,7 +136,7 @@ namespace Avalonia.Input return StringBuilderCache.GetStringAndRelease(s); } - public bool Matches(KeyEventArgs keyEvent) => + public bool Matches(KeyEventArgs? keyEvent) => keyEvent != null && keyEvent.KeyModifiers == KeyModifiers && ResolveNumPadOperationKey(keyEvent.Key) == ResolveNumPadOperationKey(Key); diff --git a/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs b/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs index b05d8f30bb..ba909de60f 100644 --- a/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Avalonia.Input.Navigation; using Avalonia.VisualTree; @@ -51,7 +50,7 @@ namespace Avalonia.Input // If there's a custom keyboard navigation handler as an ancestor, use that. var custom = (element as Visual)?.FindAncestorOfType(true); - if (custom is object && HandlePreCustomNavigation(custom, element, direction, out var ce)) + if (custom is not null && HandlePreCustomNavigation(custom, element, direction, out var ce)) return ce; var result = direction switch @@ -117,32 +116,27 @@ namespace Avalonia.Input NavigationDirection direction, [NotNullWhen(true)] out IInputElement? result) { - if (customHandler != null) + var (handled, next) = customHandler.GetNext(element, direction); + + if (handled) { - var (handled, next) = customHandler.GetNext(element, direction); + if (next is not null) + { + result = next; + return true; + } - if (handled) + var r = direction switch { - if (next != null) - { - result = next; - return true; - } - else if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous) - { - var r = direction switch - { - NavigationDirection.Next => TabNavigation.GetNextTabOutside(customHandler), - NavigationDirection.Previous => TabNavigation.GetPrevTabOutside(customHandler), - _ => throw new NotSupportedException(), - }; - - if (r is object) - { - result = r; - return true; - } - } + NavigationDirection.Next => TabNavigation.GetNextTabOutside(customHandler), + NavigationDirection.Previous => TabNavigation.GetPrevTabOutside(customHandler), + _ => null + }; + + if (r is not null) + { + result = r; + return true; } } diff --git a/src/Avalonia.Base/Input/Navigation/TabNavigation.cs b/src/Avalonia.Base/Input/Navigation/TabNavigation.cs index d218867cf2..c460ecf3b3 100644 --- a/src/Avalonia.Base/Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Base/Input/Navigation/TabNavigation.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; using Avalonia.VisualTree; namespace Avalonia.Input.Navigation @@ -54,8 +52,7 @@ namespace Avalonia.Input.Navigation // Avoid the endless loop here for Cycle groups if (loopStartElement == nextTabElement) break; - if (loopStartElement == null) - loopStartElement = nextTabElement; + loopStartElement ??= nextTabElement; var firstTabElementInside = GetNextTab(null, nextTabElement, true); if (firstTabElementInside != null) @@ -80,12 +77,9 @@ namespace Avalonia.Input.Navigation public static IInputElement? GetNextTabOutside(ICustomKeyboardNavigation e) { - if (e is IInputElement container) + if (e is IInputElement container && GetLastInTree(container) is { } last) { - var last = GetLastInTree(container); - - if (last is object) - return GetNextTab(last, false); + return GetNextTab(last, false); } return null; @@ -93,11 +87,8 @@ namespace Avalonia.Input.Navigation public static IInputElement? GetPrevTab(IInputElement? e, IInputElement? container, bool goDownOnly) { - if (e is null && container is null) - throw new InvalidOperationException("Either 'e' or 'container' must be non-null."); - - if (container is null) - container = GetGroupParent(e!); + container ??= + GetGroupParent(e ?? throw new InvalidOperationException("Either 'e' or 'container' must be non-null.")); KeyboardNavigationMode tabbingType = GetKeyNavigationMode(container); @@ -163,8 +154,7 @@ namespace Avalonia.Input.Navigation // Avoid the endless loop here if (loopStartElement == nextTabElement) break; - if (loopStartElement == null) - loopStartElement = nextTabElement; + loopStartElement ??= nextTabElement; // At this point nextTabElement is TabGroup var lastTabElementInside = GetPrevTab(null, nextTabElement, true); @@ -189,22 +179,18 @@ namespace Avalonia.Input.Navigation public static IInputElement? GetPrevTabOutside(ICustomKeyboardNavigation e) { - if (e is IInputElement container) + if (e is IInputElement container && GetFirstChild(container) is { } first) { - var first = GetFirstChild(container); - - if (first is object) - return GetPrevTab(first, null, false); + return GetPrevTab(first, null, false); } return null; } - private static IInputElement? FocusedElement(IInputElement e) + private static IInputElement? FocusedElement(IInputElement? e) { - var iie = e; // Focus delegation is enabled only if keyboard focus is outside the container - if (iie != null && !iie.IsKeyboardFocusWithin) + if (e != null && !e.IsKeyboardFocusWithin) { var focusedElement = (FocusManager.Instance as FocusManager)?.GetFocusedElement(e); if (focusedElement != null) @@ -229,13 +215,11 @@ namespace Avalonia.Input.Navigation private static IInputElement? GetFirstChild(IInputElement e) { // If the element has a FocusedElement it should be its first child - if (FocusedElement(e) is IInputElement focusedElement) + if (FocusedElement(e) is { } focusedElement) return focusedElement; // Return the first visible element. - var uiElement = e as InputElement; - - if (uiElement is null || IsVisibleAndEnabled(uiElement)) + if (e is not InputElement uiElement || IsVisibleAndEnabled(uiElement)) { if (e is Visual elementAsVisual) { @@ -265,7 +249,7 @@ namespace Avalonia.Input.Navigation private static IInputElement? GetLastChild(IInputElement e) { // If the element has a FocusedElement it should be its last child - if (FocusedElement(e) is IInputElement focusedElement) + if (FocusedElement(e) is { } focusedElement) return focusedElement; // Return the last visible element. @@ -273,9 +257,7 @@ namespace Avalonia.Input.Navigation if (uiElement == null || IsVisibleAndEnabled(uiElement)) { - var elementAsVisual = e as Visual; - - if (elementAsVisual != null) + if (e is Visual elementAsVisual) { var children = elementAsVisual.VisualChildren; var count = children.Count; @@ -322,7 +304,7 @@ namespace Avalonia.Input.Navigation return firstTabElement; } - private static IInputElement? GetLastInTree(IInputElement container) + private static IInputElement GetLastInTree(IInputElement container) { IInputElement? result; IInputElement? c = container; diff --git a/src/Avalonia.Base/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/LogicalTree/LogicalExtensions.cs b/src/Avalonia.Base/LogicalTree/LogicalExtensions.cs index 74720c0a77..9ab7da3ff7 100644 --- a/src/Avalonia.Base/LogicalTree/LogicalExtensions.cs +++ b/src/Avalonia.Base/LogicalTree/LogicalExtensions.cs @@ -48,7 +48,7 @@ namespace Avalonia.LogicalTree /// The logical. /// If given logical should be included in search. /// First ancestor of given type. - public static T? FindLogicalAncestorOfType(this ILogical logical, bool includeSelf = false) where T : class + public static T? FindLogicalAncestorOfType(this ILogical? logical, bool includeSelf = false) where T : class { if (logical is null) { @@ -120,7 +120,7 @@ namespace Avalonia.LogicalTree /// The logical. /// If given logical should be included in search. /// First descendant of given type. - public static T? FindLogicalDescendantOfType(this ILogical logical, bool includeSelf = false) where T : class + public static T? FindLogicalDescendantOfType(this ILogical? logical, bool includeSelf = false) where T : class { if (logical is null) { @@ -185,7 +185,7 @@ namespace Avalonia.LogicalTree /// True if is an ancestor of ; /// otherwise false. /// - public static bool IsLogicalAncestorOf(this ILogical logical, ILogical target) + public static bool IsLogicalAncestorOf(this ILogical? logical, ILogical? target) { var current = target?.LogicalParent; diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs index 5470a735b3..ab89177295 100644 --- a/src/Avalonia.Base/Media/Color.cs +++ b/src/Avalonia.Base/Media/Color.cs @@ -147,16 +147,11 @@ namespace Avalonia.Media /// The color string. /// The parsed color /// The status of the operation. - public static bool TryParse(string s, out Color color) + public static bool TryParse(string? s, out Color color) { color = default; - if (s is null) - { - return false; - } - - if (s.Length == 0) + if (string.IsNullOrEmpty(s)) { return false; } diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs index d295111d72..622181dba0 100644 --- a/src/Avalonia.Base/Media/DrawingContext.cs +++ b/src/Avalonia.Base/Media/DrawingContext.cs @@ -240,7 +240,7 @@ namespace Avalonia.Media /// /// The foreground brush. /// The glyph run. - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) { _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun)); diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index 481329c20c..b7abda2c61 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -13,14 +13,14 @@ namespace Avalonia.Media public static readonly StyledProperty OpacityProperty = AvaloniaProperty.Register(nameof(Opacity), 1); - public static readonly StyledProperty TransformProperty = - AvaloniaProperty.Register(nameof(Transform)); + public static readonly StyledProperty TransformProperty = + AvaloniaProperty.Register(nameof(Transform)); - public static readonly StyledProperty ClipGeometryProperty = - AvaloniaProperty.Register(nameof(ClipGeometry)); + public static readonly StyledProperty ClipGeometryProperty = + AvaloniaProperty.Register(nameof(ClipGeometry)); - public static readonly StyledProperty OpacityMaskProperty = - AvaloniaProperty.Register(nameof(OpacityMask)); + public static readonly StyledProperty OpacityMaskProperty = + AvaloniaProperty.Register(nameof(OpacityMask)); public static readonly DirectProperty ChildrenProperty = AvaloniaProperty.RegisterDirect( @@ -36,19 +36,19 @@ namespace Avalonia.Media set => SetValue(OpacityProperty, value); } - public Transform Transform + public Transform? Transform { get => GetValue(TransformProperty); set => SetValue(TransformProperty, value); } - public Geometry ClipGeometry + public Geometry? ClipGeometry { get => GetValue(ClipGeometryProperty); set => SetValue(ClipGeometryProperty, value); } - public IBrush OpacityMask + public IBrush? OpacityMask { get => GetValue(OpacityMaskProperty); set => SetValue(OpacityMaskProperty, value); @@ -159,7 +159,7 @@ namespace Avalonia.Media public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) { - if (((brush == null) && (pen == null)) || (geometry == null)) + if ((brush == null) && (pen == null)) { return; } @@ -167,9 +167,9 @@ namespace Avalonia.Media AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); } - public void DrawGlyphRun(IBrush foreground, IRef glyphRun) + public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) { - if (foreground == null || glyphRun == null) + if (foreground == null) { return; } @@ -184,7 +184,7 @@ namespace Avalonia.Media AddDrawing(glyphRunDrawing); } - public void DrawLine(IPen pen, Point p1, Point p2) + public void DrawLine(IPen? pen, Point p1, Point p2) { if (pen == null) { diff --git a/src/Avalonia.Base/Media/DrawingImage.cs b/src/Avalonia.Base/Media/DrawingImage.cs index 38ddbdfaed..1b22a1ee69 100644 --- a/src/Avalonia.Base/Media/DrawingImage.cs +++ b/src/Avalonia.Base/Media/DrawingImage.cs @@ -20,8 +20,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty DrawingProperty = - AvaloniaProperty.Register(nameof(Drawing)); + public static readonly StyledProperty DrawingProperty = + AvaloniaProperty.Register(nameof(Drawing)); /// public event EventHandler? Invalidated; @@ -30,7 +30,7 @@ namespace Avalonia.Media /// Gets or sets the drawing content. /// [Content] - public Drawing Drawing + public Drawing? Drawing { get => GetValue(DrawingProperty); set => SetValue(DrawingProperty, value); diff --git a/src/Avalonia.Base/Media/FontFamily.cs b/src/Avalonia.Base/Media/FontFamily.cs index da84861668..f4406bd010 100644 --- a/src/Avalonia.Base/Media/FontFamily.cs +++ b/src/Avalonia.Base/Media/FontFamily.cs @@ -119,7 +119,7 @@ namespace Avalonia.Media case 2: { - var source = segments[0].StartsWith("/") + var source = segments[0].StartsWith("/", StringComparison.Ordinal) ? new Uri(segments[0], UriKind.Relative) : new Uri(segments[0], UriKind.RelativeOrAbsolute); @@ -188,7 +188,7 @@ namespace Avalonia.Media { unchecked { - return ((FamilyNames != null ? FamilyNames.GetHashCode() : 0) * 397) ^ (Key != null ? Key.GetHashCode() : 0); + return (FamilyNames.GetHashCode() * 397) ^ (Key is not null ? Key.GetHashCode() : 0); } } diff --git a/src/Avalonia.Base/Media/Fonts/FontFamilyKey.cs b/src/Avalonia.Base/Media/Fonts/FontFamilyKey.cs index f607c67fed..12bb7e77e7 100644 --- a/src/Avalonia.Base/Media/Fonts/FontFamilyKey.cs +++ b/src/Avalonia.Base/Media/Fonts/FontFamilyKey.cs @@ -41,10 +41,7 @@ namespace Avalonia.Media.Fonts { var hash = (int)2166136261; - if (Source != null) - { - hash = (hash * 16777619) ^ Source.GetHashCode(); - } + hash = (hash * 16777619) ^ Source.GetHashCode(); if (BaseUri != null) { diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs index 0bab473442..3b63a98720 100644 --- a/src/Avalonia.Base/Media/FormattedText.cs +++ b/src/Avalonia.Base/Media/FormattedText.cs @@ -741,6 +741,11 @@ namespace Avalonia.Media null // no previous line break ); + if(Current is null) + { + return false; + } + // check if this line fits the text height if (_totalHeight + Current.Height > _that._maxTextHeight) { @@ -779,7 +784,7 @@ namespace Avalonia.Media // maybe there is no next line at all if (Position + Current.Length < _that._text.Length) { - bool nextLineFits; + bool nextLineFits = false; if (_lineCount + 1 >= _that._maxLineCount) { @@ -795,7 +800,10 @@ namespace Avalonia.Media currentLineBreak ); - nextLineFits = (_totalHeight + Current.Height + _nextLine.Height <= _that._maxTextHeight); + if(_nextLine != null) + { + nextLineFits = (_totalHeight + Current.Height + _nextLine.Height <= _that._maxTextHeight); + } } if (!nextLineFits) @@ -819,16 +827,22 @@ namespace Avalonia.Media _previousLineBreak ); - currentLineBreak = Current.TextLineBreak; + if(Current != null) + { + currentLineBreak = Current.TextLineBreak; + } _that._defaultParaProps.SetTextWrapping(currentWrap); } } } - _previousHeight = Current.Height; + if(Current != null) + { + _previousHeight = Current.Height; - Length = Current.Length; + Length = Current.Length; + } _previousLineBreak = currentLineBreak; @@ -838,7 +852,7 @@ namespace Avalonia.Media /// /// Wrapper of TextFormatter.FormatLine that auto-collapses the line if needed. /// - private TextLine FormatLine(ITextSource textSource, int textSourcePosition, double maxLineLength, TextParagraphProperties paraProps, TextLineBreak? lineBreak) + private TextLine? FormatLine(ITextSource textSource, int textSourcePosition, double maxLineLength, TextParagraphProperties paraProps, TextLineBreak? lineBreak) { var line = _formatter.FormatLine( textSource, @@ -848,7 +862,7 @@ namespace Avalonia.Media lineBreak ); - if (_that._trimming != TextTrimming.None && line.HasOverflowed && line.Length > 0) + if (line != null && _that._trimming != TextTrimming.None && line.HasOverflowed && line.Length > 0) { // what I really need here is the last displayed text run of the line // textSourcePosition + line.Length - 1 works except the end of paragraph case, @@ -1340,7 +1354,7 @@ namespace Avalonia.Media { var highlightBounds = currentLine.GetTextBounds(x0,x1 - x0); - if (highlightBounds != null) + if (highlightBounds.Count > 0) { foreach (var bound in highlightBounds) { @@ -1351,7 +1365,7 @@ namespace Avalonia.Media // Convert logical units (which extend leftward from the right edge // of the paragraph) to physical units. // - // Note that since rect is in logical units, rect.Right corresponds to + // Note that since rect is in logical units, rect.Right corresponds to // the visual *left* edge of the rectangle in the RTL case. Specifically, // is the distance leftward from the right edge of the formatting rectangle // whose width is the paragraph width passed to FormatLine. @@ -1370,7 +1384,7 @@ namespace Avalonia.Media else { accumulatedBounds = Geometry.Combine(accumulatedBounds, rectangleGeometry, GeometryCombineMode.Union); - } + } } } } @@ -1601,11 +1615,11 @@ namespace Avalonia.Media } /// - public TextRun? GetTextRun(int textSourceCharacterIndex) + public TextRun GetTextRun(int textSourceCharacterIndex) { if (textSourceCharacterIndex >= _that._text.Length) { - return null; + return new TextEndOfParagraph(); } var thatFormatRider = new SpanRider(_that._formatRuns, _that._latestPosition, textSourceCharacterIndex); diff --git a/src/Avalonia.Base/Media/GeometryDrawing.cs b/src/Avalonia.Base/Media/GeometryDrawing.cs index 26cc2c3cab..ac2dce1e42 100644 --- a/src/Avalonia.Base/Media/GeometryDrawing.cs +++ b/src/Avalonia.Base/Media/GeometryDrawing.cs @@ -15,8 +15,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty GeometryProperty = - AvaloniaProperty.Register(nameof(Geometry)); + public static readonly StyledProperty GeometryProperty = + AvaloniaProperty.Register(nameof(Geometry)); /// /// Defines the property. @@ -34,7 +34,7 @@ namespace Avalonia.Media /// Gets or sets the that describes the shape of this . /// [Content] - public Geometry Geometry + public Geometry? Geometry { get => GetValue(GeometryProperty); set => SetValue(GeometryProperty, value); diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 0ec7152359..2966ceee8d 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -166,7 +166,7 @@ namespace Avalonia.Media /// public Point BaselineOrigin { - get => _baselineOrigin ?? default; + get => PlatformImpl.Item.BaselineOrigin; set => Set(ref _baselineOrigin, value); } diff --git a/src/Avalonia.Base/Media/GlyphRunDrawing.cs b/src/Avalonia.Base/Media/GlyphRunDrawing.cs index 242b9913fa..06d92fd81c 100644 --- a/src/Avalonia.Base/Media/GlyphRunDrawing.cs +++ b/src/Avalonia.Base/Media/GlyphRunDrawing.cs @@ -2,19 +2,19 @@ { public class GlyphRunDrawing : Drawing { - public static readonly StyledProperty ForegroundProperty = - AvaloniaProperty.Register(nameof(Foreground)); + public static readonly StyledProperty ForegroundProperty = + AvaloniaProperty.Register(nameof(Foreground)); - public static readonly StyledProperty GlyphRunProperty = - AvaloniaProperty.Register(nameof(GlyphRun)); + public static readonly StyledProperty GlyphRunProperty = + AvaloniaProperty.Register(nameof(GlyphRun)); - public IBrush Foreground + public IBrush? Foreground { get => GetValue(ForegroundProperty); set => SetValue(ForegroundProperty, value); } - public GlyphRun GlyphRun + public GlyphRun? GlyphRun { get => GetValue(GlyphRunProperty); set => SetValue(GlyphRunProperty, value); diff --git a/src/Avalonia.Base/Media/HslColor.cs b/src/Avalonia.Base/Media/HslColor.cs index 425a3138c3..b4bf6fd217 100644 --- a/src/Avalonia.Base/Media/HslColor.cs +++ b/src/Avalonia.Base/Media/HslColor.cs @@ -254,7 +254,7 @@ namespace Avalonia.Media /// The HSL color string to parse. /// The parsed . /// True if parsing was successful; otherwise, false. - public static bool TryParse(string s, out HslColor hslColor) + public static bool TryParse(string? s, out HslColor hslColor) { bool prefixMatched = false; diff --git a/src/Avalonia.Base/Media/HsvColor.cs b/src/Avalonia.Base/Media/HsvColor.cs index 9f95b31518..f97457c54d 100644 --- a/src/Avalonia.Base/Media/HsvColor.cs +++ b/src/Avalonia.Base/Media/HsvColor.cs @@ -254,7 +254,7 @@ namespace Avalonia.Media /// The HSV color string to parse. /// The parsed . /// True if parsing was successful; otherwise, false. - public static bool TryParse(string s, out HsvColor hsvColor) + public static bool TryParse(string? s, out HsvColor hsvColor) { bool prefixMatched = false; diff --git a/src/Avalonia.Base/Media/IVisualBrush.cs b/src/Avalonia.Base/Media/IVisualBrush.cs index 6662613ff4..a7d3e4da10 100644 --- a/src/Avalonia.Base/Media/IVisualBrush.cs +++ b/src/Avalonia.Base/Media/IVisualBrush.cs @@ -1,5 +1,4 @@ using Avalonia.Metadata; -using Avalonia.VisualTree; namespace Avalonia.Media { @@ -12,6 +11,6 @@ namespace Avalonia.Media /// /// Gets the visual to draw. /// - Visual Visual { get; } + Visual? Visual { get; } } } diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs b/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs index 1f53f06955..6dff006045 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs @@ -39,17 +39,8 @@ namespace Avalonia.Media.Immutable { return true; } - else if (other is null) - { - return false; - } - if (Offset != other.Offset) - { - return false; - } - - return SequenceEqual(Dashes, other.Dashes); + return other is not null && Offset == other.Offset && SequenceEqual(_dashes, other.Dashes); } /// @@ -58,30 +49,27 @@ namespace Avalonia.Media.Immutable var hashCode = 717868523; hashCode = hashCode * -1521134295 + Offset.GetHashCode(); - if (_dashes != null) + foreach (var i in _dashes) { - foreach (var i in _dashes) - { - hashCode = hashCode * -1521134295 + i.GetHashCode(); - } + hashCode = hashCode * -1521134295 + i.GetHashCode(); } return hashCode; } - private static bool SequenceEqual(IReadOnlyList left, IReadOnlyList? right) + private static bool SequenceEqual(double[] left, IReadOnlyList? right) { if (ReferenceEquals(left, right)) { return true; } - if (left == null || right == null || left.Count != right.Count) + if (right is null || left.Length != right.Count) { return false; } - for (var c = 0; c < left.Count; c++) + for (var c = 0; c < left.Length; c++) { if (left[c] != right[c]) { diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs index 9b443391c5..0b625080e3 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs @@ -1,5 +1,4 @@ using Avalonia.Media.Imaging; -using Avalonia.VisualTree; namespace Avalonia.Media.Immutable { @@ -31,11 +30,11 @@ namespace Avalonia.Media.Immutable RelativeRect? destinationRect = null, double opacity = 1, ImmutableTransform? transform = null, - RelativePoint transformOrigin = new RelativePoint(), + RelativePoint transformOrigin = default, RelativeRect? sourceRect = null, Stretch stretch = Stretch.Uniform, TileMode tileMode = TileMode.None, - Imaging.BitmapInterpolationMode bitmapInterpolationMode = Imaging.BitmapInterpolationMode.Default) + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) : base( alignmentX, alignmentY, @@ -62,6 +61,6 @@ namespace Avalonia.Media.Immutable } /// - public Visual Visual { get; } + public Visual? Visual { get; } } } diff --git a/src/Avalonia.Base/Media/TextDecoration.cs b/src/Avalonia.Base/Media/TextDecoration.cs index dc9e5cb907..b74b7df9c5 100644 --- a/src/Avalonia.Base/Media/TextDecoration.cs +++ b/src/Avalonia.Base/Media/TextDecoration.cs @@ -22,8 +22,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty StrokeProperty = - AvaloniaProperty.Register(nameof(Stroke)); + public static readonly StyledProperty StrokeProperty = + AvaloniaProperty.Register(nameof(Stroke)); /// /// Defines the property. @@ -34,8 +34,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty> StrokeDashArrayProperty = - AvaloniaProperty.Register>(nameof(StrokeDashArray)); + public static readonly StyledProperty?> StrokeDashArrayProperty = + AvaloniaProperty.Register?>(nameof(StrokeDashArray)); /// /// Defines the property. @@ -82,7 +82,7 @@ namespace Avalonia.Media /// /// Gets or sets the that specifies how the is painted. /// - public IBrush Stroke + public IBrush? Stroke { get { return GetValue(StrokeProperty); } set { SetValue(StrokeProperty, value); } @@ -101,7 +101,7 @@ namespace Avalonia.Media /// Gets or sets a collection of values that indicate the pattern of dashes and gaps /// that is used to draw the . /// - public AvaloniaList StrokeDashArray + public AvaloniaList? StrokeDashArray { get { return GetValue(StrokeDashArrayProperty); } set { SetValue(StrokeDashArrayProperty, value); } @@ -220,7 +220,7 @@ namespace Avalonia.Media var intersections = glyphRun.PlatformImpl.Item.GetIntersections((float)(thickness * 0.5d - offsetY), (float)(thickness * 1.5d - offsetY)); - if (intersections != null && intersections.Count > 0) + if (intersections.Count > 0) { var last = baselineOrigin.X; var finalPos = last + glyphRun.Size.Width; diff --git a/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs b/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs index 26966b37bc..32012ab8e9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs @@ -1,6 +1,4 @@ -using Avalonia.Metadata; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { /// /// Produces objects that are used by the . diff --git a/src/Avalonia.Base/Media/TextFormatting/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/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 82cf3297fd..b4734d702b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -82,24 +82,15 @@ namespace Avalonia.Media.TextFormatting var previousGlyphTypeface = previousProperties?.CachedGlyphTypeface; var textSpan = text.Span; - if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count, out var script)) + if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count)) { - if (script == Script.Common && previousGlyphTypeface is not null) - { - if (TryGetShapeableLength(textSpan, previousGlyphTypeface, null, out var fallbackCount, out _)) - { - return new UnshapedTextRun(text.Slice(0, fallbackCount), - defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel); - } - } - return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(defaultTypeface), biDiLevel); } if (previousGlyphTypeface is not null) { - if (TryGetShapeableLength(textSpan, previousGlyphTypeface, defaultGlyphTypeface, out count, out _)) + if (TryGetShapeableLength(textSpan, previousGlyphTypeface, defaultGlyphTypeface, out count)) { return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel); @@ -127,14 +118,17 @@ namespace Avalonia.Media.TextFormatting fontManager.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo, out var fallbackTypeface); - - var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface); - - if (matchFound && TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count, out _)) + + if (matchFound) { - //Fallback found - return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface), - biDiLevel); + // Fallback found + var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface); + + if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count)) + { + return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface), + biDiLevel); + } } // no fallback found @@ -160,17 +154,15 @@ namespace Avalonia.Media.TextFormatting /// The typeface that is used to find matching characters. /// The default typeface. /// The shapeable length. - /// /// internal static bool TryGetShapeableLength( ReadOnlySpan text, IGlyphTypeface glyphTypeface, IGlyphTypeface? defaultGlyphTypeface, - out int length, - out Script script) + out int length) { length = 0; - script = Script.Unknown; + var script = Script.Unknown; if (text.IsEmpty) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs index 0b5d7649d7..ff8c1c4860 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs @@ -38,7 +38,7 @@ /// A value that specifies the text formatter state, /// in terms of where the previous line in the paragraph was broken by the text formatting process. /// The formatted line. - public abstract TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, + public abstract TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 7505b9ccdd..7f74f49982 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; @@ -19,71 +18,63 @@ namespace Avalonia.Media.TextFormatting [ThreadStatic] private static BidiAlgorithm? t_bidiAlgorithm; /// - public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, + public override TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null) { - 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) + fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, out var textEndOfLine, + out var textSourceLength); + + if (fetchedRuns.Count == 0) { - resolvedFlowDirection = previousLineBreak.FlowDirection; - textRuns = remainingRuns; - nextLineBreak = previousLineBreak; - shapedTextRuns = null; + return null; } - else - { - shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager, - out resolvedFlowDirection); - textRuns = shapedTextRuns; - 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(); + 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 +99,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 +120,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++) { @@ -249,49 +241,49 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { case UnshapedTextRun shapeableRun: - { - groupedRuns.Clear(); - groupedRuns.Add(shapeableRun); + { + groupedRuns.Clear(); + groupedRuns.Add(shapeableRun); - var text = shapeableRun.Text; - var properties = shapeableRun.Properties; + var text = shapeableRun.Text; + var properties = shapeableRun.Properties; - while (index + 1 < processedRuns.Count) - { - if (processedRuns[index + 1] is not UnshapedTextRun nextRun) + while (index + 1 < processedRuns.Count) { + if (processedRuns[index + 1] is not UnshapedTextRun nextRun) + { + break; + } + + if (shapeableRun.BidiLevel == nextRun.BidiLevel + && TryJoinContiguousMemories(text, nextRun.Text, out var joinedText) + && CanShapeTogether(properties, nextRun.Properties)) + { + groupedRuns.Add(nextRun); + index++; + shapeableRun = nextRun; + text = joinedText; + continue; + } + break; } - if (shapeableRun.BidiLevel == nextRun.BidiLevel - && TryJoinContiguousMemories(text, nextRun.Text, out var joinedText) - && CanShapeTogether(properties, nextRun.Properties)) - { - groupedRuns.Add(nextRun); - index++; - shapeableRun = nextRun; - text = joinedText; - continue; - } + var shaperOptions = new TextShaperOptions( + properties.CachedGlyphTypeface, + properties.FontRenderingEmSize, shapeableRun.BidiLevel, properties.CultureInfo, + paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); + + ShapeTogether(groupedRuns, text, shaperOptions, textShaper, shapedRuns); break; } - - var shaperOptions = new TextShaperOptions( - properties.CachedGlyphTypeface, - properties.FontRenderingEmSize, shapeableRun.BidiLevel, properties.CultureInfo, - paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); - - ShapeTogether(groupedRuns, text, shaperOptions, textShaper, shapedRuns); - - break; - } default: - { - shapedRuns.Add(currentRun); + { + shapedRuns.Add(currentRun); - break; - } + break; + } } } } @@ -504,16 +496,7 @@ namespace Avalonia.Media.TextFormatting while (textRunEnumerator.MoveNext()) { - var textRun = textRunEnumerator.Current; - - if (textRun == null) - { - textRuns.Add(new TextEndOfParagraph()); - - textSourceLength += TextRun.DefaultTextSourceLength; - - break; - } + TextRun textRun = textRunEnumerator.Current!; if (textRun is TextEndOfLine textEndOfLine) { @@ -653,7 +636,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 +658,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)) @@ -712,7 +695,7 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { case ShapedTextRun: - { + { var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); while (lineBreaker.MoveNext(out var lineBreak)) @@ -754,7 +737,7 @@ namespace Avalonia.Media.TextFormatting break; } - while (lineBreaker.MoveNext(out lineBreak) && index < textRuns.Count) + while (lineBreaker.MoveNext(out lineBreak)) { currentPosition += lineBreak.PositionWrap; @@ -780,6 +763,11 @@ namespace Avalonia.Media.TextFormatting currentPosition = currentLength + lineBreak.PositionWrap; } + if (currentPosition == 0 && measuredLength > 0) + { + currentPosition = measuredLength; + } + breakFound = true; break; @@ -819,13 +807,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 +845,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..4dbc472133 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -238,7 +238,7 @@ namespace Avalonia.Media.TextFormatting foreach (var textLine in _textLines) { //Current line isn't covered. - if (textLine.FirstTextSourceIndex + textLine.Length < start) + if (textLine.FirstTextSourceIndex + textLine.Length <= start) { currentY += textLine.Height; @@ -348,14 +348,36 @@ namespace Avalonia.Media.TextFormatting { var (x, y) = point; - var lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length; - var isInside = x >= 0 && x <= textLine.Width && y >= 0 && y <= textLine.Height; - if (x >= textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0) + var lastTrailingIndex = 0; + + if(_paragraphProperties.FlowDirection== FlowDirection.LeftToRight) { - lastTrailingIndex -= textLine.NewLineLength; + lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length; + + if (x >= textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0) + { + lastTrailingIndex -= textLine.NewLineLength; + } + + if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfLine textEndOfLine) + { + lastTrailingIndex -= textEndOfLine.Length; + } } + else + { + if (x <= textLine.WidthIncludingTrailingWhitespace - textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0) + { + lastTrailingIndex += textLine.NewLineLength; + } + + if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfLine textEndOfLine) + { + lastTrailingIndex += textEndOfLine.Length; + } + } var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; @@ -391,7 +413,7 @@ namespace Avalonia.Media.TextFormatting /// private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize, IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping, - TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight, + TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight, double letterSpacing) { var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground); @@ -416,9 +438,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 +451,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); @@ -456,12 +478,12 @@ namespace Avalonia.Media.TextFormatting var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth, _paragraphProperties, previousLine?.TextLineBreak); - if (textLine.Length == 0) + if (textLine is null) { if (previousLine != null && previousLine.NewLineLength > 0) { var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, - _paragraphProperties, fontManager); + _paragraphProperties); textLines.Add(emptyTextLine); @@ -504,7 +526,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)); } @@ -518,11 +540,9 @@ 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..187b3154ad 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -10,6 +10,7 @@ namespace Avalonia.Media.TextFormatting private readonly double _paragraphWidth; private readonly TextParagraphProperties _paragraphProperties; private TextLineMetrics _textLineMetrics; + private TextLineBreak? _textLineBreak; private readonly FlowDirection _resolvedFlowDirection; public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, double paragraphWidth, @@ -18,7 +19,7 @@ namespace Avalonia.Media.TextFormatting { FirstTextSourceIndex = firstTextSourceIndex; Length = length; - TextLineBreak = lineBreak; + _textLineBreak = lineBreak; HasCollapsed = hasCollapsed; _textRuns = textRuns; @@ -38,7 +39,7 @@ namespace Avalonia.Media.TextFormatting public override int Length { get; } /// - public override TextLineBreak? TextLineBreak { get; } + public override TextLineBreak? TextLineBreak => _textLineBreak; /// public override bool HasCollapsed { get; } @@ -167,38 +168,54 @@ namespace Avalonia.Media.TextFormatting { if (_textRuns.Length == 0) { - return new CharacterHit(); + return new CharacterHit(FirstTextSourceIndex); } distance -= Start; + var lastIndex = _textRuns.Length - 1; + + if (_textRuns[lastIndex] is TextEndOfLine) + { + lastIndex--; + } + + var currentPosition = FirstTextSourceIndex; + + if (lastIndex < 0) + { + return new CharacterHit(currentPosition); + } + if (distance <= 0) { var firstRun = _textRuns[0]; - return GetRunCharacterHit(firstRun, FirstTextSourceIndex, 0); + if (_paragraphProperties.FlowDirection == FlowDirection.RightToLeft) + { + currentPosition = Length - firstRun.Length; + } + + return GetRunCharacterHit(firstRun, currentPosition, 0); } if (distance >= WidthIncludingTrailingWhitespace) { - var lastRun = _textRuns[_textRuns.Length - 1]; + var lastRun = _textRuns[lastIndex]; - var size = 0.0; - - if (lastRun is DrawableTextRun drawableTextRun) + if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight) { - size = drawableTextRun.Size.Width; + currentPosition = Length - lastRun.Length; } - return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, size); + return GetRunCharacterHit(lastRun, currentPosition, distance); } // process hit that happens within the line var characterHit = new CharacterHit(); - var currentPosition = FirstTextSourceIndex; var currentDistance = 0.0; - for (var i = 0; i < _textRuns.Length; i++) + for (var i = 0; i <= lastIndex; i++) { var currentRun = _textRuns[i]; @@ -230,7 +247,7 @@ namespace Avalonia.Media.TextFormatting currentRun = _textRuns[j]; - if(currentRun is not ShapedTextRun) + if (currentRun is not ShapedTextRun) { continue; } @@ -262,10 +279,6 @@ namespace Avalonia.Media.TextFormatting continue; } } - else - { - continue; - } break; } @@ -410,10 +423,10 @@ namespace Avalonia.Media.TextFormatting { if (currentGlyphRun != null) { - distance = currentGlyphRun.Size.Width - distance; + currentDistance -= currentGlyphRun.Size.Width; } - return Math.Max(0, currentDistance - distance); + return currentDistance + distance; } if (currentRun is DrawableTextRun drawableTextRun) @@ -563,386 +576,505 @@ namespace Avalonia.Media.TextFormatting return GetPreviousCaretCharacterHit(characterHit); } - private IReadOnlyList GetTextBoundsLeftToRight(int firstTextSourceIndex, int textLength) + public override IReadOnlyList GetTextBounds(int firstTextSourceIndex, int textLength) { - var characterIndex = firstTextSourceIndex + textLength; + if (_textRuns.Length == 0) + { + return Array.Empty(); + } - var result = new List(_textRuns.Length); - var lastDirection = FlowDirection.LeftToRight; - var currentDirection = lastDirection; + var result = new List(); var currentPosition = FirstTextSourceIndex; var remainingLength = textLength; - var startX = Start; - double currentWidth = 0; - var currentRect = default(Rect); - - TextRunBounds lastRunBounds = default; - - for (var index = 0; index < _textRuns.Length; index++) + static FlowDirection GetDirection(TextRun textRun, FlowDirection currentDirection) { - if (_textRuns[index] is not DrawableTextRun currentRun) + if (textRun is ShapedTextRun shapedTextRun) { - continue; + return shapedTextRun.ShapedBuffer.IsLeftToRight ? + FlowDirection.LeftToRight : + FlowDirection.RightToLeft; } - var characterLength = 0; - var endX = startX; - - TextRunBounds currentRunBounds; + return currentDirection; + } - double combinedWidth; + if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight) + { + var currentX = Start; - if (currentRun is ShapedTextRun currentShapedRun) + for (int i = 0; i < _textRuns.Length; i++) { - var firstCluster = currentShapedRun.GlyphRun.Metrics.FirstCluster; + var currentRun = _textRuns[i]; - if (currentPosition + currentRun.Length <= firstTextSourceIndex) + var firstRunIndex = i; + var lastRunIndex = firstRunIndex; + var currentDirection = GetDirection(currentRun, FlowDirection.LeftToRight); + var directionalWidth = 0.0; + + if (currentRun is DrawableTextRun currentDrawable) { - startX += currentRun.Size.Width; + directionalWidth = currentDrawable.Size.Width; + } - currentPosition += currentRun.Length; + // Find consecutive runs of same direction + for (; lastRunIndex + 1 < _textRuns.Length; lastRunIndex++) + { + var nextRun = _textRuns[lastRunIndex + 1]; - continue; + var nextDirection = GetDirection(nextRun, currentDirection); + + if (currentDirection != nextDirection) + { + break; + } + + if (nextRun is DrawableTextRun nextDrawable) + { + directionalWidth += nextDrawable.Size.Width; + } } - if (currentShapedRun.ShapedBuffer.IsLeftToRight) + //Skip runs that are not part of the hit test range + switch (currentDirection) { - var startIndex = firstCluster + Math.Max(0, firstTextSourceIndex - currentPosition); + case FlowDirection.RightToLeft: + { + for (; lastRunIndex >= firstRunIndex; lastRunIndex--) + { + currentRun = _textRuns[lastRunIndex]; - double startOffset; + if (currentPosition + currentRun.Length > firstTextSourceIndex) + { + break; + } - double endOffset; + currentPosition += currentRun.Length; - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + if (currentRun is DrawableTextRun drawableTextRun) + { + directionalWidth -= drawableTextRun.Size.Width; + currentX += drawableTextRun.Size.Width; + } - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + if(lastRunIndex - 1 < 0) + { + break; + } + } - startX += startOffset; + break; + } + default: + { + for (; firstRunIndex <= lastRunIndex; firstRunIndex++) + { + currentRun = _textRuns[firstRunIndex]; - endX += endOffset; + if (currentPosition + currentRun.Length > firstTextSourceIndex) + { + break; + } - var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); + currentPosition += currentRun.Length; - var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + if (currentRun is DrawableTextRun drawableTextRun) + { + currentX += drawableTextRun.Size.Width; + directionalWidth -= drawableTextRun.Size.Width; + } - characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength); + if(firstRunIndex + 1 == _textRuns.Length) + { + break; + } + } - currentDirection = FlowDirection.LeftToRight; + break; + } } - else + + i = lastRunIndex; + + if (directionalWidth == 0) { - var rightToLeftIndex = index; - var rightToLeftWidth = currentShapedRun.Size.Width; + continue; + } - while (rightToLeftIndex + 1 <= _textRuns.Length - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun) - { - if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) + var coveredLength = 0; + TextBounds? textBounds = null; + + switch (currentDirection) + { + + case FlowDirection.RightToLeft: { + textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX + directionalWidth, firstTextSourceIndex, + currentPosition, remainingLength, out coveredLength, out currentPosition); + + currentX += directionalWidth; + break; } + default: + { + textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex, + currentPosition, remainingLength, out coveredLength, out currentPosition); - rightToLeftIndex++; - - rightToLeftWidth += nextShapedRun.Size.Width; + currentX = textBounds.Rectangle.Right; - if (currentPosition + nextShapedRun.Length > firstTextSourceIndex + textLength) - { break; } + } - currentShapedRun = nextShapedRun; - } + if (coveredLength > 0) + { + result.Add(textBounds); - startX += rightToLeftWidth; + remainingLength -= coveredLength; + } - currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); + if (remainingLength <= 0) + { + break; + } + } + } + else + { + var currentX = Start + WidthIncludingTrailingWhitespace; - remainingLength -= currentRunBounds.Length; - currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length; - endX = currentRunBounds.Rectangle.Right; - startX = currentRunBounds.Rectangle.Left; + for (int i = _textRuns.Length - 1; i >= 0; i--) + { + var currentRun = _textRuns[i]; + var firstRunIndex = i; + var lastRunIndex = firstRunIndex; + var currentDirection = GetDirection(currentRun, FlowDirection.RightToLeft); + var directionalWidth = 0.0; - var rightToLeftRunBounds = new List { currentRunBounds }; + if (currentRun is DrawableTextRun currentDrawable) + { + directionalWidth = currentDrawable.Size.Width; + } + + // Find consecutive runs of same direction + for (; firstRunIndex - 1 > 0; firstRunIndex--) + { + var previousRun = _textRuns[firstRunIndex - 1]; + + var previousDirection = GetDirection(previousRun, currentDirection); + + if (currentDirection != previousDirection) + { + break; + } - for (int i = rightToLeftIndex - 1; i >= index; i--) + if (currentRun is DrawableTextRun previousDrawable) { - if (_textRuns[i] is not ShapedTextRun shapedRun) + directionalWidth += previousDrawable.Size.Width; + } + } + + //Skip runs that are not part of the hit test range + switch (currentDirection) + { + case FlowDirection.RightToLeft: { - continue; - } + for (; lastRunIndex >= firstRunIndex; lastRunIndex--) + { + currentRun = _textRuns[lastRunIndex]; - currentShapedRun = shapedRun; + if (currentPosition + currentRun.Length <= firstTextSourceIndex) + { + currentPosition += currentRun.Length; - currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); + if (currentRun is DrawableTextRun drawableTextRun) + { + currentX -= drawableTextRun.Size.Width; + directionalWidth -= drawableTextRun.Size.Width; + } - rightToLeftRunBounds.Insert(0, currentRunBounds); + continue; + } - remainingLength -= currentRunBounds.Length; - startX = currentRunBounds.Rectangle.Left; + break; + } - currentPosition += currentRunBounds.Length; - } + break; + } + default: + { + for (; firstRunIndex <= lastRunIndex; firstRunIndex++) + { + currentRun = _textRuns[firstRunIndex]; - combinedWidth = endX - startX; + if (currentPosition + currentRun.Length <= firstTextSourceIndex) + { + currentPosition += currentRun.Length; - currentRect = new Rect(startX, 0, combinedWidth, Height); + if (currentRun is DrawableTextRun drawableTextRun) + { + currentX += drawableTextRun.Size.Width; + directionalWidth -= drawableTextRun.Size.Width; + } - currentDirection = FlowDirection.RightToLeft; + continue; + } - if (!MathUtilities.IsZero(combinedWidth)) - { - result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds)); - } + break; + } - startX = endX; + break; + } } - } - else - { - if (currentPosition + currentRun.Length <= firstTextSourceIndex) - { - startX += currentRun.Size.Width; - currentPosition += currentRun.Length; + i = firstRunIndex; + if (directionalWidth == 0) + { continue; } - if (currentPosition < firstTextSourceIndex) - { - startX += currentRun.Size.Width; - } + var coveredLength = 0; + + TextBounds? textBounds = null; - if (currentPosition + currentRun.Length <= characterIndex) + switch (currentDirection) { - endX += currentRun.Size.Width; + case FlowDirection.LeftToRight: + { + textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX - directionalWidth, firstTextSourceIndex, + currentPosition, remainingLength, out coveredLength, out currentPosition); + + currentX -= directionalWidth; + + break; + } + default: + { + textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex, + currentPosition, remainingLength, out coveredLength, out currentPosition); - characterLength = currentRun.Length; + currentX = textBounds.Rectangle.Left; + + break; + } } - } - if (endX < startX) - { - (endX, startX) = (startX, endX); - } + //Visual order is always left to right so we need to insert + result.Insert(0, textBounds); - //Lines that only contain a linebreak need to be covered here - if (characterLength == 0) - { - characterLength = NewLineLength; + remainingLength -= coveredLength; + + if (remainingLength <= 0) + { + break; + } } + } - combinedWidth = endX - startX; + return result; + } - currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun); + private TextBounds GetTextRunBoundsRightToLeft(int firstRunIndex, int lastRunIndex, double endX, + int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition) + { + coveredLength = 0; + var textRunBounds = new List(); + var startX = endX; - currentPosition += characterLength; + for (int i = lastRunIndex; i >= firstRunIndex; i--) + { + var currentRun = _textRuns[i]; - remainingLength -= characterLength; + if (currentRun is ShapedTextRun shapedTextRun) + { + var runBounds = GetRunBoundsRightToLeft(shapedTextRun, startX, firstTextSourceIndex, remainingLength, currentPosition, out var offset); - startX = endX; + textRunBounds.Insert(0, runBounds); - if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0) - { - if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right)) + if (offset > 0) { - currentRect = currentRect.WithWidth(currentWidth + combinedWidth); + endX = runBounds.Rectangle.Right; - var textBounds = result[result.Count - 1]; + startX = endX; + } - textBounds.Rectangle = currentRect; + startX -= runBounds.Rectangle.Width; - textBounds.TextRunBounds.Add(currentRunBounds); - } - else + currentPosition += runBounds.Length + offset; + + coveredLength += runBounds.Length; + + remainingLength -= runBounds.Length; + } + else + { + if (currentRun is DrawableTextRun drawableTextRun) { - currentRect = currentRunBounds.Rectangle; + startX -= drawableTextRun.Size.Width; - result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds })); + textRunBounds.Insert(0, + new TextRunBounds( + new Rect(startX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun)); } - } - lastRunBounds = currentRunBounds; + currentPosition += currentRun.Length; - currentWidth += combinedWidth; + coveredLength += currentRun.Length; - if (remainingLength <= 0 || currentPosition >= characterIndex) + remainingLength -= currentRun.Length; + } + + if (remainingLength <= 0) { break; } - - lastDirection = currentDirection; } - return result; - } + newPosition = currentPosition; - private IReadOnlyList GetTextBoundsRightToLeft(int firstTextSourceIndex, int textLength) - { - var characterIndex = firstTextSourceIndex + textLength; + var runWidth = endX - startX; - var result = new List(_textRuns.Length); - var lastDirection = FlowDirection.LeftToRight; - var currentDirection = lastDirection; + var bounds = new Rect(startX, 0, runWidth, Height); - var currentPosition = FirstTextSourceIndex; - var remainingLength = textLength; + return new TextBounds(bounds, FlowDirection.RightToLeft, textRunBounds); + } - var startX = WidthIncludingTrailingWhitespace; - double currentWidth = 0; - var currentRect = default(Rect); + private TextBounds GetTextBoundsLeftToRight(int firstRunIndex, int lastRunIndex, double startX, + int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition) + { + coveredLength = 0; + var textRunBounds = new List(); + var endX = startX; - for (var index = _textRuns.Length - 1; index >= 0; index--) + for (int i = firstRunIndex; i <= lastRunIndex; i++) { - if (_textRuns[index] is not DrawableTextRun currentRun) - { - continue; - } - - if (currentPosition + currentRun.Length < firstTextSourceIndex) - { - startX -= currentRun.Size.Width; - - currentPosition += currentRun.Length; - - continue; - } - - var characterLength = 0; - var endX = startX; + var currentRun = _textRuns[i]; - if (currentRun is ShapedTextRun currentShapedRun) + if (currentRun is ShapedTextRun shapedTextRun) { - var offset = Math.Max(0, firstTextSourceIndex - currentPosition); - - currentPosition += offset; + var runBounds = GetRunBoundsLeftToRight(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition, out var offset); - var startIndex = currentPosition; - double startOffset; - double endOffset; + textRunBounds.Add(runBounds); - if (currentShapedRun.ShapedBuffer.IsLeftToRight) + if (offset > 0) { - if (currentPosition < startIndex) - { - startOffset = endOffset = 0; - } - else - { - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + startX = runBounds.Rectangle.Left; - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - } - } - else - { - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + endX = startX; } - startX -= currentRun.Size.Width - startOffset; - endX -= currentRun.Size.Width - endOffset; + currentPosition += runBounds.Length + offset; - var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); - var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + endX += runBounds.Rectangle.Width; - characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); + coveredLength += runBounds.Length; - currentDirection = currentShapedRun.ShapedBuffer.IsLeftToRight ? - FlowDirection.LeftToRight : - FlowDirection.RightToLeft; + remainingLength -= runBounds.Length; } else { - if (currentPosition + currentRun.Length <= characterIndex) + if (currentRun is DrawableTextRun drawableTextRun) { - endX -= currentRun.Size.Width; + textRunBounds.Add( + new TextRunBounds( + new Rect(endX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun)); + + endX += drawableTextRun.Size.Width; } - if (currentPosition < firstTextSourceIndex) - { - startX -= currentRun.Size.Width; + currentPosition += currentRun.Length; - characterLength = currentRun.Length; - } - } + coveredLength += currentRun.Length; - if (endX < startX) - { - (endX, startX) = (startX, endX); + remainingLength -= currentRun.Length; } - //Lines that only contain a linebreak need to be covered here - if (characterLength == 0) + if (remainingLength <= 0) { - characterLength = NewLineLength; + break; } + } - var runWidth = endX - startX; + newPosition = currentPosition; - var currentRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); + var runWidth = endX - startX; - if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0) - { - if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, Start + startX)) - { - currentRect = currentRect.WithWidth(currentWidth + runWidth); + var bounds = new Rect(startX, 0, runWidth, Height); - var textBounds = result[result.Count - 1]; + return new TextBounds(bounds, FlowDirection.LeftToRight, textRunBounds); + } - textBounds.Rectangle = currentRect; + private TextRunBounds GetRunBoundsLeftToRight(ShapedTextRun currentRun, double startX, + int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset) + { + var startIndex = currentPosition; - textBounds.TextRunBounds.Add(currentRunBounds); - } - else - { - currentRect = currentRunBounds.Rectangle; + offset = Math.Max(0, firstTextSourceIndex - currentPosition); - result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds })); - } - } + var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster; - currentWidth += runWidth; - currentPosition += characterLength; + if (currentPosition != firstCluster) + { + startIndex = firstCluster + offset; + } + else + { + startIndex += offset; + } - if (currentPosition > characterIndex) - { - break; - } + var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - lastDirection = currentDirection; - remainingLength -= characterLength; + var endX = startX + endOffset; + startX += startOffset; - if (remainingLength <= 0) - { - break; - } + var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); + + var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); + + if (endX < startX) + { + (endX, startX) = (startX, endX); } - result.Reverse(); + //Lines that only contain a linebreak need to be covered here + if (characterLength == 0) + { + characterLength = NewLineLength; + } - return result; + var runWidth = endX - startX; + + return new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); } - private TextRunBounds GetRightToLeftTextRunBounds(ShapedTextRun currentRun, double endX, int firstTextSourceIndex, int characterIndex, int currentPosition, int remainingLength) + private TextRunBounds GetRunBoundsRightToLeft(ShapedTextRun currentRun, double endX, + int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset) { var startX = endX; - var offset = Math.Max(0, firstTextSourceIndex - currentPosition); + var startIndex = currentPosition; - currentPosition += offset; + offset = Math.Max(0, firstTextSourceIndex - currentPosition); - var startIndex = currentPosition; + var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster; - double startOffset; - double endOffset; + if (currentPosition != firstCluster) + { + startIndex = firstCluster + offset; + } + else + { + startIndex += offset; + } - endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); startX -= currentRun.Size.Width - startOffset; endX -= currentRun.Size.Width - endOffset; @@ -968,16 +1100,6 @@ namespace Avalonia.Media.TextFormatting return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); } - public override IReadOnlyList GetTextBounds(int firstTextSourceIndex, int textLength) - { - if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight) - { - return GetTextBoundsLeftToRight(firstTextSourceIndex, textLength); - } - - return GetTextBoundsRightToLeft(firstTextSourceIndex, textLength); - } - public override void Dispose() { for (int i = 0; i < _textRuns.Length; i++) @@ -993,6 +1115,11 @@ namespace Avalonia.Media.TextFormatting { _textLineMetrics = CreateLineMetrics(); + if (_textLineBreak is null && _textRuns.Length > 1 && _textRuns[_textRuns.Length - 1] is TextEndOfLine textEndOfLine) + { + _textLineBreak = new TextLineBreak(textEndOfLine); + } + BidiReorderer.Instance.BidiReorder(_textRuns, _resolvedFlowDirection); } @@ -1285,13 +1412,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) { @@ -1318,7 +1443,7 @@ namespace Avalonia.Media.TextFormatting { width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width; trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength; - newLineLength = textRun.GlyphRun.Metrics.NewLineLength; + newLineLength += textRun.GlyphRun.Metrics.NewLineLength; } widthIncludingWhitespace += textRun.Size.Width; @@ -1330,31 +1455,10 @@ namespace Avalonia.Media.TextFormatting { widthIncludingWhitespace += drawableTextRun.Size.Width; - switch (_paragraphProperties.FlowDirection) + if (index == lastRunIndex) { - case FlowDirection.LeftToRight: - { - if (index == lastRunIndex) - { - width = widthIncludingWhitespace; - trailingWhitespaceLength = 0; - newLineLength = 0; - } - - break; - } - - case FlowDirection.RightToLeft: - { - if (index == lastRunIndex) - { - width = widthIncludingWhitespace; - trailingWhitespaceLength = 0; - newLineLength = 0; - } - - break; - } + width = widthIncludingWhitespace; + trailingWhitespaceLength = 0; } if (drawableTextRun.Size.Height > height) diff --git a/src/Avalonia.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/Media/VisualBrush.cs b/src/Avalonia.Base/Media/VisualBrush.cs index 1261d233ac..2be3e9a94e 100644 --- a/src/Avalonia.Base/Media/VisualBrush.cs +++ b/src/Avalonia.Base/Media/VisualBrush.cs @@ -1,5 +1,4 @@ using Avalonia.Media.Immutable; -using Avalonia.VisualTree; namespace Avalonia.Media { @@ -11,8 +10,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty VisualProperty = - AvaloniaProperty.Register(nameof(Visual)); + public static readonly StyledProperty VisualProperty = + AvaloniaProperty.Register(nameof(Visual)); static VisualBrush() { @@ -38,7 +37,7 @@ namespace Avalonia.Media /// /// Gets or sets the visual to draw. /// - public Visual Visual + public Visual? Visual { get { return GetValue(VisualProperty); } set { SetValue(VisualProperty, value); } diff --git a/src/Avalonia.Base/Metadata/AmbientAttribute.cs b/src/Avalonia.Base/Metadata/AmbientAttribute.cs index 85ca6c4ec9..1c85a67641 100644 --- a/src/Avalonia.Base/Metadata/AmbientAttribute.cs +++ b/src/Avalonia.Base/Metadata/AmbientAttribute.cs @@ -3,10 +3,10 @@ using System; namespace Avalonia.Metadata { /// - /// Defines the ambient class/property + /// Defines the ambient class/property /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, Inherited = true)] - public class AmbientAttribute : Attribute + public sealed class AmbientAttribute : Attribute { } } diff --git a/src/Avalonia.Base/Metadata/ContentAttribute.cs b/src/Avalonia.Base/Metadata/ContentAttribute.cs index a0b2fa0e1d..f32c8e78f6 100644 --- a/src/Avalonia.Base/Metadata/ContentAttribute.cs +++ b/src/Avalonia.Base/Metadata/ContentAttribute.cs @@ -6,7 +6,7 @@ namespace Avalonia.Metadata /// Defines the property that contains the object's content in markup. /// [AttributeUsage(AttributeTargets.Property)] - public class ContentAttribute : Attribute + public sealed class ContentAttribute : Attribute { } } diff --git a/src/Avalonia.Base/Metadata/DataTypeAttribute.cs b/src/Avalonia.Base/Metadata/DataTypeAttribute.cs index ac46a0d30a..dd9603b4a9 100644 --- a/src/Avalonia.Base/Metadata/DataTypeAttribute.cs +++ b/src/Avalonia.Base/Metadata/DataTypeAttribute.cs @@ -9,7 +9,7 @@ namespace Avalonia.Metadata; /// Used on DataTemplate.DataType property so it can be inherited in compiled bindings inside of the template. /// [AttributeUsage(AttributeTargets.Property)] -public class DataTypeAttribute : Attribute +public sealed class DataTypeAttribute : Attribute { - + } diff --git a/src/Avalonia.Base/Metadata/DependsOnAttribute.cs b/src/Avalonia.Base/Metadata/DependsOnAttribute.cs index caee71ebfd..ca58a91eb9 100644 --- a/src/Avalonia.Base/Metadata/DependsOnAttribute.cs +++ b/src/Avalonia.Base/Metadata/DependsOnAttribute.cs @@ -6,7 +6,7 @@ namespace Avalonia.Metadata /// Indicates that the property depends on the value of another property in markup. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] - public class DependsOnAttribute : Attribute + public sealed class DependsOnAttribute : Attribute { /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs b/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs index 6bb820d214..fac8cd8737 100644 --- a/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs +++ b/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs @@ -25,9 +25,9 @@ public sealed class InheritDataTypeFromItemsAttribute : Attribute /// The name of the property whose item type should be used on the target property. /// public string AncestorItemsProperty { get; } - + /// - /// The ancestor type to be used in a lookup for the . + /// The ancestor type to be used in a lookup for the . /// If null, the declaring type of the target property is used. /// public Type? AncestorType { get; set; } diff --git a/src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs b/src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs index 348c983c03..75fe7b8031 100644 --- a/src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs +++ b/src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs @@ -11,7 +11,7 @@ namespace Avalonia.Metadata /// may be added to its API. /// [AttributeUsage(AttributeTargets.Interface)] - public class NotClientImplementableAttribute : Attribute + public sealed class NotClientImplementableAttribute : Attribute { } } diff --git a/src/Avalonia.Base/Metadata/TemplateContent.cs b/src/Avalonia.Base/Metadata/TemplateContent.cs index 258154aba4..78bcc2ff29 100644 --- a/src/Avalonia.Base/Metadata/TemplateContent.cs +++ b/src/Avalonia.Base/Metadata/TemplateContent.cs @@ -6,7 +6,7 @@ namespace Avalonia.Metadata /// Defines the property that contains the object's content in markup. /// [AttributeUsage(AttributeTargets.Property)] - public class TemplateContentAttribute : Attribute + public sealed class TemplateContentAttribute : Attribute { public Type? TemplateResultType { get; set; } } diff --git a/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs b/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs index c46891b3ad..a644c9afe6 100644 --- a/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs +++ b/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs @@ -3,7 +3,7 @@ namespace Avalonia.Metadata { [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] - public class TrimSurroundingWhitespaceAttribute : Attribute + public sealed class TrimSurroundingWhitespaceAttribute : Attribute { } diff --git a/src/Avalonia.Base/Metadata/UnstableAttribute.cs b/src/Avalonia.Base/Metadata/UnstableAttribute.cs index 3b6fa5168a..361f6d30fd 100644 --- a/src/Avalonia.Base/Metadata/UnstableAttribute.cs +++ b/src/Avalonia.Base/Metadata/UnstableAttribute.cs @@ -6,7 +6,8 @@ namespace Avalonia.Metadata /// This API is unstable and is not covered by API compatibility guarantees between minor and /// patch releases. /// - public class UnstableAttribute : Attribute + [AttributeUsage(AttributeTargets.All)] + public sealed class UnstableAttribute : Attribute { } } diff --git a/src/Avalonia.Base/Metadata/UsableDuringInitializationAttribute.cs b/src/Avalonia.Base/Metadata/UsableDuringInitializationAttribute.cs index 753a96b9ce..d2d163b368 100644 --- a/src/Avalonia.Base/Metadata/UsableDuringInitializationAttribute.cs +++ b/src/Avalonia.Base/Metadata/UsableDuringInitializationAttribute.cs @@ -3,8 +3,8 @@ using System; namespace Avalonia.Metadata { [AttributeUsage(AttributeTargets.Class)] - public class UsableDuringInitializationAttribute : Attribute + public sealed class UsableDuringInitializationAttribute : Attribute { - + } } diff --git a/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs b/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs index aeaa38dad9..2fd2b1da3b 100644 --- a/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs +++ b/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs @@ -6,7 +6,7 @@ namespace Avalonia.Metadata /// Indicates that a collection type should be processed as being whitespace significant by a XAML processor. /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] - public class WhitespaceSignificantCollectionAttribute : Attribute + public sealed class WhitespaceSignificantCollectionAttribute : Attribute { } } diff --git a/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs b/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs index d43fa55f5c..c6b79ba987 100644 --- a/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs +++ b/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs @@ -6,7 +6,7 @@ namespace Avalonia.Metadata /// Maps an XML namespace to a CLR namespace for use in XAML. /// [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public class XmlnsDefinitionAttribute : Attribute + public sealed class XmlnsDefinitionAttribute : Attribute { /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index c05c04c22e..8509067cd0 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -49,7 +49,7 @@ namespace Avalonia.Platform /// The stroke pen. /// The first point of the line. /// The second point of the line. - void DrawLine(IPen pen, Point p1, Point p2); + void DrawLine(IPen? pen, Point p1, Point p2); /// /// Draws a geometry. @@ -91,7 +91,7 @@ namespace Avalonia.Platform /// /// The foreground. /// The glyph run. - void DrawGlyphRun(IBrush foreground, IRef glyphRun); + void DrawGlyphRun(IBrush? foreground, IRef glyphRun); /// /// Creates a new that can be used as a render layer diff --git a/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs b/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs index 467cd530fc..d1a803fefb 100644 --- a/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs +++ b/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs @@ -19,25 +19,20 @@ internal class AssemblyDescriptor : IAssemblyDescriptor public AssemblyDescriptor(Assembly assembly) { Assembly = assembly; + Resources = assembly.GetManifestResourceNames() + .ToDictionary(n => n, n => (IAssetDescriptor)new AssemblyResourceDescriptor(assembly, n)); + Name = assembly.GetName().Name; - if (assembly != null) + using var resources = assembly.GetManifestResourceStream(Constants.AvaloniaResourceName); + if (resources != null) { - Resources = assembly.GetManifestResourceNames() - .ToDictionary(n => n, n => (IAssetDescriptor)new AssemblyResourceDescriptor(assembly, n)); - Name = assembly.GetName().Name; - using (var resources = assembly.GetManifestResourceStream(Constants.AvaloniaResourceName)) - { - if (resources != null) - { - Resources.Remove(Constants.AvaloniaResourceName); + Resources.Remove(Constants.AvaloniaResourceName); - var indexLength = new BinaryReader(resources).ReadInt32(); - var index = AvaloniaResourcesIndexReaderWriter.ReadIndex(new SlicedStream(resources, 4, indexLength)); - var baseOffset = indexLength + 4; - AvaloniaResources = index.ToDictionary(r => GetPathRooted(r), r => (IAssetDescriptor) - new AvaloniaResourceDescriptor(assembly, baseOffset + r.Offset, r.Size)); - } - } + var indexLength = new BinaryReader(resources).ReadInt32(); + var index = AvaloniaResourcesIndexReaderWriter.ReadIndex(new SlicedStream(resources, 4, indexLength)); + var baseOffset = indexLength + 4; + AvaloniaResources = index.ToDictionary(GetPathRooted, r => (IAssetDescriptor) + new AvaloniaResourceDescriptor(assembly, baseOffset + r.Offset, r.Size)); } } @@ -45,6 +40,7 @@ internal class AssemblyDescriptor : IAssemblyDescriptor public Dictionary? Resources { get; } public Dictionary? AvaloniaResources { get; } public string? Name { get; } + private static string GetPathRooted(AvaloniaResourcesIndexEntry r) => r.Path![0] == '/' ? r.Path : '/' + r.Path; } diff --git a/src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs b/src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs index cad4ab2051..4b47c93eb5 100644 --- a/src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs +++ b/src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - namespace Avalonia.Platform; -public struct PlatformGraphicsExternalImageProperties +public record struct PlatformGraphicsExternalImageProperties { public int Width { get; set; } public int Height { get; set; } diff --git a/src/Avalonia.Base/Reactive/AnonymousObserver.cs b/src/Avalonia.Base/Reactive/AnonymousObserver.cs index c2e02ae879..6c458713dc 100644 --- a/src/Avalonia.Base/Reactive/AnonymousObserver.cs +++ b/src/Avalonia.Base/Reactive/AnonymousObserver.cs @@ -1,9 +1,14 @@ using System; +using System.Runtime.CompilerServices; using System.Threading.Tasks; namespace Avalonia.Reactive; -internal class AnonymousObserver : IObserver +/// +/// Class to create an instance from delegate-based implementations of the On* methods. +/// +/// The type of the elements in the sequence. +public class AnonymousObserver : IObserver { private static readonly Action ThrowsOnError = ex => throw ex; private static readonly Action NoOpCompleted = () => { }; diff --git a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs index 263109972f..cf20f20172 100644 --- a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs +++ b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs @@ -14,7 +14,7 @@ namespace Avalonia.Reactive /// usage. This class provides a more lightweight base for some internal observable types /// in the Avalonia framework. /// - public abstract class LightweightObservableBase : IObservable + internal abstract class LightweightObservableBase : IObservable { private Exception? _error; private List>? _observers = new List>(); diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs index a6db4330a3..455e9ebb5f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs @@ -23,7 +23,7 @@ namespace Avalonia.Rendering.Composition.Animations public abstract class CompositionAnimation : CompositionObject, ICompositionAnimationBase { private readonly CompositionPropertySet _propertySet; - internal CompositionAnimation(Compositor compositor) : base(compositor, null!) + internal CompositionAnimation(Compositor compositor) : base(compositor, null) { _propertySet = new CompositionPropertySet(compositor); } diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs index bad3991f43..1500e88abe 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.Composition.Animations public void Remove(CompositionAnimation value) => Animations.Remove(value); public void RemoveAll() => Animations.Clear(); - public CompositionAnimationGroup(Compositor compositor) : base(compositor, null!) + public CompositionAnimationGroup(Compositor compositor) : base(compositor, null) { } } diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs index 72be4edd07..d9adf261f8 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs @@ -23,7 +23,7 @@ namespace Avalonia.Rendering.Composition.Animations { private Dictionary _inner = new Dictionary(); private IDictionary _innerface; - internal ImplicitAnimationCollection(Compositor compositor) : base(compositor, null!) + internal ImplicitAnimationCollection(Compositor compositor) : base(compositor, null) { _innerface = _inner; } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index c5d7ec61e0..7fa2d4955f 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 @@ -23,21 +20,37 @@ public class CompositingRenderer : IRendererWithCompositor { private readonly IRenderRoot _root; private readonly Compositor _compositor; - CompositionDrawingContext _recorder = new(); - DrawingContext _recordingContext; - private HashSet _dirty = new(); - private HashSet _recalculateChildren = new(); + private readonly CompositionDrawingContext _recorder = new(); + private readonly DrawingContext _recordingContext; + private readonly HashSet _dirty = new(); + private readonly HashSet _recalculateChildren = new(); + private readonly Action _update; + private bool _queuedUpdate; - private Action _update; private bool _updating; + private bool _isDisposed; - internal CompositionTarget CompositionTarget; + internal CompositionTarget CompositionTarget { get; } /// /// Asks the renderer to only draw frames on the render thread. Makes Paint to wait until frame is rendered. /// public bool RenderOnlyOnRenderThread { get; set; } = true; + /// + public RendererDiagnostics Diagnostics { get; } + + /// + public Compositor Compositor => _compositor; + + /// + /// Initializes a new instance of + /// + /// The render root using this renderer. + /// The associated compositors. + /// + /// A function returning the list of native platform's surfaces that can be consumed by rendering subsystems. + /// public CompositingRenderer(IRenderRoot root, Compositor compositor, Func> surfaces) { _root = root; @@ -46,26 +59,27 @@ 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 - { - get => CompositionTarget.DrawFps; - set => CompositionTarget.DrawFps = value; - } - - /// - public bool DrawDirtyRects + private void OnDiagnosticsPropertyChanged(object? sender, PropertyChangedEventArgs e) { - 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; + } } /// public event EventHandler? SceneInvalidated; - void QueueUpdate() + private void QueueUpdate() { if(_queuedUpdate) return; @@ -76,9 +90,11 @@ public class CompositingRenderer : IRendererWithCompositor /// public void AddDirty(Visual visual) { + if (_isDisposed) + return; if (_updating) throw new InvalidOperationException("Visual was invalidated during the render pass"); - _dirty.Add((Visual)visual); + _dirty.Add(visual); QueueUpdate(); } @@ -125,9 +141,11 @@ public class CompositingRenderer : IRendererWithCompositor /// public void RecalculateChildren(Visual visual) { + if (_isDisposed) + return; if (_updating) throw new InvalidOperationException("Visual was invalidated during the render pass"); - _recalculateChildren.Add((Visual)visual); + _recalculateChildren.Add(visual); QueueUpdate(); } @@ -170,7 +188,7 @@ public class CompositingRenderer : IRendererWithCompositor if (sortedChildren != null) for (var c = 0; c < visualChildren.Count; c++) { - if (!ReferenceEquals(compositionChildren[c], ((Visual)sortedChildren[c].visual).CompositionVisual)) + if (!ReferenceEquals(compositionChildren[c], sortedChildren[c].visual.CompositionVisual)) { mismatch = true; break; @@ -178,7 +196,7 @@ public class CompositingRenderer : IRendererWithCompositor } else for (var c = 0; c < visualChildren.Count; c++) - if (!ReferenceEquals(compositionChildren[c], ((Visual)visualChildren[c]).CompositionVisual)) + if (!ReferenceEquals(compositionChildren[c], visualChildren[c].CompositionVisual)) { mismatch = true; break; @@ -200,7 +218,7 @@ public class CompositingRenderer : IRendererWithCompositor { foreach (var ch in sortedChildren) { - var compositionChild = ((Visual)ch.visual).CompositionVisual; + var compositionChild = ch.visual.CompositionVisual; if (compositionChild != null) compositionChildren.Add(compositionChild); } @@ -209,7 +227,7 @@ public class CompositingRenderer : IRendererWithCompositor else foreach (var ch in v.GetVisualChildren()) { - var compositionChild = ((Visual)ch).CompositionVisual; + var compositionChild = ch.CompositionVisual; if (compositionChild != null) compositionChildren.Add(compositionChild); } @@ -288,13 +306,18 @@ public class CompositingRenderer : IRendererWithCompositor _updating = false; } } - + + /// public void Resized(Size size) { } + /// public void Paint(Rect rect) { + if (_isDisposed) + return; + QueueUpdate(); CompositionTarget.RequestRedraw(); if(RenderOnlyOnRenderThread && Compositor.Loop.RunsInBackground) @@ -303,17 +326,34 @@ public class CompositingRenderer : IRendererWithCompositor CompositionTarget.ImmediateUIThreadRender(); } - public void Start() => CompositionTarget.IsEnabled = true; - - public void Stop() + /// + public void Start() { - CompositionTarget.IsEnabled = false; + if (_isDisposed) + return; + + CompositionTarget.IsEnabled = true; } - public ValueTask TryGetRenderInterfaceFeature(Type featureType) => Compositor.TryGetRenderInterfaceFeature(featureType); + /// + public void Stop() + => CompositionTarget.IsEnabled = false; + + /// + public ValueTask TryGetRenderInterfaceFeature(Type featureType) + => Compositor.TryGetRenderInterfaceFeature(featureType); + /// public void Dispose() { + if (_isDisposed) + return; + + _isDisposed = true; + _dirty.Clear(); + _recalculateChildren.Clear(); + SceneInvalidated = null; + Stop(); CompositionTarget.Dispose(); @@ -322,9 +362,4 @@ public class CompositingRenderer : IRendererWithCompositor if (Compositor.Loop.RunsInBackground) _compositor.Commit().Wait(); } - - /// - /// The associated object - /// - public Compositor Compositor => _compositor; } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs index ab4329df62..bfe70d593d 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs @@ -1,4 +1,3 @@ -using System; using System.Threading.Tasks; using Avalonia.Rendering.Composition.Server; using Avalonia.Threading; @@ -7,7 +6,7 @@ namespace Avalonia.Rendering.Composition; public class CompositionDrawingSurface : CompositionSurface { - internal new ServerCompositionDrawingSurface Server => (ServerCompositionDrawingSurface)base.Server; + internal new ServerCompositionDrawingSurface Server => (ServerCompositionDrawingSurface)base.Server!; internal CompositionDrawingSurface(Compositor compositor) : base(compositor, new ServerCompositionDrawingSurface(compositor.Server)) { } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs index 50332926ad..8c21b534db 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs @@ -22,7 +22,7 @@ namespace Avalonia.Rendering.Composition public ImplicitAnimationCollection? ImplicitAnimations { get; set; } private protected InlineDictionary PendingAnimations; - internal CompositionObject(Compositor compositor, ServerObject server) + internal CompositionObject(Compositor compositor, ServerObject? server) { Compositor = compositor; Server = server; @@ -32,7 +32,7 @@ namespace Avalonia.Rendering.Composition /// The associated Compositor /// public Compositor Compositor { get; } - internal ServerObject Server { get; } + internal ServerObject? Server { get; } public bool IsDisposed { get; private set; } private bool _registeredForSerialization; diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs index 7d794af9a2..efd89951bb 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs @@ -23,7 +23,7 @@ namespace Avalonia.Rendering.Composition private readonly Dictionary _variants = new Dictionary(); private readonly Dictionary _objects = new Dictionary(); - internal CompositionPropertySet(Compositor compositor) : base(compositor, null!) + internal CompositionPropertySet(Compositor compositor) : base(compositor, null) { } diff --git a/src/Avalonia.Base/Rendering/Composition/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/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs index 05488a558f..b75d080cfd 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -88,8 +88,13 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } /// - public void DrawLine(IPen pen, Point p1, Point p2) + public void DrawLine(IPen? pen, Point p1, Point p2) { + if (pen is null) + { + return; + } + var next = NextDrawAs(); if (next == null || !next.Item.Equals(Transform, pen, p1, p2)) @@ -159,8 +164,13 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW public object? GetFeature(Type t) => null; /// - public void DrawGlyphRun(IBrush foreground, IRef glyphRun) + public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) { + if (foreground is null) + { + return; + } + var next = NextDrawAs(); if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs index ff2069e71e..b15da5d05d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs @@ -39,7 +39,8 @@ namespace Avalonia.Rendering.Composition.Expressions } } - internal class PrettyPrintStringAttribute : Attribute + [AttributeUsage(AttributeTargets.Field)] + internal sealed class PrettyPrintStringAttribute : Attribute { public string Name { get; } @@ -164,8 +165,6 @@ namespace Avalonia.Rendering.Composition.Expressions public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) { - if (context.ForeignFunctionInterface == null) - return default; var args = new List(); foreach (var expr in Parameters) args.Add(expr.Evaluate(ref context)); diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs index 9086c59aad..f268364b54 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using Avalonia.Rendering.Composition.Server; // Special license applies License.md diff --git a/src/Avalonia.Base/Rendering/Composition/Server/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/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index c58beebe7f..50df8bd32b 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -66,7 +66,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont _impl.DrawBitmap(source, opacityMask, opacityMaskRect, destRect); } - public void DrawLine(IPen pen, Point p1, Point p2) + public void DrawLine(IPen? pen, Point p1, Point p2) { _impl.DrawLine(pen, p1, p2); } @@ -86,7 +86,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont _impl.DrawEllipse(brush, pen, rect); } - public void DrawGlyphRun(IBrush foreground, IRef glyphRun) + public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) { _impl.DrawGlyphRun(foreground, glyphRun); } diff --git a/src/Avalonia.Base/Rendering/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/IRenderRoot.cs b/src/Avalonia.Base/Rendering/IRenderRoot.cs index fa3260ffb4..09df7b7830 100644 --- a/src/Avalonia.Base/Rendering/IRenderRoot.cs +++ b/src/Avalonia.Base/Rendering/IRenderRoot.cs @@ -1,6 +1,4 @@ using Avalonia.Metadata; -using Avalonia.Platform; -using Avalonia.VisualTree; namespace Avalonia.Rendering { diff --git a/src/Avalonia.Base/Rendering/IRenderer.cs b/src/Avalonia.Base/Rendering/IRenderer.cs index f3f5b5e99b..7e32504e17 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. @@ -97,6 +90,9 @@ namespace Avalonia.Rendering public interface IRendererWithCompositor : IRenderer { + /// + /// The associated object + /// Compositor Compositor { get; } } } diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index c67ac7057d..8e5dc38317 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -48,8 +48,10 @@ namespace Avalonia.Rendering /// void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) { - var visual = brush.Visual; - Render(new DrawingContext(context), visual, visual.Bounds); + if (brush.Visual is { } visual) + { + Render(new DrawingContext(context), visual, visual.Bounds); + } } internal static void Render(Visual visual, DrawingContext context, bool updateTransformedBounds) diff --git a/src/Avalonia.Base/Rendering/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/Rendering/SceneGraph/ExperimentalAcrylicNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs index 12b67105e9..82f8fc2d56 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs @@ -80,11 +80,8 @@ namespace Avalonia.Rendering.SceneGraph { p *= Transform.Invert(); - if (Material != null) - { - var rect = Rect.Rect; - return rect.ContainsExclusive(p); - } + var rect = Rect.Rect; + return rect.ContainsExclusive(p); } return false; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs index 6d30358119..2bfd2080c3 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// The point in global coordinates. /// True if the point hits the node's geometry; otherwise false. /// - /// This method does not recurse to child s, if you want + /// This method does not recurse to childs, if you want /// to hit test children they must be hit tested manually. /// bool HitTest(Point p); 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/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index 3c44dd63ce..fafafabd82 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -212,7 +212,7 @@ namespace Avalonia.Utilities var toTypeConverter = TypeDescriptor.GetConverter(toUnderl); - if (toTypeConverter.CanConvertFrom(from) == true) + if (toTypeConverter.CanConvertFrom(from)) { result = toTypeConverter.ConvertFrom(null, culture, value); return true; @@ -220,7 +220,7 @@ namespace Avalonia.Utilities var fromTypeConverter = TypeDescriptor.GetConverter(from); - if (fromTypeConverter.CanConvertTo(toUnderl) == true) + if (fromTypeConverter.CanConvertTo(toUnderl)) { result = fromTypeConverter.ConvertTo(null, culture, value, toUnderl); return true; @@ -329,7 +329,7 @@ namespace Avalonia.Utilities } [RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConvertionRequiresUnreferencedCodeMessage)] - public static T ConvertImplicit(object value) + public static T ConvertImplicit(object? value) { if (TryConvertImplicit(typeof(T), value, out var result)) { @@ -369,11 +369,6 @@ namespace Avalonia.Utilities /// public static bool IsNumeric(Type type) { - if (type == null) - { - return false; - } - var underlyingType = Nullable.GetUnderlyingType(type); if (underlyingType != null) diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index e72606bf70..237a491615 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; using System.Runtime.CompilerServices; using Avalonia.Threading; @@ -15,7 +11,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event { private readonly Func, Action> _subscribe; - readonly ConditionalWeakTable _subscriptions = new(); + private readonly ConditionalWeakTable _subscriptions = new(); internal WeakEvent( Action> subscribe, @@ -51,56 +47,6 @@ public class WeakEvent : WeakEvent where TEventArgs : Event private readonly WeakEvent _ev; private readonly TSender _target; private readonly Action _compact; - - struct Entry - { - WeakReference>? _reference; - int _hashCode; - - public Entry(IWeakEventSubscriber r) - { - if (r == null) - { - _reference = null; - _hashCode = 0; - return; - } - - _hashCode = r.GetHashCode(); - _reference = new WeakReference>(r); - } - - public bool IsEmpty - { - get - { - if (_reference == null) - return true; - if (_reference.TryGetTarget(out _)) - return false; - _reference = null; - return true; - } - } - - public bool TryGetTarget([MaybeNullWhen(false)]out IWeakEventSubscriber target) - { - if (_reference == null) - { - target = null!; - return false; - } - return _reference.TryGetTarget(out target); - } - - public bool Equals(IWeakEventSubscriber r) - { - if (_reference == null || r.GetHashCode() != _hashCode) - return false; - return _reference.TryGetTarget(out var target) && target == r; - } - } - private readonly Action _unsubscribe; private readonly WeakHashList> _list = new(); private bool _compactScheduled; @@ -114,7 +60,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event _unsubscribe = ev._subscribe(target, OnEvent); } - void Destroy() + private void Destroy() { if(_destroyed) return; @@ -134,15 +80,15 @@ public class WeakEvent : WeakEvent where TEventArgs : Event ScheduleCompact(); } - void ScheduleCompact() + private void ScheduleCompact() { if(_compactScheduled || _destroyed) return; _compactScheduled = true; Dispatcher.UIThread.Post(_compact, DispatcherPriority.Background); } - - void Compact() + + private void Compact() { if(!_compactScheduled) return; @@ -152,7 +98,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event Destroy(); } - void OnEvent(object? sender, TEventArgs eventArgs) + private void OnEvent(object? sender, TEventArgs eventArgs) { var alive = _list.GetAlive(); if(alive == null) @@ -196,4 +142,4 @@ public class WeakEvent return () => unsubscribe(s, handler); }); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs index 020ba7a6d9..ef143144e6 100644 --- a/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs +++ b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs @@ -60,8 +60,7 @@ namespace Avalonia.Utilities private static class SubscriptionTypeStorage where TArgs : EventArgs where TSubscriber : class { - public static readonly ConditionalWeakTable> Subscribers - = new ConditionalWeakTable>(); + public static readonly ConditionalWeakTable> Subscribers = new(); } private class SubscriptionDic : Dictionary> @@ -69,8 +68,7 @@ namespace Avalonia.Utilities { } - private static readonly Dictionary> Accessors - = new Dictionary>(); + private static readonly Dictionary> s_accessors = new(); private class Subscription where T : EventArgs where TSubscriber : class { @@ -81,18 +79,17 @@ namespace Avalonia.Utilities private readonly Delegate _delegate; private Descriptor[] _data = new Descriptor[2]; - private int _count = 0; + private int _count; - delegate void CallerDelegate(TSubscriber s, object sender, T args); - - struct Descriptor + private delegate void CallerDelegate(TSubscriber s, object? sender, T args); + + private struct Descriptor { - public WeakReference Subscriber; - public CallerDelegate Caller; + public WeakReference? Subscriber; + public CallerDelegate? Caller; } - private static Dictionary s_Callers = - new Dictionary(); + private static readonly Dictionary s_callers = new(); public Subscription(SubscriptionDic sdic, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents)] Type targetType, @@ -101,8 +98,8 @@ namespace Avalonia.Utilities _sdic = sdic; _target = target; _eventName = eventName; - if (!Accessors.TryGetValue(targetType, out var evDic)) - Accessors[targetType] = evDic = new Dictionary(); + if (!s_accessors.TryGetValue(targetType, out var evDic)) + s_accessors[targetType] = evDic = new Dictionary(); if (evDic.TryGetValue(eventName, out var info)) { @@ -123,12 +120,12 @@ namespace Avalonia.Utilities var del = new Action(OnEvent); _delegate = del.GetMethodInfo().CreateDelegate(_info.EventHandlerType!, del.Target); - _info.AddMethod!.Invoke(target, new[] { _delegate }); + _info.AddMethod!.Invoke(target, new object?[] { _delegate }); } - void Destroy() + private void Destroy() { - _info.RemoveMethod!.Invoke(_target, new[] { _delegate }); + _info.RemoveMethod!.Invoke(_target, new object?[] { _delegate }); _sdic.Remove(_eventName); } @@ -146,8 +143,8 @@ namespace Avalonia.Utilities MethodInfo method = s.Method; var subscriber = (TSubscriber)s.Target!; - if (!s_Callers.TryGetValue(method, out var caller)) - s_Callers[method] = caller = + if (!s_callers.TryGetValue(method, out var caller)) + s_callers[method] = caller = (CallerDelegate)Delegate.CreateDelegate(typeof(CallerDelegate), null, method); _data[_count] = new Descriptor { @@ -178,7 +175,7 @@ namespace Avalonia.Utilities } } - void Compact(bool preventDestroy = false) + private void Compact(bool preventDestroy = false) { int empty = -1; for (int c = 0; c < _count; c++) @@ -206,15 +203,15 @@ namespace Avalonia.Utilities Destroy(); } - void OnEvent(object sender, T eventArgs) + private void OnEvent(object? sender, T eventArgs) { var needCompact = false; - for(var c=0; c<_count; c++) + for (var c = 0; c < _count; c++) { - var r = _data[c].Subscriber; + var r = _data[c].Subscriber!; if (r.TryGetTarget(out var sub)) { - _data[c].Caller(sub, sender, eventArgs); + _data[c].Caller!(sub, sender, eventArgs); } else needCompact = true; diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index e6d7492c51..87bb1d3790 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -348,7 +348,7 @@ namespace Avalonia /// public void InvalidateVisual() { - VisualRoot?.Renderer?.AddDirty(this); + VisualRoot?.Renderer.AddDirty(this); } /// @@ -449,7 +449,7 @@ namespace Avalonia protected override void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { base.LogicalChildrenCollectionChanged(sender, e); - VisualRoot?.Renderer?.RecalculateChildren(this); + VisualRoot?.Renderer.RecalculateChildren(this); } /// @@ -477,23 +477,19 @@ namespace Avalonia OnAttachedToVisualTree(e); AttachedToVisualTree?.Invoke(this, e); InvalidateVisual(); - _visualRoot.Renderer?.RecalculateChildren(_visualParent!); + _visualRoot.Renderer.RecalculateChildren(_visualParent!); if (ZIndex != 0 && VisualParent is Visual parent) parent.HasNonUniformZIndexChildren = true; var visualChildren = VisualChildren; + var visualChildrenCount = visualChildren.Count; - if (visualChildren != null) + for (var i = 0; i < visualChildrenCount; i++) { - var visualChildrenCount = visualChildren.Count; - - for (var i = 0; i < visualChildrenCount; i++) + if (visualChildren[i] is { } child) { - if (visualChildren[i] is Visual child) - { - child.OnAttachedToVisualTreeCore(e); - } + child.OnAttachedToVisualTreeCore(e); } } } @@ -540,20 +536,16 @@ namespace Avalonia } DetachedFromVisualTree?.Invoke(this, e); - e.Root?.Renderer?.AddDirty(this); + e.Root.Renderer.AddDirty(this); var visualChildren = VisualChildren; + var visualChildrenCount = visualChildren.Count; - if (visualChildren != null) + for (var i = 0; i < visualChildrenCount; i++) { - var visualChildrenCount = visualChildren.Count; - - for (var i = 0; i < visualChildrenCount; i++) + if (visualChildren[i] is { } child) { - if (visualChildren[i] is Visual child) - { - child.OnDetachedFromVisualTreeCore(e); - } + child.OnDetachedFromVisualTreeCore(e); } } } @@ -659,7 +651,7 @@ namespace Avalonia parentVisual.HasNonUniformZIndexChildren = true; sender?.InvalidateVisual(); - parent?.VisualRoot?.Renderer?.RecalculateChildren(parent); + parent?.VisualRoot?.Renderer.RecalculateChildren(parent); } /// diff --git a/src/Avalonia.Base/VisualTree/VisualExtensions.cs b/src/Avalonia.Base/VisualTree/VisualExtensions.cs index b58db3b276..9e38c6e7f2 100644 --- a/src/Avalonia.Base/VisualTree/VisualExtensions.cs +++ b/src/Avalonia.Base/VisualTree/VisualExtensions.cs @@ -46,7 +46,7 @@ namespace Avalonia.VisualTree Visual? v = visual ?? throw new ArgumentNullException(nameof(visual)); var result = 0; - v = v?.VisualParent; + v = v.VisualParent; while (v != null) { @@ -64,17 +64,13 @@ namespace Avalonia.VisualTree /// The first visual. /// The second visual. /// The common ancestor, or null if not found. - public static Visual? FindCommonVisualAncestor(this Visual visual, Visual target) + public static Visual? FindCommonVisualAncestor(this Visual? visual, Visual? target) { - Visual? v = visual ?? throw new ArgumentNullException(nameof(visual)); - - if (target is null) + if (visual is null || target is null) { return null; } - Visual? t = target; - void GoUpwards(ref Visual? node, int count) { for (int i = 0; i < count; ++i) @@ -83,6 +79,9 @@ namespace Avalonia.VisualTree } } + Visual? v = visual; + Visual? t = target; + // We want to find lowest node first, then make sure that both nodes are at the same height. // By doing that we can sometimes find out that other node is our lowest common ancestor. var firstHeight = CalculateDistanceFromRoot(v); @@ -144,7 +143,7 @@ namespace Avalonia.VisualTree /// The visual. /// If given visual should be included in search. /// First ancestor of given type. - public static T? FindAncestorOfType(this Visual visual, bool includeSelf = false) where T : class + public static T? FindAncestorOfType(this Visual? visual, bool includeSelf = false) where T : class { if (visual is null) { @@ -173,7 +172,7 @@ namespace Avalonia.VisualTree /// The visual. /// If given visual should be included in search. /// First descendant of given type. - public static T? FindDescendantOfType(this Visual visual, bool includeSelf = false) where T : class + public static T? FindDescendantOfType(this Visual? visual, bool includeSelf = false) where T : class { if (visual is null) { @@ -392,7 +391,7 @@ namespace Avalonia.VisualTree /// True if is an ancestor of ; /// otherwise false. /// - public static bool IsVisualAncestorOf(this Visual visual, Visual target) + public static bool IsVisualAncestorOf(this Visual? visual, Visual? target) { Visual? current = target?.VisualParent; diff --git a/src/Avalonia.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.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index f35124ee0a..91b65a1f72 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -3979,7 +3979,7 @@ namespace Avalonia.Controls { if (focusedObject is Control element) { - parent = element.Parent; + parent = element.VisualParent; if (parent != null) { dataGridWillReceiveRoutedEvent = false; diff --git a/src/Avalonia.Controls.DataGrid/Utils/TreeHelper.cs b/src/Avalonia.Controls.DataGrid/Utils/TreeHelper.cs index f4ba644ae6..6aebf05d6b 100644 --- a/src/Avalonia.Controls.DataGrid/Utils/TreeHelper.cs +++ b/src/Avalonia.Controls.DataGrid/Utils/TreeHelper.cs @@ -36,7 +36,7 @@ namespace Avalonia.Controls.Utils { if (child is Control childElement) { - parent = childElement.Parent; + parent = childElement.VisualParent; } } child = parent; diff --git a/src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj b/src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj new file mode 100644 index 0000000000..1ec0ee33a7 --- /dev/null +++ b/src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj @@ -0,0 +1,20 @@ + + + net6.0;netstandard2.0 + Avalonia.Controls.ItemsRepeater + Avalonia + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls/Repeater/ElementFactory.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ElementFactory.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/ElementFactory.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ElementFactory.cs diff --git a/src/Avalonia.Controls/Repeater/IElementFactory.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/IElementFactory.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/IElementFactory.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/IElementFactory.cs diff --git a/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemTemplateWrapper.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ItemTemplateWrapper.cs diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/ItemsRepeater.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementClearingEventArgs.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementClearingEventArgs.cs diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementIndexChangedEventArgs.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementIndexChangedEventArgs.cs diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementPreparedEventArgs.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementPreparedEventArgs.cs diff --git a/src/Avalonia.Controls/Repeater/RecyclePool.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/RecyclePool.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/RecyclePool.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/RecyclePool.cs diff --git a/src/Avalonia.Controls/Repeater/RecyclingElementFactory.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/RecyclingElementFactory.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/RecyclingElementFactory.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/RecyclingElementFactory.cs diff --git a/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/RepeaterLayoutContext.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/RepeaterLayoutContext.cs diff --git a/src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/UniqueIdElementPool.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/UniqueIdElementPool.cs diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ViewManager.cs similarity index 99% rename from src/Avalonia.Controls/Repeater/ViewManager.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ViewManager.cs index 2dff18cd04..2d302a95dd 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls.ItemsRepeater/Controls/ViewManager.cs @@ -256,7 +256,7 @@ namespace Avalonia.Controls public void UpdatePin(Control element, bool addPin) { - var parent = element.VisualParent; + var parent = element.GetVisualParent(); var child = (Visual)element; while (parent != null) @@ -283,7 +283,7 @@ namespace Avalonia.Controls } child = parent; - parent = child.VisualParent; + parent = child.GetVisualParent(); } } @@ -656,7 +656,7 @@ namespace Avalonia.Controls // that handlers can walk up the tree in case they want to find their IndexPath in the // nested case. var children = repeater.Children; - if (element.VisualParent != repeater) + if (element.GetVisualParent() != repeater) { children.Add(element); } @@ -701,7 +701,7 @@ namespace Avalonia.Controls if (FocusManager.Instance?.Current is Visual child) { - var parent = child.VisualParent; + var parent = child.GetVisualParent(); var owner = _owner; // Find out if the focused element belongs to one of our direct @@ -722,7 +722,7 @@ namespace Avalonia.Controls } child = parent; - parent = child?.VisualParent; + parent = child?.GetVisualParent(); } } diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ViewportManager.cs similarity index 98% rename from src/Avalonia.Controls/Repeater/ViewportManager.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ViewportManager.cs index 56e0cda8fe..336fb2d228 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls.ItemsRepeater/Controls/ViewportManager.cs @@ -67,7 +67,7 @@ namespace Avalonia.Controls // be a direct child of ours, or even an indirect child. We need to walk up the tree starting // from anchorElement to figure out what child of ours (if any) to use as the suggested element. var child = anchorElement; - var parent = child.VisualParent as Control; + var parent = child.GetVisualParent() as Control; while (parent != null) { @@ -78,7 +78,7 @@ namespace Avalonia.Controls } child = parent; - parent = parent.VisualParent as Control; + parent = parent.GetVisualParent() as Control; } } } @@ -369,11 +369,11 @@ namespace Avalonia.Controls private Control? GetImmediateChildOfRepeater(Control descendant) { var targetChild = descendant; - var parent = (Control?)descendant.VisualParent; + var parent = (Control?)descendant.GetVisualParent(); while (parent != null && parent != _owner) { targetChild = parent; - parent = (Control?)parent.VisualParent; + parent = (Control?)parent.GetVisualParent(); } if (parent == null) @@ -471,7 +471,7 @@ namespace Avalonia.Controls break; } - parent = parent.VisualParent; + parent = parent.GetVisualParent(); } if (!_managingViewportDisabled) diff --git a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/VirtualizationInfo.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/VirtualizationInfo.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/VirtualizationInfo.cs diff --git a/src/Avalonia.Base/Layout/AttachedLayout.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/AttachedLayout.cs similarity index 100% rename from src/Avalonia.Base/Layout/AttachedLayout.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/AttachedLayout.cs diff --git a/src/Avalonia.Base/Layout/ElementManager.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/ElementManager.cs similarity index 100% rename from src/Avalonia.Base/Layout/ElementManager.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/ElementManager.cs diff --git a/src/Avalonia.Base/Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/FlowLayoutAlgorithm.cs similarity index 100% rename from src/Avalonia.Base/Layout/FlowLayoutAlgorithm.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/FlowLayoutAlgorithm.cs diff --git a/src/Avalonia.Base/Layout/IFlowLayoutAlgorithmDelegates.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/IFlowLayoutAlgorithmDelegates.cs similarity index 100% rename from src/Avalonia.Base/Layout/IFlowLayoutAlgorithmDelegates.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/IFlowLayoutAlgorithmDelegates.cs diff --git a/src/Avalonia.Base/Layout/LayoutContext.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/LayoutContext.cs similarity index 100% rename from src/Avalonia.Base/Layout/LayoutContext.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/LayoutContext.cs diff --git a/src/Avalonia.Base/Layout/LayoutContextAdapter.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/LayoutContextAdapter.cs similarity index 100% rename from src/Avalonia.Base/Layout/LayoutContextAdapter.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/LayoutContextAdapter.cs diff --git a/src/Avalonia.Base/Layout/NonVirtualizingLayout.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingLayout.cs similarity index 100% rename from src/Avalonia.Base/Layout/NonVirtualizingLayout.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingLayout.cs diff --git a/src/Avalonia.Base/Layout/NonVirtualizingLayoutContext.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingLayoutContext.cs similarity index 100% rename from src/Avalonia.Base/Layout/NonVirtualizingLayoutContext.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingLayoutContext.cs diff --git a/src/Avalonia.Base/Layout/NonVirtualizingStackLayout.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingStackLayout.cs similarity index 100% rename from src/Avalonia.Base/Layout/NonVirtualizingStackLayout.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingStackLayout.cs diff --git a/src/Avalonia.Base/Layout/OrientationBasedMeasures.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/OrientationBasedMeasures.cs similarity index 100% rename from src/Avalonia.Base/Layout/OrientationBasedMeasures.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/OrientationBasedMeasures.cs diff --git a/src/Avalonia.Base/Layout/StackLayout.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/StackLayout.cs similarity index 98% rename from src/Avalonia.Base/Layout/StackLayout.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/StackLayout.cs index e9093cc146..5e2b2b8574 100644 --- a/src/Avalonia.Base/Layout/StackLayout.cs +++ b/src/Avalonia.Controls.ItemsRepeater/Layout/StackLayout.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Specialized; +using Avalonia.Controls; using Avalonia.Data; using Avalonia.Logging; @@ -25,13 +26,13 @@ namespace Avalonia.Layout /// Defines the property. /// public static readonly StyledProperty OrientationProperty = - AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical); + StackPanel.OrientationProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty SpacingProperty = - AvaloniaProperty.Register(nameof(Spacing)); + StackPanel.SpacingProperty.AddOwner(); private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures(); diff --git a/src/Avalonia.Base/Layout/StackLayoutState.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/StackLayoutState.cs similarity index 100% rename from src/Avalonia.Base/Layout/StackLayoutState.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/StackLayoutState.cs diff --git a/src/Avalonia.Base/Layout/UniformGridLayout.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/UniformGridLayout.cs similarity index 100% rename from src/Avalonia.Base/Layout/UniformGridLayout.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/UniformGridLayout.cs diff --git a/src/Avalonia.Base/Layout/UniformGridLayoutState.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/UniformGridLayoutState.cs similarity index 100% rename from src/Avalonia.Base/Layout/UniformGridLayoutState.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/UniformGridLayoutState.cs diff --git a/src/Avalonia.Base/Layout/Utils/ListUtils.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/Utils/ListUtils.cs similarity index 100% rename from src/Avalonia.Base/Layout/Utils/ListUtils.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/Utils/ListUtils.cs diff --git a/src/Avalonia.Base/Layout/WrapLayout/UvBounds.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/UvBounds.cs similarity index 100% rename from src/Avalonia.Base/Layout/WrapLayout/UvBounds.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/UvBounds.cs diff --git a/src/Avalonia.Base/Layout/WrapLayout/UvMeasure.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/UvMeasure.cs similarity index 100% rename from src/Avalonia.Base/Layout/WrapLayout/UvMeasure.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/UvMeasure.cs diff --git a/src/Avalonia.Base/Layout/VirtualLayoutContextAdapter.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/VirtualLayoutContextAdapter.cs similarity index 100% rename from src/Avalonia.Base/Layout/VirtualLayoutContextAdapter.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/VirtualLayoutContextAdapter.cs diff --git a/src/Avalonia.Base/Layout/VirtualizingLayout.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/VirtualizingLayout.cs similarity index 100% rename from src/Avalonia.Base/Layout/VirtualizingLayout.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/VirtualizingLayout.cs diff --git a/src/Avalonia.Base/Layout/VirtualizingLayoutContext.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/VirtualizingLayoutContext.cs similarity index 100% rename from src/Avalonia.Base/Layout/VirtualizingLayoutContext.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/VirtualizingLayoutContext.cs diff --git a/src/Avalonia.Base/Layout/WrapLayout/WrapItem.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/WrapItem.cs similarity index 100% rename from src/Avalonia.Base/Layout/WrapLayout/WrapItem.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/WrapItem.cs diff --git a/src/Avalonia.Base/Layout/WrapLayout/WrapLayout.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/WrapLayout.cs similarity index 100% rename from src/Avalonia.Base/Layout/WrapLayout/WrapLayout.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/WrapLayout.cs diff --git a/src/Avalonia.Base/Layout/WrapLayout/WrapLayoutState.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/WrapLayoutState.cs similarity index 100% rename from src/Avalonia.Base/Layout/WrapLayout/WrapLayoutState.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/WrapLayoutState.cs diff --git a/src/Avalonia.Controls.ItemsRepeater/Properties/AssemblyInfo.cs b/src/Avalonia.Controls.ItemsRepeater/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..d8023b0853 --- /dev/null +++ b/src/Avalonia.Controls.ItemsRepeater/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using Avalonia.Metadata; + +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Layout")] diff --git a/src/Avalonia.Controls/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/AutoCompleteBox/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs index 98885e11ca..55649660f7 100644 --- a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs @@ -792,7 +792,7 @@ namespace Avalonia.Controls Control? element = focused as Control; if (element != null) { - parent = element.Parent; + parent = element.VisualParent; } } focused = parent; diff --git a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs index 85f139a6a3..aea91b5e26 100644 --- a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs @@ -1,5 +1,4 @@ -using System; -using Avalonia.Automation.Provider; +using Avalonia.Automation.Provider; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Controls.Selection; @@ -64,7 +63,7 @@ namespace Avalonia.Automation.Peers if (Owner.Parent is ItemsControl parent && parent.GetValue(ListBox.SelectionProperty) is ISelectionModel selectionModel) { - var index = parent.ItemContainerGenerator.IndexFromContainer(Owner); + var index = parent.IndexFromContainer(Owner); if (index != -1) selectionModel.Deselect(index); diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 42c577041a..3195c38eef 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 9627f200df..1ec6f8dabc 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -394,10 +394,10 @@ namespace Avalonia.Controls if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { IsPressed = true; + e.Handled = true; if (ClickMode == ClickMode.Press) { - e.Handled = true; OnClick(); } } @@ -411,11 +411,11 @@ namespace Avalonia.Controls if (IsPressed && e.InitialPressMouseButton == MouseButton.Left) { IsPressed = false; + e.Handled = true; if (ClickMode == ClickMode.Release && this.GetVisualsAt(e.GetPosition(this)).Any(c => this == c || this.IsVisualAncestorOf(c))) { - e.Handled = true; OnClick(); } } diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index ed24c3c7c2..ab7c9948c4 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -2,14 +2,12 @@ using System; using System.Collections.Generic; using System.ComponentModel; using Avalonia.Automation.Peers; -using Avalonia.Controls.Documents; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.LogicalTree; -using Avalonia.Media; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Threading; @@ -211,8 +209,6 @@ namespace Avalonia.Controls remove => RemoveHandler(SizeChangedEvent, value); } - public new Control? Parent => (Control?)base.Parent; - /// bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; diff --git a/src/Avalonia.Controls/Documents/InlineUIContainer.cs b/src/Avalonia.Controls/Documents/InlineUIContainer.cs index 58afb24b5c..f06c8515ee 100644 --- a/src/Avalonia.Controls/Documents/InlineUIContainer.cs +++ b/src/Avalonia.Controls/Documents/InlineUIContainer.cs @@ -64,5 +64,23 @@ namespace Avalonia.Controls.Documents internal override void AppendText(StringBuilder stringBuilder) { } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ChildProperty) + { + if(change.OldValue is Control oldChild) + { + LogicalChildren.Remove(oldChild); + } + + if(change.NewValue is Control newChild) + { + LogicalChildren.Add(newChild); + } + } + } } } diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 8b1a307182..80d1677c2f 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -104,6 +104,7 @@ namespace Avalonia.Controls public void UnselectAll() => Selection.Clear(); protected internal override Control CreateContainerForItemOverride() => new ListBoxItem(); + protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is ListBoxItem; /// protected override void OnGotFocus(GotFocusEventArgs e) diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index de3aca76d9..4dd868253e 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -553,7 +553,7 @@ namespace Avalonia.Controls.Platform } } - protected static IMenuItem? GetMenuItem(Control? item) + protected static IMenuItem? GetMenuItem(StyledElement? item) { while (true) { diff --git a/src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs b/src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs index 5a34c5c0e1..f271abb59a 100644 --- a/src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs +++ b/src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs @@ -41,7 +41,7 @@ namespace Avalonia.Platform /// The fallback module will only be initialized if the Skia-specific module is not applicable. /// [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public class ExportAvaloniaModuleAttribute : Attribute + public sealed class ExportAvaloniaModuleAttribute : Attribute { public ExportAvaloniaModuleAttribute(string name, Type moduleType) { diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 8594b584fa..e8eaac7d17 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -28,19 +28,19 @@ namespace Avalonia.Controls.Presenters /// Defines the property. /// public static readonly StyledProperty AreHorizontalSnapPointsRegularProperty = - AvaloniaProperty.Register(nameof(AreHorizontalSnapPointsRegular)); + AvaloniaProperty.Register(nameof(AreHorizontalSnapPointsRegular)); /// /// Defines the property. /// public static readonly StyledProperty AreVerticalSnapPointsRegularProperty = - AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular)); + AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular)); /// /// Defines the event. /// public static readonly RoutedEvent HorizontalSnapPointsChangedEvent = - RoutedEvent.Register( + RoutedEvent.Register( nameof(HorizontalSnapPointsChanged), RoutingStrategies.Bubble); @@ -48,7 +48,7 @@ namespace Avalonia.Controls.Presenters /// Defines the event. /// public static readonly RoutedEvent VerticalSnapPointsChangedEvent = - RoutedEvent.Register( + RoutedEvent.Register( nameof(VerticalSnapPointsChanged), RoutingStrategies.Bubble); @@ -139,7 +139,7 @@ namespace Avalonia.Controls.Presenters Size IScrollable.Viewport => _logicalScrollable?.Viewport ?? default; /// - /// Gets or sets whether the horizontal snap points for the are equidistant from each other. + /// Gets or sets whether the horizontal snap points for the are equidistant from each other. /// public bool AreHorizontalSnapPointsRegular { @@ -148,7 +148,7 @@ namespace Avalonia.Controls.Presenters } /// - /// Gets or sets whether the vertical snap points for the are equidistant from each other. + /// Gets or sets whether the vertical snap points for the are equidistant from each other. /// public bool AreVerticalSnapPointsRegular { diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 762702efcc..454f7eac9d 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -15,7 +15,6 @@ namespace Avalonia.Controls.Presenters public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider { private const double EdgeDetectionTolerance = 0.1; - private const int ProximityPoints = 10; /// /// Defines the property. diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index e265f4eb6a..d466edeb33 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -48,7 +48,7 @@ namespace Avalonia.Controls.Primitives set { /* Not currently supported in overlay popups */ } } - protected internal override Interactive? InteractiveParent => Parent; + protected internal override Interactive? InteractiveParent => (Interactive?)VisualParent; public void Dispose() => Hide(); diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index c85199a665..3b68cd2ae8 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -723,7 +723,7 @@ namespace Avalonia.Controls.Primitives while (e is object && (!e.Focusable || !e.IsEffectivelyEnabled || !e.IsVisible)) { - e = e.Parent; + e = e.VisualParent as Control; } if (e is object) diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 57ec864cad..b3436d4176 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -72,12 +72,12 @@ namespace Avalonia.Controls.Primitives /// /// Popup events are passed to their parent window. This facilitates this. /// - protected internal override Interactive? InteractiveParent => Parent; + protected internal override Interactive? InteractiveParent => (Interactive?)Parent; /// /// Gets the control that is hosting the popup root. /// - Visual? IHostedVisualTreeRoot.Host => Parent; + Visual? IHostedVisualTreeRoot.Host => VisualParent; /// /// Gets the styling parent of the popup root. diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 7e0d695264..ce3158b282 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -231,7 +231,7 @@ namespace Avalonia.Controls private void UpdateIndicator() { // Gets the size of the parent indicator container - var barSize = _indicator?.Parent?.Bounds.Size ?? Bounds.Size; + var barSize = _indicator?.VisualParent?.Bounds.Size ?? Bounds.Size; if (_indicator != null) { diff --git a/src/Avalonia.Controls/ResolveByNameAttribute.cs b/src/Avalonia.Controls/ResolveByNameAttribute.cs index a13b10d630..3c56c20db0 100644 --- a/src/Avalonia.Controls/ResolveByNameAttribute.cs +++ b/src/Avalonia.Controls/ResolveByNameAttribute.cs @@ -7,7 +7,8 @@ namespace Avalonia.Controls /// When applying this to attached properties, ensure to put on both /// the Getter and Setter methods. /// - public class ResolveByNameAttribute : Attribute + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] + public sealed class ResolveByNameAttribute : Attribute { } } diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 1c23919d0e..ab114da933 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -154,15 +154,15 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty HorizontalSnapPointsTypeProperty = - AvaloniaProperty.Register( + public static readonly AttachedProperty HorizontalSnapPointsTypeProperty = + AvaloniaProperty.RegisterAttached( nameof(HorizontalSnapPointsType)); /// /// Defines the property. /// - public static readonly StyledProperty VerticalSnapPointsTypeProperty = - AvaloniaProperty.Register( + public static readonly AttachedProperty VerticalSnapPointsTypeProperty = + AvaloniaProperty.RegisterAttached( nameof(VerticalSnapPointsType)); /// @@ -625,6 +625,86 @@ namespace Avalonia.Controls control.SetValue(HorizontalScrollBarVisibilityProperty, value); } + /// + /// Gets the value of the HorizontalSnapPointsType attached property. + /// + /// The control to read the value from. + /// The value of the property. + public static SnapPointsType GetHorizontalSnapPointsType(Control control) + { + return control.GetValue(HorizontalSnapPointsTypeProperty); + } + + /// + /// Gets the value of the HorizontalSnapPointsType attached property. + /// + /// The control to set the value on. + /// The value of the property. + public static void SetHorizontalSnapPointsType(Control control, SnapPointsType value) + { + control.SetValue(HorizontalSnapPointsTypeProperty, value); + } + + /// + /// Gets the value of the VerticalSnapPointsType attached property. + /// + /// The control to read the value from. + /// The value of the property. + public static SnapPointsType GetVerticalSnapPointsType(Control control) + { + return control.GetValue(VerticalSnapPointsTypeProperty); + } + + /// + /// Gets the value of the VerticalSnapPointsType attached property. + /// + /// The control to set the value on. + /// The value of the property. + public static void SetVerticalSnapPointsType(Control control, SnapPointsType value) + { + control.SetValue(VerticalSnapPointsTypeProperty, value); + } + + /// + /// Gets the value of the HorizontalSnapPointsAlignment attached property. + /// + /// The control to read the value from. + /// The value of the property. + public static SnapPointsAlignment GetHorizontalSnapPointsAlignment(Control control) + { + return control.GetValue(HorizontalSnapPointsAlignmentProperty); + } + + /// + /// Gets the value of the HorizontalSnapPointsAlignment attached property. + /// + /// The control to set the value on. + /// The value of the property. + public static void SetHorizontalSnapPointsAlignment(Control control, SnapPointsAlignment value) + { + control.SetValue(HorizontalSnapPointsAlignmentProperty, value); + } + + /// + /// Gets the value of the VerticalSnapPointsAlignment attached property. + /// + /// The control to read the value from. + /// The value of the property. + public static SnapPointsAlignment GetVerticalSnapPointsAlignment(Control control) + { + return control.GetValue(VerticalSnapPointsAlignmentProperty); + } + + /// + /// Gets the value of the VerticalSnapPointsAlignment attached property. + /// + /// The control to set the value on. + /// The value of the property. + public static void SetVerticalSnapPointsAlignment(Control control, SnapPointsAlignment value) + { + control.SetValue(VerticalSnapPointsAlignmentProperty, value); + } + /// /// Gets the value of the VerticalScrollBarVisibility attached property. /// diff --git a/src/Avalonia.Controls/SelectableTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs index f4c4d54951..f8ce5d23f6 100644 --- a/src/Avalonia.Controls/SelectableTextBlock.cs +++ b/src/Avalonia.Controls/SelectableTextBlock.cs @@ -229,7 +229,11 @@ namespace Avalonia.Controls if (Match(keymap.Copy)) { Copy(); - + handled = true; + } + else if (Match(keymap.SelectAll)) + { + SelectAll(); handled = true; } diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index aa63ac975e..9362dab553 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -22,13 +22,13 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty SpacingProperty = - StackLayout.SpacingProperty.AddOwner(); + AvaloniaProperty.Register(nameof(Spacing)); /// /// Defines the property. /// public static readonly StyledProperty OrientationProperty = - StackLayout.OrientationProperty.AddOwner(); + AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical); /// /// Defines the property. diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 9bd1dc95f9..ec31470126 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -673,8 +673,6 @@ namespace Avalonia.Controls controlRun.Control is Control control) { VisualChildren.Remove(control); - - LogicalChildren.Remove(control); } } } @@ -693,8 +691,6 @@ namespace Avalonia.Controls { VisualChildren.Add(control); - LogicalChildren.Add(control); - control.Measure(Size.Infinity); } } @@ -720,6 +716,16 @@ namespace Avalonia.Controls var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); + if (HasComplexContent) + { + ArrangeComplexContent(TextLayout, padding); + } + + if (MathUtilities.AreClose(_constraint.Inflate(padding).Width, finalSize.Width)) + { + return finalSize; + } + _constraint = new Size(Math.Ceiling(finalSize.Deflate(padding).Width), double.PositiveInfinity); _textLayout?.Dispose(); @@ -727,31 +733,36 @@ namespace Avalonia.Controls if (HasComplexContent) { - var currentY = padding.Top; + ArrangeComplexContent(TextLayout, padding); + } - foreach (var textLine in TextLayout.TextLines) - { - var currentX = padding.Left + textLine.Start; + return finalSize; + } + + private static void ArrangeComplexContent(TextLayout textLayout, Thickness padding) + { + var currentY = padding.Top; - foreach (var run in textLine.TextRuns) + foreach (var textLine in textLayout.TextLines) + { + var currentX = padding.Left + textLine.Start; + + foreach (var run in textLine.TextRuns) + { + if (run is DrawableTextRun drawable) { - if (run is DrawableTextRun drawable) + if (drawable is EmbeddedControlRun controlRun + && controlRun.Control is Control control) { - if (drawable is EmbeddedControlRun controlRun - && controlRun.Control is Control control) - { - control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize)); - } - - currentX += drawable.Size.Width; + control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize)); } - } - currentY += textLine.Height; + currentX += drawable.Size.Width; + } } - } - return finalSize; + currentY += textLine.Height; + } } protected override AutomationPeer OnCreateAutomationPeer() @@ -892,7 +903,7 @@ namespace Avalonia.Controls return textRun; } - return null; + return new TextEndOfParagraph(); } } } 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..4db71abfa8 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; @@ -94,8 +94,8 @@ namespace Avalonia.Controls private readonly IInputManager? _inputManager; private readonly IAccessKeyHandler? _accessKeyHandler; private readonly IKeyboardNavigationHandler? _keyboardNavigationHandler; - private readonly IPlatformRenderInterface? _renderInterface; private readonly IGlobalStyles? _globalStyles; + private readonly IGlobalThemeVariantProvider? _applicationThemeHost; private readonly PointerOverPreProcessor? _pointerOverPreProcessor; private readonly IDisposable? _pointerOverPreProcessorSubscription; private readonly IDisposable? _backGestureSubscription; @@ -106,6 +106,7 @@ namespace Avalonia.Controls private Border? _transparencyFallbackBorder; private TargetWeakEventSubscriber? _resourcesChangesSubscriber; private IStorageProvider? _storageProvider; + private LayoutDiagnosticBridge? _layoutDiagnosticBridge; /// /// Initializes static members of the class. @@ -114,16 +115,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); - } - }); } /// @@ -144,35 +135,21 @@ namespace Avalonia.Controls /// public TopLevel(ITopLevelImpl impl, IAvaloniaDependencyResolver? dependencyResolver) { - if (impl == null) - { - throw new InvalidOperationException( - "Could not create window implementation: maybe no windowing subsystem was initialized?"); - } - - PlatformImpl = impl; + PlatformImpl = impl ?? throw new InvalidOperationException( + "Could not create window implementation: maybe no windowing subsystem was initialized?"); _actualTransparencyLevel = PlatformImpl.TransparencyLevel; - dependencyResolver = dependencyResolver ?? AvaloniaLocator.Current; + dependencyResolver ??= AvaloniaLocator.Current; _accessKeyHandler = TryGetService(dependencyResolver); _inputManager = TryGetService(dependencyResolver); _keyboardNavigationHandler = TryGetService(dependencyResolver); - _renderInterface = TryGetService(dependencyResolver); _globalStyles = TryGetService(dependencyResolver); + _applicationThemeHost = TryGetService(dependencyResolver); Renderer = impl.CreateRenderer(this); - - if (Renderer != null) - { - Renderer.SceneInvalidated += SceneInvalidated; - } - else - { - // Prevent nullable error. - Renderer = null!; - } + Renderer.SceneInvalidated += SceneInvalidated; impl.SetInputRoot(this); @@ -191,10 +168,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()) @@ -218,7 +200,7 @@ namespace Avalonia.Controls if(impl.TryGetFeature() is {} systemNavigationManager) { - systemNavigationManager.BackRequested += (s, e) => + systemNavigationManager.BackRequested += (_, e) => { e.RoutedEvent = BackRequestedEvent; RaiseEvent(e); @@ -315,6 +297,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 +317,17 @@ namespace Avalonia.Controls { get { - if (_layoutManager == null) + if (_layoutManager is null) + { _layoutManager = CreateLayoutManager(); + + if (_layoutManager is LayoutManager typedLayoutManager) + { + _layoutDiagnosticBridge = new LayoutDiagnosticBridge(Renderer.Diagnostics, typedLayoutManager); + _layoutDiagnosticBridge.SetupBridge(); + } + } + return _layoutManager; } } @@ -342,7 +340,7 @@ namespace Avalonia.Controls /// /// Gets the renderer for the window. /// - public IRenderer Renderer { get; private set; } + public IRenderer Renderer { get; } internal PixelPoint? LastPointerPosition => _pointerOverPreProcessor?.LastPosition; @@ -407,6 +405,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 . /// @@ -418,7 +434,7 @@ namespace Avalonia.Controls /// The dirty area. protected virtual void HandlePaint(Rect rect) { - Renderer?.Paint(rect); + Renderer.Paint(rect); } /// @@ -431,9 +447,16 @@ namespace Avalonia.Controls _globalStyles.GlobalStylesAdded -= ((IStyleHost)this).StylesAdded; _globalStyles.GlobalStylesRemoved -= ((IStyleHost)this).StylesRemoved; } + if (_applicationThemeHost is { }) + { + _applicationThemeHost.ActualThemeVariantChanged -= GlobalActualThemeVariantChanged; + } - Renderer?.Dispose(); - Renderer = null!; + Renderer.SceneInvalidated -= SceneInvalidated; + Renderer.Dispose(); + + _layoutDiagnosticBridge?.Dispose(); + _layoutDiagnosticBridge = null; _pointerOverPreProcessor?.OnCompleted(); _pointerOverPreProcessorSubscription?.Dispose(); @@ -449,7 +472,7 @@ namespace Avalonia.Controls OnClosed(EventArgs.Empty); - LayoutManager?.Dispose(); + LayoutManager.Dispose(); } /// @@ -464,7 +487,7 @@ namespace Avalonia.Controls Width = clientSize.Width; Height = clientSize.Height; LayoutManager.ExecuteLayoutPass(); - Renderer?.Resized(clientSize); + Renderer.Resized(clientSize); } /// @@ -592,6 +615,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 +645,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.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 022e1a74b1..9f8e3e38c0 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; -using Avalonia.Controls.Generators; using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Threading; @@ -22,11 +22,10 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty IsExpandedProperty = - AvaloniaProperty.RegisterDirect( + public static readonly StyledProperty IsExpandedProperty = + AvaloniaProperty.Register( nameof(IsExpanded), - o => o.IsExpanded, - (o, v) => o.IsExpanded = v); + defaultBindingMode: BindingMode.TwoWay); /// /// Defines the property. @@ -46,7 +45,6 @@ namespace Avalonia.Controls private TreeView? _treeView; private Control? _header; - private bool _isExpanded; private int _level; private bool _templateApplied; private bool _deferredBringIntoViewFlag; @@ -68,8 +66,8 @@ namespace Avalonia.Controls /// public bool IsExpanded { - get { return _isExpanded; } - set { SetAndRaise(IsExpandedProperty, ref _isExpanded, value); } + get => GetValue(IsExpandedProperty); + set => SetValue(IsExpandedProperty, value); } /// @@ -77,8 +75,8 @@ namespace Avalonia.Controls /// public bool IsSelected { - get { return GetValue(IsSelectedProperty); } - set { SetValue(IsSelectedProperty, value); } + get => GetValue(IsSelectedProperty); + set => SetValue(IsSelectedProperty, value); } /// @@ -86,8 +84,8 @@ namespace Avalonia.Controls /// public int Level { - get { return _level; } - private set { SetAndRaise(LevelProperty, ref _level, value); } + get => _level; + private set => SetAndRaise(LevelProperty, ref _level, value); } internal TreeView? TreeViewOwner => _treeView; @@ -115,11 +113,6 @@ namespace Avalonia.Controls } } - protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) - { - base.OnDetachedFromLogicalTree(e); - } - protected virtual void OnRequestBringIntoView(RequestBringIntoViewEventArgs e) { if (e.TargetObject == this) @@ -264,9 +257,9 @@ namespace Avalonia.Controls Dispatcher.UIThread.Post(this.BringIntoView); // must use the Dispatcher, otherwise the TreeView doesn't scroll } } - + /// - /// Invoked when the event occurs in the header. + /// Invoked when the event occurs in the header. /// protected virtual void OnHeaderDoubleTapped(TappedEventArgs e) { diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index c5276741b6..634efbd699 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty OrientationProperty = - StackLayout.OrientationProperty.AddOwner(); + StackPanel.OrientationProperty.AddOwner(); /// /// Defines the property. diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index a20b4eee58..ba1b599421 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -450,7 +450,7 @@ namespace Avalonia.Controls /// resulting task will produce the value when the window /// is closed. /// - public void Close(object dialogResult) + public void Close(object? dialogResult) { _dialogResult = dialogResult; CloseCore(WindowCloseReason.WindowClosing, true); @@ -573,7 +573,7 @@ namespace Avalonia.Controls return; } - Renderer?.Stop(); + Renderer.Stop(); if (Owner is Window owner) { @@ -721,7 +721,7 @@ namespace Avalonia.Controls SetWindowStartupLocation(owner?.PlatformImpl); PlatformImpl?.Show(ShowActivated, false); - Renderer?.Start(); + Renderer.Start(); OnOpened(EventArgs.Empty); } } @@ -798,7 +798,7 @@ namespace Avalonia.Controls PlatformImpl?.Show(ShowActivated, true); - Renderer?.Start(); + Renderer.Start(); Observable.FromEventPattern( x => Closed += x, diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index aad0482b50..0c9a91148b 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -129,7 +129,7 @@ namespace Avalonia.Controls { using (FreezeVisibilityChangeHandling()) { - Renderer?.Stop(); + Renderer.Stop(); PlatformImpl?.Hide(); IsVisible = false; } @@ -153,7 +153,7 @@ namespace Avalonia.Controls } PlatformImpl?.Show(true, false); - Renderer?.Start(); + Renderer.Start(); OnOpened(EventArgs.Empty); } } @@ -219,7 +219,7 @@ namespace Avalonia.Controls { ClientSize = clientSize; LayoutManager.ExecuteLayoutPass(); - Renderer?.Resized(clientSize); + Renderer.Resized(clientSize); } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs index 00173dbb35..a0ff3a714f 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; @@ -30,12 +33,15 @@ namespace Avalonia.Diagnostics.Controls RendererRoot = application.ApplicationLifetime switch { Lifetimes.IClassicDesktopStyleApplicationLifetime classic => classic.MainWindow?.Renderer, - Lifetimes.ISingleViewApplicationLifetime single => (single.MainView as Visual)?.VisualRoot?.Renderer, + Lifetimes.ISingleViewApplicationLifetime single => single.MainView?.VisualRoot?.Renderer, _ => null }; + + 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.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 225e846390..68466fe381 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -141,9 +141,7 @@ namespace Avalonia.Headless } public IReadOnlyList GetIntersections(float lowerBound, float upperBound) - { - return null; - } + => Array.Empty(); } class HeadlessGeometryStub : IGeometryImpl diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index f27d94b61a..64c1d0da10 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -119,7 +119,8 @@ namespace Avalonia.Native { if(e.Type == RawPointerEventType.LeftButtonDown) { - var visual = (_inputRoot as Window).Renderer.HitTestFirst(e.Position, _inputRoot as Window, x => + var window = _inputRoot as Window; + var visual = window?.Renderer.HitTestFirst(e.Position, window, x => { if (x is IInputElement ie && (!ie.IsHitTestVisible || !ie.IsEffectivelyVisible)) { diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 1f290acd86..50bee0d395 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -501,7 +501,7 @@ namespace Avalonia.Native } } - public WindowTransparencyLevel TransparencyLevel { get; private set; } = WindowTransparencyLevel.Transparent; + public WindowTransparencyLevel TransparencyLevel { get; private set; } = WindowTransparencyLevel.None; public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { diff --git a/src/Avalonia.OpenGL/GlEntryPointAttribute.cs b/src/Avalonia.OpenGL/GlEntryPointAttribute.cs index 3e31de6995..386db30f92 100644 --- a/src/Avalonia.OpenGL/GlEntryPointAttribute.cs +++ b/src/Avalonia.OpenGL/GlEntryPointAttribute.cs @@ -3,7 +3,7 @@ using System; namespace Avalonia.OpenGL { [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - class GlMinVersionEntryPoint : Attribute + sealed class GlMinVersionEntryPoint : Attribute { public GlMinVersionEntryPoint(string entry, int minVersionMajor, int minVersionMinor) { @@ -28,7 +28,7 @@ namespace Avalonia.OpenGL } [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - class GlExtensionEntryPoint : Attribute + sealed class GlExtensionEntryPoint : Attribute { public GlExtensionEntryPoint(string entry, string extension) { diff --git a/src/Avalonia.Remote.Protocol/AvaloniaRemoteMessageGuidAttribute.cs b/src/Avalonia.Remote.Protocol/AvaloniaRemoteMessageGuidAttribute.cs index 98a843bad1..44605a2ffb 100644 --- a/src/Avalonia.Remote.Protocol/AvaloniaRemoteMessageGuidAttribute.cs +++ b/src/Avalonia.Remote.Protocol/AvaloniaRemoteMessageGuidAttribute.cs @@ -3,7 +3,7 @@ namespace Avalonia.Remote.Protocol { [AttributeUsage(AttributeTargets.Class)] - public class AvaloniaRemoteMessageGuidAttribute : Attribute + public sealed class AvaloniaRemoteMessageGuidAttribute : Attribute { public Guid Guid { get; } diff --git a/src/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/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj b/src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj index a9cad0538f..9017ce1546 100644 --- a/src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj +++ b/src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 2dcce12df9..c3e90f5fd7 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -154,7 +154,7 @@ public static class LinuxFramebufferPlatformExtensions var lifetime = LinuxFramebufferPlatform.Initialize(builder, outputBackend, inputBackend); builder.SetupWithLifetime(lifetime); lifetime.Start(args); - builder.Instance.Run(lifetime.Token); + builder.Instance!.Run(lifetime.Token); return lifetime.ExitCode; } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index d61dcd4f91..0135cb3d1f 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -43,13 +43,13 @@ namespace Avalonia.LinuxFramebuffer.Output public IPlatformGraphics PlatformGraphics { get; private set; } public DrmOutput(DrmCard card, DrmResources resources, DrmConnector connector, DrmModeInfo modeInfo, - DrmOutputOptions? options = null) + DrmOutputOptions options = null) { if(options != null) _outputOptions = options; Init(card, resources, connector, modeInfo); } - public DrmOutput(string path = null, bool connectorsForceProbe = false, DrmOutputOptions? options = null) + public DrmOutput(string path = null, bool connectorsForceProbe = false, DrmOutputOptions options = null) { if(options != null) _outputOptions = options; @@ -63,7 +63,7 @@ namespace Avalonia.LinuxFramebuffer.Output if(connector == null) throw new InvalidOperationException("Unable to find connected DRM connector"); - DrmModeInfo? mode = null; + DrmModeInfo mode = null; if (options?.VideoMode != null) { diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index aaaee39b0d..197815f9a0 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -49,6 +49,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions InsertBefore( new AvaloniaXamlIlControlThemeTransformer(), new AvaloniaXamlIlSelectorTransformer(), + new AvaloniaXamlIlDuplicateSettersChecker(), new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), new AvaloniaXamlIlBindingPathParser(), new AvaloniaXamlIlPropertyPathTransformer(), diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/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/AvaloniaXamlIlControlThemeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs index 1338dc7248..51fe58d1c9 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs @@ -11,7 +11,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (!(node is XamlAstObjectNode on && on.Type.GetClrType().FullName == "Avalonia.Styling.ControlTheme")) + if (node is not XamlAstObjectNode on || + !context.GetAvaloniaTypes().ControlTheme.IsAssignableFrom(on.Type.GetClrType())) return node; // Check if we've already transformed this node. diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDuplicateSettersChecker.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDuplicateSettersChecker.cs new file mode 100644 index 0000000000..4ab9594cd8 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDuplicateSettersChecker.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; + +class AvaloniaXamlIlDuplicateSettersChecker : IXamlAstTransformer +{ + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (node is not XamlAstObjectNode objectNode) + { + return node; + } + + var nodeType = objectNode.Type.GetClrType(); + if (!context.GetAvaloniaTypes().Style.IsAssignableFrom(nodeType) && + !context.GetAvaloniaTypes().ControlTheme.IsAssignableFrom(nodeType)) + { + return node; + } + + var properties = objectNode.Children + .OfType() + .Where(n => n.Type.GetClrType().Name == "Setter") + .SelectMany(setter => + setter.Children.OfType() + .Where(c => c.Property.GetClrProperty().Name == "Property")) + .Select(p => p.Values[0]) + .OfType() + .Select(x => x.Text); + var index = new HashSet(); + foreach (var property in properties) + { + if (!index.Add(property)) + { + throw new XamlParseException($"Duplicate setter encountered for property '{property}'", node); + } + } + + return node; + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs index 078e23bc02..edddc5424a 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs @@ -19,7 +19,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (!(node is XamlAstObjectNode on && on.Type.GetClrType().FullName == "Avalonia.Styling.Style")) + if (node is not XamlAstObjectNode on || + !context.GetAvaloniaTypes().Style.IsAssignableFrom(on.Type.GetClrType())) return node; var pn = on.Children.OfType() diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 0aa3dda693..60a7d953ab 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -56,7 +56,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType DataTemplate { get; } public IXamlType IDataTemplate { get; } public IXamlType ItemsControl { get; } - public IXamlType ItemsRepeater { get; } public IXamlType ReflectionBindingExtension { get; } public IXamlType RelativeSource { get; } @@ -68,6 +67,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; } @@ -111,6 +111,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlMethod ResourceDictionaryDeferredAdd { get; } public IXamlType UriKind { get; } public IXamlConstructor UriConstructor { get; } + public IXamlType Style { get; } + public IXamlType ControlTheme { get; } public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg) { @@ -181,7 +183,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers DataTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.DataTemplate"); IDataTemplate = cfg.TypeSystem.GetType("Avalonia.Controls.Templates.IDataTemplate"); ItemsControl = cfg.TypeSystem.GetType("Avalonia.Controls.ItemsControl"); - ItemsRepeater = cfg.TypeSystem.GetType("Avalonia.Controls.ItemsRepeater"); ReflectionBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension"); RelativeSource = cfg.TypeSystem.GetType("Avalonia.Data.RelativeSource"); UInt = cfg.TypeSystem.GetType("System.UInt32"); @@ -190,6 +191,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) { @@ -248,6 +250,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers XamlIlTypes.Object)); UriKind = cfg.TypeSystem.GetType("System.UriKind"); UriConstructor = Uri.GetConstructor(new List() { cfg.WellKnownTypes.String, UriKind }); + Style = cfg.TypeSystem.GetType("Avalonia.Styling.Style"); + ControlTheme = cfg.TypeSystem.GetType("Avalonia.Styling.ControlTheme"); } } 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/Markup/Avalonia.Markup.Xaml/XamlTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs index 8d6f8cdf3a..da4d7374d4 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs @@ -34,7 +34,8 @@ namespace Avalonia.Markup.Xaml } - public class ConstructorArgumentAttribute : Attribute + [AttributeUsage(AttributeTargets.Property)] + public sealed class ConstructorArgumentAttribute : Attribute { public ConstructorArgumentAttribute(string name) { diff --git a/src/Shared/ModuleInitializer.cs b/src/Shared/ModuleInitializer.cs index a72929e06f..e58b296474 100644 --- a/src/Shared/ModuleInitializer.cs +++ b/src/Shared/ModuleInitializer.cs @@ -1,7 +1,8 @@ namespace System.Runtime.CompilerServices { #if NETSTANDARD2_0 - internal class ModuleInitializerAttribute : Attribute + [AttributeUsage(AttributeTargets.Method)] + internal sealed class ModuleInitializerAttribute : Attribute { } diff --git a/src/Shared/SourceGeneratorAttributes.cs b/src/Shared/SourceGeneratorAttributes.cs index 3f00fbef57..bdd21d0426 100644 --- a/src/Shared/SourceGeneratorAttributes.cs +++ b/src/Shared/SourceGeneratorAttributes.cs @@ -16,7 +16,9 @@ namespace Avalonia.SourceGenerator } - internal class GetProcAddressAttribute : Attribute + + [AttributeUsage(AttributeTargets.Method)] + internal sealed class GetProcAddressAttribute : Attribute { public GetProcAddressAttribute(string proc) { @@ -39,11 +41,14 @@ namespace Avalonia.SourceGenerator } } - internal class GenerateEnumValueDictionaryAttribute : Attribute + [AttributeUsage(AttributeTargets.Method)] + internal sealed class GenerateEnumValueDictionaryAttribute : Attribute { } - internal class GenerateEnumValueListAttribute : Attribute + + [AttributeUsage(AttributeTargets.Method)] + internal sealed class GenerateEnumValueListAttribute : Attribute { } } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index dcb20d2a44..ba646c64ee 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -208,6 +208,12 @@ namespace Avalonia.Skia public void DrawLine(IPen pen, Point p1, Point p2) { CheckLease(); + + if (pen is null) + { + return; + } + using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) { if (paint.Paint is object) @@ -495,6 +501,12 @@ namespace Avalonia.Skia public void DrawGlyphRun(IBrush foreground, IRef glyphRun) { CheckLease(); + + if (foreground is null) + { + return; + } + using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Item.Size)) { var glyphRunImpl = (GlyphRunImpl)glyphRun.Item; diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index d12db39ad6..e795f3d304 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -86,7 +86,7 @@ namespace Avalonia.Skia SKPath path = new SKPath(); - var (currentX, currentY) = glyphRun.PlatformImpl.Item.BaselineOrigin; + var (currentX, currentY) = glyphRun.BaselineOrigin; for (var i = 0; i < glyphRun.GlyphInfos.Count; i++) { diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index eb3f9911df..99c01dd111 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -257,7 +257,7 @@ namespace Avalonia.Direct2D1 sink.Close(); } - var (baselineOriginX, baselineOriginY) = glyphRun.PlatformImpl.Item.BaselineOrigin; + var (baselineOriginX, baselineOriginY) = glyphRun.BaselineOrigin; var transformedGeometry = new SharpDX.Direct2D1.TransformedGeometry( Direct2D1Factory, diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs index 24b8fc04b3..446db47d92 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Platform; using SharpDX.DirectWrite; @@ -25,8 +26,6 @@ namespace Avalonia.Direct2D1.Media } public IReadOnlyList GetIntersections(float lowerBound, float upperBound) - { - return null; - } + => Array.Empty(); } } diff --git a/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..a24fe31df8 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -10,7 +10,7 @@ - + @@ -27,4 +27,7 @@ $(NoWarn);CA1416 + + + diff --git a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj index 0c0fe5b921..f3af312d1a 100644 --- a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj +++ b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj @@ -23,7 +23,7 @@ - + diff --git a/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs b/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs index 181883656c..690926a193 100644 --- a/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs +++ b/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs @@ -1,16 +1,79 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; +using System.Text.RegularExpressions; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.XamlIl; -namespace Avalonia.Designer.HostApp +namespace Avalonia.Designer.HostApp; + +class DesignXamlLoader : AvaloniaXamlLoader.IRuntimeXamlLoader { - class DesignXamlLoader : AvaloniaXamlLoader.IRuntimeXamlLoader + public object Load(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration) + { + PreloadDepsAssemblies(configuration.LocalAssembly ?? Assembly.GetEntryAssembly()); + + return AvaloniaXamlIlRuntimeCompiler.Load(document, configuration); + } + + private void PreloadDepsAssemblies(Assembly targetAssembly) { - public object Load(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration) + // Assemblies loaded in memory (e.g. single file) return empty string from Location. + // In these cases, don't try probing next to the assembly. + var assemblyLocation = targetAssembly.Location; + if (string.IsNullOrEmpty(assemblyLocation)) + { + return; + } + + var depsJsonFile = Path.ChangeExtension(assemblyLocation, ".deps.json"); + if (!File.Exists(depsJsonFile)) + { + return; + } + + using var stream = File.OpenRead(depsJsonFile); + + /* + We can't use any references in the Avalonia.Designer.HostApp. Including even json. + Ideally we would prefer Microsoft.Extensions.DependencyModel package, but can't use it here. + So, instead we need to fallback to some JSON parsing using pretty easy regex. + + Json part example: +"Avalonia.Xaml.Interactions/11.0.0-preview5": { + "dependencies": { + "Avalonia": "11.0.999", + "Avalonia.Xaml.Interactivity": "11.0.0-preview5" + }, + "runtime": { + "lib/net6.0/Avalonia.Xaml.Interactions.dll": { + "assemblyVersion": "11.0.0.0", + "fileVersion": "11.0.0.0" + } + } +}, + We want to extract "lib/net6.0/Avalonia.Xaml.Interactions.dll" from here. + No need to resolve real path of ref assemblies. + No need to handle special cases with .NET Framework and GAC. + */ + var text = new StreamReader(stream).ReadToEnd(); + var matches = Regex.Matches( text, """runtime"\s*:\s*{\s*"([^"]+)"""); + + foreach (Match match in matches) { - return AvaloniaXamlIlRuntimeCompiler.Load(document, configuration); + if (match.Groups[1] is { Success: true } g) + { + var assemblyName = Path.GetFileNameWithoutExtension(g.Value); + try + { + _ = Assembly.Load(new AssemblyName(assemblyName)); + } + catch + { + } + } } } } diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs index 135ab0426e..c293a9101d 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs @@ -112,7 +112,7 @@ class Template var defs = cl.Members.OfType().First(m => m.Identifier.Text == "InitializeDefaults"); - cl = cl.ReplaceNode(defs.Body, defs.Body.AddStatements( + cl = cl.ReplaceNode(defs.Body!, defs.Body!.AddStatements( ParseStatement($"_list = new ServerListProxyHelper<{itemType}, {serverItemType}>(this);"))); diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.cs index 3b5d3d8c3f..dfc8b45579 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.cs @@ -297,8 +297,8 @@ namespace Avalonia.SourceGenerator.CompositionGenerator server = server.WithBaseList( server.BaseList?.AddTypes(SimpleBaseType(ParseTypeName(impl.ServerName)))); - client = client.AddMembers( - ParseMemberDeclaration($"{impl.ServerName} {impl.Name}.Server => Server;")); + if(ParseMemberDeclaration($"{impl.ServerName} {impl.Name}.Server => Server;") is { } member) + client = client.AddMembers(member); } diff --git a/src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs b/src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs index 86dbb3a452..c975bb8444 100644 --- a/src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs +++ b/src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs @@ -32,7 +32,7 @@ public class EnumMemberDictionaryGenerator : IIncrementalGenerator ).Collect(); context.RegisterSourceOutput(all, static (context, methods) => { - foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default)) + foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default)) { var classBuilder = new StringBuilder(); if (typeGroup.Key.ContainingNamespace != null) diff --git a/src/tools/DevGenerators/GetProcAddressInitialization.cs b/src/tools/DevGenerators/GetProcAddressInitialization.cs index aedc13e7f6..e8d7c251fa 100644 --- a/src/tools/DevGenerators/GetProcAddressInitialization.cs +++ b/src/tools/DevGenerators/GetProcAddressInitialization.cs @@ -34,7 +34,7 @@ public class GetProcAddressInitializationGenerator : IIncrementalGenerator var all = fieldsWithAttribute.Collect(); context.RegisterSourceOutput(all, static (context, methods) => { - foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default)) + foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default)) { var nextContext = 0; var contexts = new Dictionary(); 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/Rendering/SceneGraph/DrawOperationTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs index 07d2d672ae..c1468a28e4 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs @@ -29,7 +29,7 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph double height, double scaleX, double scaleY, - double? penThickness, + double penThickness, double expectedX, double expectedY, double expectedWidth, @@ -38,7 +38,7 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph var target = new TestRectangleDrawOperation( new Rect(x, y, width, height), Matrix.CreateScale(scaleX, scaleY), - penThickness.HasValue ? new Pen(Brushes.Black, penThickness.Value) : null); + new Pen(Brushes.Black, penThickness)); Assert.Equal(new Rect(expectedX, expectedY, expectedWidth, expectedHeight), target.Bounds); } diff --git a/tests/Avalonia.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/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index 0ddee2ad7a..9ea0482abc 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -14,7 +14,7 @@ - + diff --git a/tests/Avalonia.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.ItemsRepeater.UnitTests/Avalonia.Controls.ItemsRepeater.UnitTests.csproj b/tests/Avalonia.Controls.ItemsRepeater.UnitTests/Avalonia.Controls.ItemsRepeater.UnitTests.csproj new file mode 100644 index 0000000000..6f9815757e --- /dev/null +++ b/tests/Avalonia.Controls.ItemsRepeater.UnitTests/Avalonia.Controls.ItemsRepeater.UnitTests.csproj @@ -0,0 +1,22 @@ + + + net6.0 + Library + true + + + + + + + + + + + + + + + + + diff --git a/tests/Avalonia.Controls.UnitTests/ItemsRepeaterTests.cs b/tests/Avalonia.Controls.ItemsRepeater.UnitTests/ItemsRepeaterTests.cs similarity index 100% rename from tests/Avalonia.Controls.UnitTests/ItemsRepeaterTests.cs rename to tests/Avalonia.Controls.ItemsRepeater.UnitTests/ItemsRepeaterTests.cs diff --git a/tests/Avalonia.Base.UnitTests/Layout/NonVirtualizingStackLayoutTests.cs b/tests/Avalonia.Controls.ItemsRepeater.UnitTests/NonVirtualizingStackLayoutTests.cs similarity index 100% rename from tests/Avalonia.Base.UnitTests/Layout/NonVirtualizingStackLayoutTests.cs rename to tests/Avalonia.Controls.ItemsRepeater.UnitTests/NonVirtualizingStackLayoutTests.cs diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs index 8bd51ec500..2679d4ce06 100644 --- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs @@ -134,16 +134,15 @@ 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() + var target = new TestButton(renderer.Object) { - Bounds = new Rect(0, 0, 100, 100), - Renderer = renderer + Bounds = new Rect(0, 0, 100, 100) }; bool clicked = false; @@ -166,16 +165,15 @@ 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() + var target = new TestButton(renderer.Object) { - Bounds = new Rect(0, 0, 100, 100), - Renderer = renderer + Bounds = new Rect(0, 0, 100, 100) }; bool clicked = false; @@ -199,18 +197,17 @@ 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]); - var target = new TestButton() + var target = new TestButton(renderer.Object) { Bounds = new Rect(0, 0, 100, 100), - RenderTransform = new TranslateTransform { X = 100, Y = 0 }, - Renderer = renderer + RenderTransform = new TranslateTransform { X = 100, Y = 0 } }; //actual bounds of button should be 100,0,100,100 x -> translated 100 pixels @@ -386,9 +383,10 @@ namespace Avalonia.Controls.UnitTests private class TestButton : Button, IRenderRoot { - public TestButton() + public TestButton(IRenderer renderer) { IsVisible = true; + Renderer = renderer; } public new Rect Bounds @@ -399,7 +397,7 @@ namespace Avalonia.Controls.UnitTests public Size ClientSize => throw new NotImplementedException(); - public IRenderer Renderer { get; set; } + public IRenderer Renderer { get; } public double RenderScaling => throw new NotImplementedException(); diff --git a/tests/Avalonia.Controls.UnitTests/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/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index d63251c1f5..2644e7184a 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -6,6 +6,7 @@ using Avalonia.Input.Raw; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Platform; +using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.UnitTests; using Moq; @@ -20,7 +21,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); var target = new TestTopLevel(impl.Object); Assert.True(((ILogical)target).IsAttachedToLogicalTree); @@ -32,7 +33,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.Setup(x => x.ClientSize).Returns(new Size(123, 456)); var target = new TestTopLevel(impl.Object); @@ -46,7 +47,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.Setup(x => x.ClientSize).Returns(new Size(123, 456)); var target = new TestTopLevel(impl.Object); @@ -60,7 +61,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.Setup(x => x.ClientSize).Returns(new Size(123, 456)); var target = new TestTopLevel(impl.Object); @@ -76,7 +77,7 @@ namespace Avalonia.Controls.UnitTests using (UnitTestApplication.Start(services)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); var target = new TestTopLevel(impl.Object, Mock.Of()); @@ -91,7 +92,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.SetupProperty(x => x.Resized); impl.SetupGet(x => x.RenderScaling).Returns(1); @@ -117,7 +118,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.Setup(x => x.ClientSize).Returns(new Size(123, 456)); var target = new TestTopLevel(impl.Object); @@ -133,7 +134,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.SetupAllProperties(); impl.Setup(x => x.ClientSize).Returns(new Size(123, 456)); @@ -151,7 +152,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.SetupAllProperties(); bool raised = false; @@ -169,7 +170,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.SetupAllProperties(); var target = new TestTopLevel(impl.Object); @@ -200,7 +201,7 @@ namespace Avalonia.Controls.UnitTests using (UnitTestApplication.Start(services)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.SetupAllProperties(); var target = new TestTopLevel(impl.Object); @@ -222,7 +223,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.SetupAllProperties(); var target = new TestTopLevel(impl.Object); var child = new TestTopLevel(impl.Object); @@ -240,7 +241,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.SetupAllProperties(); var target = new TestTopLevel(impl.Object); var raised = false; @@ -257,7 +258,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.SetupAllProperties(); var layoutManager = new Mock(); @@ -274,7 +275,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockTopLevelImpl(); impl.SetupGet(x => x.RenderScaling).Returns(1); var child = new Border { Classes = { "foo" } }; @@ -317,6 +318,14 @@ namespace Avalonia.Controls.UnitTests }.RegisterInNameScope(scope)); } + private static Mock CreateMockTopLevelImpl() + { + var renderer = new Mock(); + renderer.Setup(r => r.CreateRenderer(It.IsAny())) + .Returns(RendererMocks.CreateRenderer().Object); + return renderer; + } + private class TestTopLevel : TopLevel { private readonly ILayoutManager _layoutManager; diff --git a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs index f367112cc0..336aad79da 100644 --- a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs @@ -4,9 +4,7 @@ using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Platform; -using Avalonia.Styling; using Avalonia.UnitTests; -using Moq; using Xunit; using Factory = System.Func, Avalonia.Controls.Window, Avalonia.AvaloniaObject>; @@ -20,7 +18,7 @@ namespace Avalonia.Controls.UnitTests.Utils using (AvaloniaLocator.EnterScope()) { AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new WindowingPlatformMock()); + .Bind().ToConstant(new MockWindowingPlatform()); var gesture1 = new KeyGesture(Key.A, KeyModifiers.Control); var gesture2 = new KeyGesture(Key.B, KeyModifiers.Control); @@ -64,7 +62,7 @@ namespace Avalonia.Controls.UnitTests.Utils var commandResult = 0; var expectedParameter = 1; AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new WindowingPlatformMock()); + .Bind().ToConstant(new MockWindowingPlatform()); var gesture = new KeyGesture(Key.A, KeyModifiers.Control); @@ -106,7 +104,7 @@ namespace Avalonia.Controls.UnitTests.Utils var target = new KeyboardDevice(); var isExecuted = false; AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new WindowingPlatformMock()); + .Bind().ToConstant(new MockWindowingPlatform()); var gesture = new KeyGesture(Key.A, KeyModifiers.Control); @@ -146,7 +144,7 @@ namespace Avalonia.Controls.UnitTests.Utils var target = new KeyboardDevice(); var clickExecutedCount = 0; AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new WindowingPlatformMock()); + .Bind().ToConstant(new MockWindowingPlatform()); var gesture = new KeyGesture(Key.A, KeyModifiers.Control); @@ -199,7 +197,7 @@ namespace Avalonia.Controls.UnitTests.Utils var clickExecutedCount = 0; var commandExecutedCount = 0; AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new WindowingPlatformMock()); + .Bind().ToConstant(new MockWindowingPlatform()); var gesture = new KeyGesture(Key.A, KeyModifiers.Control); diff --git a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs index 8f9af52ed8..d65fa06183 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs @@ -22,7 +22,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockWindowBaseImpl(); var target = new TestWindowBase(impl.Object); target.Activate(); @@ -36,7 +36,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockWindowBaseImpl(); impl.SetupAllProperties(); bool raised = false; @@ -55,7 +55,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var impl = new Mock(); + var impl = CreateMockWindowBaseImpl(); impl.SetupAllProperties(); bool raised = false; @@ -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); @@ -235,17 +241,28 @@ namespace Avalonia.Controls.UnitTests }.RegisterInNameScope(scope)); } + private static Mock CreateMockWindowBaseImpl() + { + var renderer = new Mock(); + renderer.Setup(r => r.CreateRenderer(It.IsAny())) + .Returns(RendererMocks.CreateRenderer().Object); + return renderer; + } + private class TestWindowBase : WindowBase { public bool IsClosed { get; private set; } 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..cada2bfa6f 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -15,6 +15,8 @@ namespace Avalonia.Controls.UnitTests public void Setting_Title_Should_Set_Impl_Title() { var windowImpl = new Mock(); + windowImpl.Setup(r => r.CreateRenderer(It.IsAny())) + .Returns(RendererMocks.CreateRenderer().Object); var windowingPlatform = new MockWindowingPlatform(() => windowImpl.Object); using (UnitTestApplication.Start(new TestServices(windowingPlatform: windowingPlatform))) @@ -98,6 +100,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 +273,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 +288,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 +321,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 +338,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 +381,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.Controls.UnitTests/WindowingPlatformMock.cs b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs deleted file mode 100644 index e8471d41fb..0000000000 --- a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using Moq; -using Avalonia.Platform; - -namespace Avalonia.Controls.UnitTests -{ - public class WindowingPlatformMock : IWindowingPlatform - { - private readonly Func _windowImpl; - private readonly Func _popupImpl; - - public WindowingPlatformMock(Func windowImpl = null, Func popupImpl = null ) - { - _windowImpl = windowImpl; - _popupImpl = popupImpl; - } - - public IWindowImpl CreateWindow() - { - return _windowImpl?.Invoke() ?? Mock.Of(x => x.RenderScaling == 1); - } - - public IWindowImpl CreateEmbeddableWindow() - { - throw new NotImplementedException(); - } - - public ITrayIconImpl CreateTrayIcon() - { - return null; - } - - public IPopupImpl CreatePopup() => _popupImpl?.Invoke() ?? Mock.Of(x => x.RenderScaling == 1); - } -} diff --git a/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj b/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj index 57338a1e08..5de2b85569 100644 --- a/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj +++ b/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj @@ -10,10 +10,11 @@ - + + diff --git a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs index e7837a6971..b9df420270 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs @@ -118,7 +118,16 @@ namespace Avalonia.IntegrationTests.Appium Thread.Sleep(1000); var newWindows = session.FindElements(By.XPath("/XCUIElementTypeApplication/XCUIElementTypeWindow")); - var newWindowTitles = newWindows.ToDictionary(x => x.Text); + + // Try to find the new window by looking for a window with a title that didn't exist before the button + // was clicked. Sometimes it seems that when a window becomes fullscreen, all other windows in the + // application lose their titles, so filter out windows with no title (this may have started happening + // with macOS 13.1?) + var newWindowTitles = newWindows + .Select(x => (x.Text, x)) + .Where(x => !string.IsNullOrEmpty(x.Text)) + .ToDictionary(x => x.Text, x => x.x); + var newWindowTitle = Assert.Single(newWindowTitles.Keys.Except(oldWindowTitles.Keys)); return Disposable.Create(() => diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index 4d833cdb1f..7bb991aae6 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -1,11 +1,14 @@ using System; +using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading; using Avalonia.Controls; +using Avalonia.Media.Imaging; using OpenQA.Selenium; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Interactions; +using SixLabors.ImageSharp.PixelFormats; using Xunit; using Xunit.Sdk; @@ -141,7 +144,6 @@ namespace Avalonia.IntegrationTests.Appium } } - [Theory] [InlineData(ShowWindowMode.NonOwned)] [InlineData(ShowWindowMode.Owned)] @@ -187,6 +189,47 @@ namespace Avalonia.IntegrationTests.Appium } } + [Fact] + public void TransparentWindow() + { + var showTransparentWindow = _session.FindElementByAccessibilityId("ShowTransparentWindow"); + showTransparentWindow.Click(); + Thread.Sleep(1000); + + var window = _session.FindElementByAccessibilityId("TransparentWindow"); + var screenshot = window.GetScreenshot(); + + window.Click(); + + var img = SixLabors.ImageSharp.Image.Load(screenshot.AsByteArray); + var topLeftColor = img[10, 10]; + var centerColor = img[img.Width / 2, img.Height / 2]; + + Assert.Equal(new Rgba32(0, 128, 0), topLeftColor); + Assert.Equal(new Rgba32(255, 0, 0), centerColor); + } + + [Fact] + public void TransparentPopup() + { + var showTransparentWindow = _session.FindElementByAccessibilityId("ShowTransparentPopup"); + showTransparentWindow.Click(); + Thread.Sleep(1000); + + var window = _session.FindElementByAccessibilityId("TransparentPopupBackground"); + var container = window.FindElementByAccessibilityId("PopupContainer"); + var screenshot = container.GetScreenshot(); + + window.Click(); + + var img = SixLabors.ImageSharp.Image.Load(screenshot.AsByteArray); + var topLeftColor = img[10, 10]; + var centerColor = img[img.Width / 2, img.Height / 2]; + + Assert.Equal(new Rgba32(0, 128, 0), topLeftColor); + Assert.Equal(new Rgba32(255, 0, 0), centerColor); + } + public static TheoryData StartupLocationData() { var sizes = new Size?[] { null, new Size(400, 300) }; diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 6c61a85561..d9817ecdd1 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -264,7 +264,7 @@ namespace Avalonia.IntegrationTests.Appium var secondaryWindow = GetWindow("SecondaryWindow"); var (_, miniaturizeButton, _) = secondaryWindow.GetChromeButtons(); - Assert.Equal(false, miniaturizeButton.Enabled); + Assert.False(miniaturizeButton.Enabled); } } diff --git a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj index 4572f7ae7c..c3d9aa0622 100644 --- a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj +++ b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj @@ -1,6 +1,6 @@  - net461 + net462 @@ -11,6 +11,7 @@ + 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/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj index fa4957c24c..ade6010bae 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -17,6 +17,7 @@ + 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.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs index f42f787117..be2cae8ec4 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs @@ -334,6 +334,47 @@ namespace Avalonia.Markup.Xaml.UnitTests var parsed = (Button)AvaloniaRuntimeXamlLoader.Load(document); Assert.Equal(Colors.Blue, ((ISolidColorBrush)parsed.Background!).Color); } + + [Fact] + public void Style_Parser_Throws_For_Duplicate_Setter() + { + var xaml = @" + + + + + +"; + AssertThrows(() => AvaloniaRuntimeXamlLoader.Load(xaml, typeof(XamlIlTests).Assembly, designMode: true), + e => e.Message.StartsWith("Duplicate setter encountered for property 'Height'")); + } + + [Fact] + public void Control_Theme_Parser_Throws_For_Duplicate_Setter() + { + var xaml = @" + + + + + + + + + + +"; + AssertThrows(() => AvaloniaRuntimeXamlLoader.Load(xaml, typeof(XamlIlTests).Assembly, designMode: true), + e => e.Message.StartsWith("Duplicate setter encountered for property 'Height'")); + } } public class XamlIlBugTestsEventHandlerCodeBehind : Window diff --git a/tests/Avalonia.RenderTests/Assets/NotoSansHebrew-Regular.ttf b/tests/Avalonia.RenderTests/Assets/NotoSansHebrew-Regular.ttf new file mode 100644 index 0000000000..703cfa472d Binary files /dev/null and b/tests/Avalonia.RenderTests/Assets/NotoSansHebrew-Regular.ttf differ diff --git a/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs b/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs index c11bd2b816..4210ee8238 100644 --- a/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs +++ b/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs @@ -1,3 +1,4 @@ +using System.Net; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Layout; @@ -17,6 +18,56 @@ namespace Avalonia.Direct2D1.RenderTests.Controls { } + [Win32Fact("Has text")] + public async Task Should_Draw_TextDecorations() + { + Border target = new Border + { + Padding = new Thickness(8), + Width = 200, + Height = 30, + Background = Brushes.White, + Child = new TextBlock + { + FontFamily = TestFontFamily, + FontSize = 12, + Foreground = Brushes.Black, + Text = "Neque porro quisquam est qui dolorem", + VerticalAlignment = VerticalAlignment.Top, + TextWrapping = TextWrapping.NoWrap, + TextDecorations = new TextDecorationCollection + { + new TextDecoration + { + Location = TextDecorationLocation.Overline, + StrokeThickness= 1.5, + StrokeThicknessUnit = TextDecorationUnit.Pixel, + Stroke = new SolidColorBrush(Colors.Red) + }, + new TextDecoration + { + Location = TextDecorationLocation.Baseline, + StrokeThickness= 1.5, + StrokeThicknessUnit = TextDecorationUnit.Pixel, + Stroke = new SolidColorBrush(Colors.Green) + }, + new TextDecoration + { + Location = TextDecorationLocation.Underline, + StrokeThickness= 1.5, + StrokeThicknessUnit = TextDecorationUnit.Pixel, + Stroke = new SolidColorBrush(Colors.Blue), + StrokeOffset = 2, + StrokeOffsetUnit = TextDecorationUnit.Pixel + } + } + } + }; + + await RenderToFile(target); + CompareImages(); + } + [Win32Fact("Has text")] public async Task Wrapping_NoWrap() { diff --git a/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj b/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj index ba45bbbc2e..0d182678ef 100644 --- a/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj +++ b/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj @@ -8,7 +8,7 @@ - + diff --git a/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj index ea91b8c196..86a680fac5 100644 --- a/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj +++ b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj @@ -9,7 +9,7 @@ - + diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index a748f6cf00..5a6d7f2cdf 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs @@ -17,6 +17,8 @@ namespace Avalonia.Skia.UnitTests.Media new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); private readonly Typeface _arabicTypeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans Arabic"); + private readonly Typeface _hebrewTypeface = + new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans Hebrew"); private readonly Typeface _italicTypeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans", FontStyle.Italic); private readonly Typeface _emojiTypeface = @@ -24,7 +26,7 @@ namespace Avalonia.Skia.UnitTests.Media public CustomFontManagerImpl() { - _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _arabicTypeface, _defaultTypeface }; + _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _arabicTypeface, _hebrewTypeface, _defaultTypeface }; _defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName; } @@ -88,6 +90,12 @@ namespace Avalonia.Skia.UnitTests.Media skTypeface = typefaceCollection.Get(typeface); break; } + case "Noto Sans Hebrew": + { + var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_hebrewTypeface.FontFamily); + skTypeface = typefaceCollection.Get(typeface); + break; + } case FontFamily.DefaultFontFamilyName: case "Noto Mono": { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs index f963277397..7837749adf 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs @@ -7,25 +7,27 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { private readonly string _text; private readonly GenericTextRunProperties _defaultGenericPropertiesRunProperties; + private readonly bool _addEndOfParagraph; - public SingleBufferTextSource(string text, GenericTextRunProperties defaultProperties) + public SingleBufferTextSource(string text, GenericTextRunProperties defaultProperties, bool addEndOfParagraph = false) { _text = text; _defaultGenericPropertiesRunProperties = defaultProperties; + _addEndOfParagraph = addEndOfParagraph; } public TextRun GetTextRun(int textSourceIndex) { if (textSourceIndex >= _text.Length) { - return null; + return _addEndOfParagraph ? new TextEndOfParagraph() : null; } var runText = _text.AsMemory(textSourceIndex); if (runText.IsEmpty) { - return null; + return _addEndOfParagraph ? new TextEndOfParagraph() : null; } return new TextCharacters(runText, _defaultGenericPropertiesRunProperties); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 7822d6624b..8a2d4ecc6b 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -242,10 +242,16 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var defaultProperties = new GenericTextRunProperties(Typeface.Default); - var textSource = new SingleBufferTextSource(text, defaultProperties); + var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.WrapWithOverflow); + + var textSource = new SingleBufferTextSource("ABCDEFHFFHFJHKHFK", defaultProperties, true); var formatter = new TextFormatterImpl(); + var line = formatter.FormatLine(textSource, 0, 33, paragraphProperties); + + textSource = new SingleBufferTextSource(text, defaultProperties); + var numberOfLines = 0; var currentPosition = 0; @@ -253,8 +259,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting while (currentPosition < text.Length) { var textLine = - formatter.FormatLine(textSource, currentPosition, 1, - new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.WrapWithOverflow)); + formatter.FormatLine(textSource, currentPosition, 1, paragraphProperties); if (text.Length - currentPosition > expectedCharactersPerLine) { @@ -558,7 +563,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); } } @@ -652,6 +660,90 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_Return_Null_For_Empty_TextSource() + { + using (Start()) + { + var defaultRunProperties = new GenericTextRunProperties(Typeface.Default); + var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties); + var textSource = new EmptyTextSource(); + + var textLine = TextFormatter.Current.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties); + + Assert.Null(textLine); + } + } + + [Fact] + public void Should_Retain_TextEndOfParagraph_With_TextWrapping() + { + using (Start()) + { + var defaultRunProperties = new GenericTextRunProperties(Typeface.Default); + var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties, textWrap: TextWrapping.Wrap); + + var text = "Hello World"; + + var textSource = new SimpleTextSource(text, defaultRunProperties); + + var pos = 0; + + TextLineBreak previousLineBreak = null; + TextLine textLine = null; + + while (pos < text.Length) + { + textLine = TextFormatter.Current.FormatLine(textSource, pos, 30, paragraphProperties, previousLineBreak); + + pos += textLine.Length; + + previousLineBreak = textLine.TextLineBreak; + } + + Assert.NotNull(textLine); + + Assert.NotNull(textLine.TextLineBreak.TextEndOfLine); + } + } + + protected readonly record struct SimpleTextSource : ITextSource + { + private readonly string _text; + private readonly TextRunProperties _defaultProperties; + + public SimpleTextSource(string text, TextRunProperties defaultProperties) + { + _text = text; + _defaultProperties = defaultProperties; + } + + public TextRun? GetTextRun(int textSourceIndex) + { + if (textSourceIndex > _text.Length) + { + return new TextEndOfParagraph(); + } + + var runText = _text.AsMemory(textSourceIndex); + + if (runText.IsEmpty) + { + return new TextEndOfParagraph(); + } + + return new TextCharacters(runText, _defaultProperties); + } + } + + private class EmptyTextSource : ITextSource + { + public TextRun GetTextRun(int textSourceIndex) + { + return null; + } + } + private class EndOfLineTextSource : ITextSource { public TextRun GetTextRun(int textSourceIndex) diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 2b63f24cf6..9a7460c218 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -9,7 +9,6 @@ using Avalonia.Media.TextFormatting.Unicode; using Avalonia.UnitTests; using Avalonia.Utilities; using Xunit; - namespace Avalonia.Skia.UnitTests.Media.TextFormatting { public class TextLayoutTests @@ -725,7 +724,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var selectedRect = rects[0]; - Assert.Equal(selectedText.Bounds.Width, selectedRect.Width); + Assert.Equal(selectedText.Bounds.Width, selectedRect.Width, 2); } } @@ -886,7 +885,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var distance = hitRange.First().Left; - Assert.Equal(currentX, distance); + Assert.Equal(currentX, distance, 2); currentX += advance; } @@ -916,7 +915,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var distance = hitRange.First().Left + 0.5; - Assert.Equal(currentX, distance); + Assert.Equal(currentX, distance, 2); currentX += advance; } @@ -1028,6 +1027,65 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [InlineData("mgfg🧐df f sdf", "g🧐d", 20, 40)] + [InlineData("وه. وقد تعرض لانتقادات", "دات", 5, 30)] + [InlineData("وه. وقد تعرض لانتقادات", "تعرض", 20, 50)] + [InlineData(" علمية 😱ومضللة ،", " علمية 😱ومضللة ،", 40, 100)] + [InlineData("في عام 2018 ، رفعت ل", "في عام 2018 ، رفعت ل", 100, 120)] + [Theory] + public void HitTestTextRange_Range_ValidLength(string text, string textToSelect, double minWidth, double maxWidth) + { + using (Start()) + { + var layout = new TextLayout(text, Typeface.Default, 12, Brushes.Black); + var start = text.IndexOf(textToSelect); + var selectionRectangles = layout.HitTestTextRange(start, textToSelect.Length); + Assert.Equal(1, selectionRectangles.Count()); + var rect = selectionRectangles.First(); + Assert.InRange(rect.Width, minWidth, maxWidth); + } + } + + [InlineData("012🧐210", 2, 4, FlowDirection.LeftToRight, "14.40234375,40.8046875")] + [InlineData("210🧐012", 2, 4, FlowDirection.RightToLeft, "0,7.201171875;21.603515625,33.603515625;48.005859375,55.20703125")] + [InlineData("שנב🧐שנב", 2, 4, FlowDirection.LeftToRight, "11.268,38.208")] + [InlineData("שנב🧐שנב", 2, 4, FlowDirection.RightToLeft, "11.268,38.208")] + [Theory] + public void Should_HitTextTextRangeBetweenRuns(string text, int start, int length, + FlowDirection flowDirection, string expected) + { + using (Start()) + { + var expectedRects = expected.Split(';').Select(x => + { + var startEnd = x.Split(','); + + var start = double.Parse(startEnd[0], CultureInfo.InvariantCulture); + + var end = double.Parse(startEnd[1], CultureInfo.InvariantCulture); + + return new Rect(start, 0, end - start, 0); + }).ToArray(); + + var textLayout = new TextLayout(text, Typeface.Default, 12, Brushes.Black, flowDirection: flowDirection); + + var rects = textLayout.HitTestTextRange(start, length).ToArray(); + + Assert.Equal(expectedRects.Length, rects.Length); + + var endX = textLayout.TextLines[0].GetDistanceFromCharacterHit(new CharacterHit(2)); + var startX = textLayout.TextLines[0].GetDistanceFromCharacterHit(new CharacterHit(5, 1)); + + for (int i = 0; i < expectedRects.Length; i++) + { + var expectedRect = expectedRects[i]; + + Assert.Equal(expectedRect.Left, rects[i].Left, 2); + + Assert.Equal(expectedRect.Right, rects[i].Right, 2); + } + } + } private static IDisposable Start() diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 544b84912e..70e74cdf83 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -604,19 +604,19 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textBounds = textLine.GetTextBounds(0, 20); - Assert.Equal(2, textBounds.Count); + Assert.Equal(1, textBounds.Count); Assert.Equal(144.0234375, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 30); - Assert.Equal(3, textBounds.Count); + Assert.Equal(1, textBounds.Count); Assert.Equal(216.03515625, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 40); - Assert.Equal(4, textBounds.Count); + Assert.Equal(1, textBounds.Count); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); } @@ -658,7 +658,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(TextTestHelper.GetStartCharIndex(run.Text), bounds.TextSourceCharacterIndex); Assert.Equal(run, bounds.TextRun); - Assert.Equal(run.Size.Width, bounds.Rectangle.Width); + Assert.Equal(run.Size.Width, bounds.Rectangle.Width, 2); } for (var i = 0; i < textBounds.Count; i++) @@ -667,19 +667,19 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting if (lastBounds != null) { - Assert.Equal(lastBounds.Rectangle.Right, currentBounds.Rectangle.Left); + Assert.Equal(lastBounds.Rectangle.Right, currentBounds.Rectangle.Left, 2); } var sumOfRunWidth = currentBounds.TextRunBounds.Sum(x => x.Rectangle.Width); - Assert.Equal(sumOfRunWidth, currentBounds.Rectangle.Width); + Assert.Equal(sumOfRunWidth, currentBounds.Rectangle.Width, 2); lastBounds = currentBounds; } var sumOfBoundsWidth = textBounds.Sum(x => x.Rectangle.Width); - Assert.Equal(lineWidth, sumOfBoundsWidth); + Assert.Equal(lineWidth, sumOfBoundsWidth, 2); } } @@ -847,7 +847,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textBounds = textLine.GetTextBounds(0, textLine.Length); - Assert.Equal(6, textBounds.Count); + Assert.Equal(1, textBounds.Count); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 1); @@ -857,7 +857,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textBounds = textLine.GetTextBounds(0, firstRun.Length + 1); - Assert.Equal(2, textBounds.Count); + Assert.Equal(1, textBounds.Count); Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(1, firstRun.Length); @@ -867,7 +867,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textBounds = textLine.GetTextBounds(0, 1 + firstRun.Length); - Assert.Equal(2, textBounds.Count); + Assert.Equal(1, textBounds.Count); Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); } } @@ -958,14 +958,15 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(secondRun.Size.Width, textBounds[1].Rectangle.Width); Assert.Equal(7.201171875, textBounds[0].Rectangle.Width); - Assert.Equal(textLine.Start + 7.201171875, textBounds[0].Rectangle.Right); - Assert.Equal(textLine.Start + firstRun.Size.Width, textBounds[1].Rectangle.Left); + + Assert.Equal(textLine.Start + 7.201171875, textBounds[0].Rectangle.Right, 2); + Assert.Equal(textLine.Start + firstRun.Size.Width, textBounds[1].Rectangle.Left, 2); textBounds = textLine.GetTextBounds(0, text.Length); Assert.Equal(2, textBounds.Count); Assert.Equal(7, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length))); - Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); + Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width), 2); } } diff --git a/tests/Avalonia.UnitTests/MockGlyphRun.cs b/tests/Avalonia.UnitTests/MockGlyphRun.cs index 477f34565f..0319803a5e 100644 --- a/tests/Avalonia.UnitTests/MockGlyphRun.cs +++ b/tests/Avalonia.UnitTests/MockGlyphRun.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media.TextFormatting; using Avalonia.Platform; @@ -24,12 +25,9 @@ namespace Avalonia.UnitTests public void Dispose() { - } public IReadOnlyList GetIntersections(float lowerBound, float upperBound) - { - return null; - } + => Array.Empty(); } } 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() diff --git a/tests/Avalonia.UnitTests/TestTemplatedRoot.cs b/tests/Avalonia.UnitTests/TestTemplatedRoot.cs deleted file mode 100644 index 38ab3c3c5d..0000000000 --- a/tests/Avalonia.UnitTests/TestTemplatedRoot.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using Avalonia.Controls; -using Avalonia.Controls.Presenters; -using Avalonia.Controls.Templates; -using Avalonia.Layout; -using Avalonia.LogicalTree; -using Avalonia.Platform; -using Avalonia.Rendering; -using Avalonia.Styling; - -namespace Avalonia.UnitTests -{ - public class TestTemplatedRoot : ContentControl, ILayoutRoot, IRenderRoot, ILogicalRoot - { - private readonly NameScope _nameScope = new NameScope(); - - public TestTemplatedRoot() - { - LayoutManager = new LayoutManager(this); - Template = new FuncControlTemplate((x, scope) => new ContentPresenter - { - Name = "PART_ContentPresenter", - }.RegisterInNameScope(scope)); - } - - public Size ClientSize => new Size(100, 100); - - public Size MaxClientSize => Size.Infinity; - - public double LayoutScaling => 1; - - public ILayoutManager LayoutManager { get; set; } - - public double RenderScaling => 1; - - public IRenderTarget RenderTarget => null; - - public IRenderer Renderer => null; - - public IRenderTarget CreateRenderTarget() - { - throw new NotImplementedException(); - } - - public void Invalidate(Rect rect) - { - throw new NotImplementedException(); - } - - public Point PointToClient(PixelPoint p) => p.ToPoint(1); - - public PixelPoint PointToScreen(Point p) => PixelPoint.FromPoint(p, 1); - } -} diff --git a/tests/TestFiles/Direct2D1/Controls/TextBlock/Should_Draw_TextDecorations.expected.png b/tests/TestFiles/Direct2D1/Controls/TextBlock/Should_Draw_TextDecorations.expected.png new file mode 100644 index 0000000000..494c8a9002 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Controls/TextBlock/Should_Draw_TextDecorations.expected.png differ diff --git a/tests/TestFiles/Skia/Controls/TextBlock/Should_Draw_TextDecorations.expected.png b/tests/TestFiles/Skia/Controls/TextBlock/Should_Draw_TextDecorations.expected.png new file mode 100644 index 0000000000..297bd592ff Binary files /dev/null and b/tests/TestFiles/Skia/Controls/TextBlock/Should_Draw_TextDecorations.expected.png differ