diff --git a/.editorconfig b/.editorconfig index 1583d3e469..3620896f34 100644 --- a/.editorconfig +++ b/.editorconfig @@ -140,6 +140,8 @@ dotnet_analyzer_diagnostic.category-Performance.severity = none #error - Uncomme # CS1591: Missing XML comment for publicly visible type or member dotnet_diagnostic.CS1591.severity = suggestion +# CS0162: Remove unreachable code +dotnet_diagnostic.CS0162.severity = error # CA1304: Specify CultureInfo dotnet_diagnostic.CA1304.severity = warning # CA1802: Use literals where appropriate @@ -152,6 +154,8 @@ dotnet_diagnostic.CA1820.severity = warning dotnet_diagnostic.CA1821.severity = warning # CA1822: Mark members as static dotnet_diagnostic.CA1822.severity = suggestion +# CA1823: Avoid unused private fields +dotnet_diagnostic.CA1823.severity = warning dotnet_code_quality.CA1822.api_surface = private, internal # CA1825: Avoid zero-length array allocations dotnet_diagnostic.CA1825.severity = warning @@ -165,6 +169,8 @@ dotnet_diagnostic.CA1828.severity = warning dotnet_diagnostic.CA1829.severity = warning #CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters dotnet_diagnostic.CA1847.severity = warning +#CACA2211:Non-constant fields should not be visible +dotnet_diagnostic.CA2211.severity = error # Wrapping preferences csharp_wrap_before_ternary_opsigns = false diff --git a/Avalonia.sln b/Avalonia.sln index 7efb294b64..fc42a5d63b 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -100,7 +100,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\EmbedXaml.props = build\EmbedXaml.props build\HarfBuzzSharp.props = build\HarfBuzzSharp.props build\ImageSharp.props = build\ImageSharp.props - build\JetBrains.Annotations.props = build\JetBrains.Annotations.props build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.props build\Microsoft.CSharp.props = build\Microsoft.CSharp.props build\Microsoft.Reactive.Testing.props = build\Microsoft.Reactive.Testing.props @@ -136,8 +135,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Linux", "Linux", "{86C53C40 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.LinuxFramebuffer", "src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj", "{854568D5-13D1-4B4F-B50D-534DC7EFD3C9}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Direct3DInteropSample", "samples\interop\Direct3DInteropSample\Direct3DInteropSample.csproj", "{638580B0-7910-40EF-B674-DCB34DA308CD}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Win32.Interop", "src\Windows\Avalonia.Win32.Interop\Avalonia.Win32.Interop.csproj", "{CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia.RenderTests", "tests\Avalonia.Skia.RenderTests\Avalonia.Skia.RenderTests.csproj", "{E1582370-37B3-403C-917F-8209551B1634}" @@ -228,9 +225,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Browser", "src\Bro EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Browser.Blazor", "src\Browser\Avalonia.Browser.Blazor\Avalonia.Browser.Blazor.csproj", "{47F8530C-F19B-4B1A-B4D6-EB231522AE5D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser", "samples\ControlCatalog.Browser\ControlCatalog.Browser.csproj", "{15B93A4C-1B46-43F6-B534-7B25B6E99932}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser", "samples\ControlCatalog.Browser\ControlCatalog.Browser.csproj", "{15B93A4C-1B46-43F6-B534-7B25B6E99932}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser.Blazor", "samples\ControlCatalog.Browser.Blazor\ControlCatalog.Browser.Blazor.csproj", "{90B08091-9BBD-4362-B712-E9F2CC62B218}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser.Blazor", "samples\ControlCatalog.Browser.Blazor\ControlCatalog.Browser.Blazor.csproj", "{90B08091-9BBD-4362-B712-E9F2CC62B218}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}" EndProject @@ -366,10 +363,6 @@ Global {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|Any CPU.Build.0 = Debug|Any CPU {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|Any CPU.ActiveCfg = Release|Any CPU {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|Any CPU.Build.0 = Release|Any CPU - {638580B0-7910-40EF-B674-DCB34DA308CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {638580B0-7910-40EF-B674-DCB34DA308CD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {638580B0-7910-40EF-B674-DCB34DA308CD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {638580B0-7910-40EF-B674-DCB34DA308CD}.Release|Any CPU.Build.0 = Release|Any CPU {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Debug|Any CPU.Build.0 = Debug|Any CPU {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -580,7 +573,6 @@ Global {7D2D3083-71DD-4CC9-8907-39A0D86FB322} = {3743B0F2-CC41-4F14-A8C8-267F579BF91E} {39D7B147-1A5B-47C2-9D01-21FB7C47C4B3} = {9B9E3891-2366-4253-A952-D08BCEB71098} {854568D5-13D1-4B4F-B50D-534DC7EFD3C9} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} - {638580B0-7910-40EF-B674-DCB34DA308CD} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E} = {B39A8919-9F95-48FE-AD7B-76E08B509888} {E1582370-37B3-403C-917F-8209551B1634} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {E2999E4A-9086-401F-898C-AEB0AD38E676} = {9B9E3891-2366-4253-A952-D08BCEB71098} diff --git a/Directory.Build.props b/Directory.Build.props index 42daa2df7f..c19a55e8ea 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,5 +7,6 @@ false false False + 11 diff --git a/NOTICE.md b/NOTICE.md index e97fc654c9..7083706c3e 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -81,14 +81,14 @@ A "contributor" is any person that distributes its contribution under this licen https://github.com/wayland-project/wayland-protocols -Copyright 2008-2013 Kristian Hgsberg -Copyright 2010-2013 Intel Corporation -Copyright 2013 Rafael Antognolli -Copyright 2013 Jasper St. Pierre -Copyright 2014 Jonas dahl -Copyright 2014 Jason Ekstrand -Copyright 2014-2015 Collabora, Ltd. -Copyright 2015 Red Hat Inc. +Copyright © 2008-2013 Kristian Høgsberg +Copyright © 2010-2013 Intel Corporation +Copyright © 2013 Rafael Antognolli +Copyright © 2013 Jasper St. Pierre +Copyright © 2014 Jonas Ådahl +Copyright © 2014 Jason Ekstrand +Copyright © 2014-2015 Collabora, Ltd. +Copyright © 2015 Red Hat Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), @@ -140,7 +140,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. https://github.com/toptensoftware/RichTextKit -Copyright 2019 Topten Software. All Rights Reserved. +Copyright © 2019 Topten Software. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this product except in compliance with the License. You may obtain @@ -303,3 +303,62 @@ https://github.com/chromium/chromium // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# Flutter + +https://github.com/flutter/flutter + +//Copyright 2014 The Flutter Authors. All rights reserved. + +//Redistribution and use in source and binary forms, with or without modification, +//are permitted provided that the following conditions are met: + +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. + +//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +//LOSS OF USE, DATA, OR PROFITS; +//OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +//ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# Reactive Extensions + +https://github.com/dotnet/reactive + +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 43253ac6be..5735da19ab 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -13,14 +13,14 @@ jobs: steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.401' + displayName: 'Use .NET Core SDK 6.0.404' inputs: - version: 6.0.401 + version: 6.0.404 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 7.0.100' + displayName: 'Use .NET Core SDK 7.0.101' inputs: - version: 7.0.100 + version: 7.0.101 - script: system_profiler SPDisplaysDataType |grep Resolution @@ -51,14 +51,14 @@ jobs: steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.401' + displayName: 'Use .NET Core SDK 6.0.404' inputs: - version: 6.0.401 + version: 6.0.404 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 7.0.100' + displayName: 'Use .NET Core SDK 7.0.101' inputs: - version: 7.0.100 + version: 7.0.101 - task: Windows Application Driver@0 inputs: diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a3bbc33418..8bab6e68e2 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -30,14 +30,14 @@ jobs: vmImage: 'ubuntu-20.04' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.401' + displayName: 'Use .NET Core SDK 6.0.404' inputs: - version: 6.0.401 + version: 6.0.404 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 7.0' + displayName: 'Use .NET Core SDK 7.0.101' inputs: - version: 7.0.100 + version: 7.0.101 - task: CmdLine@2 displayName: 'Install Workloads' @@ -67,14 +67,14 @@ jobs: vmImage: 'macos-12' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.401' + displayName: 'Use .NET Core SDK 6.0.404' inputs: - version: 6.0.401 + version: 6.0.404 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 7.0.100' + displayName: 'Use .NET Core SDK 7.0.101' inputs: - version: 7.0.100 + version: 7.0.101 - task: CmdLine@2 displayName: 'Install Workloads' @@ -138,14 +138,14 @@ jobs: SolutionDir: '$(Build.SourcesDirectory)' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.401' + displayName: 'Use .NET Core SDK 6.0.404' inputs: - version: 6.0.401 + version: 6.0.404 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 7.0.100' + displayName: 'Use .NET Core SDK 7.0.101' inputs: - version: 7.0.100 + version: 7.0.101 - task: CmdLine@2 displayName: 'Install Workloads' diff --git a/build/Base.props b/build/Base.props index 100c9088cd..26f19e3abc 100644 --- a/build/Base.props +++ b/build/Base.props @@ -1,6 +1,7 @@  - + + diff --git a/build/JetBrains.Annotations.props b/build/JetBrains.Annotations.props deleted file mode 100644 index 7bc12cbd84..0000000000 --- a/build/JetBrains.Annotations.props +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/build/SharedVersion.props b/build/SharedVersion.props index e9c3d65b41..eca3ba37b0 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -8,7 +8,6 @@ https://github.com/AvaloniaUI/Avalonia/ true $(NoWarn);CS1591 - preview MIT Icon.png Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly. diff --git a/build/System.Memory.props b/build/System.Memory.props index b36998a780..a413e18927 100644 --- a/build/System.Memory.props +++ b/build/System.Memory.props @@ -1,5 +1,5 @@ - + diff --git a/global.json b/global.json index a9318b212f..8536535b51 100644 --- a/global.json +++ b/global.json @@ -1,11 +1,9 @@ { "sdk": { - "version": "7.0.100", + "version": "7.0.101", "rollForward": "latestFeature" }, "msbuild-sdks": { - "Microsoft.Build.Traversal": "1.0.43", - "MSBuild.Sdk.Extras": "3.0.22", - "AggregatePackage.NuGet.Sdk" : "0.1.12" + "Microsoft.Build.Traversal": "3.2.0" } } diff --git a/nukebuild/DotNetConfigHelper.cs b/nukebuild/DotNetConfigHelper.cs index eca1e2684d..9d43261616 100644 --- a/nukebuild/DotNetConfigHelper.cs +++ b/nukebuild/DotNetConfigHelper.cs @@ -1,5 +1,4 @@ using System.Globalization; -using JetBrains.Annotations; using Nuke.Common.Tools.DotNet; // ReSharper disable ReturnValueOfPureMethodIsNotUsed diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index ca2d4b66ed..33f22f4d02 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -48,8 +48,12 @@ + + <_AvaloniaResourcesInputsCacheFilePath>$(IntermediateOutputPath)/Avalonia/Resources.Inputs.cache + + - + @@ -57,7 +61,11 @@ - + + + + + - + + + + @@ -103,6 +114,9 @@ File="$(AvaloniaXamlReferencesTemporaryFilePath)" Lines="@(ReferencePathWithRefAssemblies)" Overwrite="true" /> + + + + DefaultCompileBindings="$(AvaloniaUseCompiledBindingsByDefault)"> + + diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs index 62c582610c..f6fa07dbde 100644 --- a/samples/ControlCatalog.Android/MainActivity.cs +++ b/samples/ControlCatalog.Android/MainActivity.cs @@ -5,7 +5,7 @@ using Avalonia.Android; namespace ControlCatalog.Android { - [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)] + [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.Main", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)] public class MainActivity : AvaloniaMainActivity { } diff --git a/samples/ControlCatalog.Android/Resources/values/styles.xml b/samples/ControlCatalog.Android/Resources/values/styles.xml index 2759d2904a..49e079a719 100644 --- a/samples/ControlCatalog.Android/Resources/values/styles.xml +++ b/samples/ControlCatalog.Android/Resources/values/styles.xml @@ -14,4 +14,8 @@ @null + + diff --git a/samples/ControlCatalog.Android/SplashActivity.cs b/samples/ControlCatalog.Android/SplashActivity.cs index 908b5f082a..a0b68b129b 100644 --- a/samples/ControlCatalog.Android/SplashActivity.cs +++ b/samples/ControlCatalog.Android/SplashActivity.cs @@ -28,6 +28,8 @@ namespace ControlCatalog.Android base.OnResume(); StartActivity(new Intent(Application.Context, typeof(MainActivity))); + + Finish(); } } } diff --git a/samples/ControlCatalog.Browser/Properties/launchSettings.json b/samples/ControlCatalog.Browser/Properties/launchSettings.json new file mode 100644 index 0000000000..76c1834e3c --- /dev/null +++ b/samples/ControlCatalog.Browser/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "ControlCatalog.Browser": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/debug?browser={browserInspectUri}" + } + } +} diff --git a/samples/ControlCatalog.Desktop/Program.cs b/samples/ControlCatalog.Desktop/Program.cs index 7b8b27fff7..4d28f15e2c 100644 --- a/samples/ControlCatalog.Desktop/Program.cs +++ b/samples/ControlCatalog.Desktop/Program.cs @@ -23,7 +23,7 @@ namespace ControlCatalog private static void ConfigureAssetAssembly(AppBuilder builder) { AvaloniaLocator.CurrentMutable - .GetService() + .GetRequiredService() .SetDefaultAssembly(typeof(App).Assembly); } } diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 4a5f5bc96c..166b98436e 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -92,6 +92,9 @@ + + + @@ -206,8 +209,7 @@ + HorizontalAlignment="Stretch"> None Transparent diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 15e666ae7b..f3c1a68e72 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -6,6 +6,7 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Media.Immutable; +using Avalonia.VisualTree; using ControlCatalog.Models; using ControlCatalog.Pages; @@ -59,17 +60,25 @@ namespace ControlCatalog }; var transparencyLevels = this.Get("TransparencyLevels"); - IDisposable? backgroundSetter = null, paneBackgroundSetter = null; + IDisposable? topLevelBackgroundSideSetter = null, sideBarBackgroundSetter = null, paneBackgroundSetter = null; transparencyLevels.SelectionChanged += (sender, e) => { - backgroundSetter?.Dispose(); + topLevelBackgroundSideSetter?.Dispose(); + sideBarBackgroundSetter?.Dispose(); paneBackgroundSetter?.Dispose(); - if (transparencyLevels.SelectedItem is WindowTransparencyLevel selected - && selected != WindowTransparencyLevel.None) + if (transparencyLevels.SelectedItem is WindowTransparencyLevel selected) { - var semiTransparentBrush = new ImmutableSolidColorBrush(Colors.Gray, 0.5); - backgroundSetter = sideBar.SetValue(BackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style); - paneBackgroundSetter = sideBar.SetValue(SplitView.PaneBackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style); + var topLevel = (TopLevel)this.GetVisualRoot()!; + topLevel.TransparencyLevelHint = selected; + + if (selected != WindowTransparencyLevel.None) + { + var transparentBrush = new ImmutableSolidColorBrush(Colors.White, 0); + var semiTransparentBrush = new ImmutableSolidColorBrush(Colors.Gray, 0.2); + topLevelBackgroundSideSetter = topLevel.SetValue(BackgroundProperty, transparentBrush, Avalonia.Data.BindingPriority.Style); + sideBarBackgroundSetter = sideBar.SetValue(BackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style); + paneBackgroundSetter = sideBar.SetValue(SplitView.PaneBackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style); + } } }; } diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index cebb3e0916..442c1d37b0 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -10,7 +10,6 @@ ExtendClientAreaToDecorationsHint="{Binding ExtendClientAreaEnabled}" ExtendClientAreaChromeHints="{Binding ChromeHints}" ExtendClientAreaTitleBarHeightHint="{Binding TitleBarHeight}" - TransparencyLevelHint="{Binding TransparencyLevel}" x:Name="MainWindow" Background="Transparent" x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}" diff --git a/samples/ControlCatalog/Models/StateData.cs b/samples/ControlCatalog/Models/StateData.cs index bd6d186252..a7ea655cd2 100644 --- a/samples/ControlCatalog/Models/StateData.cs +++ b/samples/ControlCatalog/Models/StateData.cs @@ -6,10 +6,10 @@ public class StateData public string Abbreviation { get; private set; } public string Capital { get; private set; } - public StateData(string name, string abbreviatoin, string capital) + public StateData(string name, string abbreviation, string capital) { Name = name; - Abbreviation = abbreviatoin; + Abbreviation = abbreviation; Capital = capital; } @@ -17,4 +17,4 @@ public class StateData { return Name; } -} \ No newline at end of file +} diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml b/samples/ControlCatalog/Pages/CompositionPage.axaml index 403f45b8eb..602b9b768d 100644 --- a/samples/ControlCatalog/Pages/CompositionPage.axaml +++ b/samples/ControlCatalog/Pages/CompositionPage.axaml @@ -2,44 +2,57 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:pages="using:ControlCatalog.Pages" x:Class="ControlCatalog.Pages.CompositionPage"> - - Implicit animations + + + - - - - - - - - - - - - - - - - - - - - - - Resize me - - + + + + + + + + + + + + + + + + + + + + + Resize me + + - - - + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml.cs b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs index c70675b606..8b12a2d663 100644 --- a/samples/ControlCatalog/Pages/CompositionPage.axaml.cs +++ b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs @@ -1,28 +1,39 @@ using System; using System.Collections.Generic; +using System.Numerics; +using System.Threading; using Avalonia; +using Avalonia.Animation; using Avalonia.Controls; +using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition.Animations; using Avalonia.VisualTree; +using Math = System.Math; namespace ControlCatalog.Pages; public partial class CompositionPage : UserControl { private ImplicitAnimationCollection? _implicitAnimations; + private CompositionCustomVisual? _customVisual; + private CompositionSolidColorVisual? _solidVisual; public CompositionPage() { AvaloniaXamlLoader.Load(this); + AttachAnimatedSolidVisual(this.FindControl("SolidVisualHost")!); + AttachCustomVisual(this.FindControl("CustomVisualHost")!); } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); this.Get("Items").Items = CreateColorItems(); + } private static List CreateColorItems() @@ -126,6 +137,167 @@ public partial class CompositionPage : UserControl compositionVisual.ImplicitAnimations = page._implicitAnimations; } } + + void AttachAnimatedSolidVisual(Visual v) + { + void Update() + { + if(_solidVisual == null) + return; + _solidVisual.Size = new Vector2((float)v.Bounds.Width / 3, (float)v.Bounds.Height / 3); + _solidVisual.Offset = new Vector3((float)v.Bounds.Width / 3, (float)v.Bounds.Height / 3, 0); + } + v.AttachedToVisualTree += delegate + { + var compositor = ElementComposition.GetElementVisual(v)?.Compositor; + if(compositor == null || _solidVisual?.Compositor == compositor) + return; + _solidVisual = compositor.CreateSolidColorVisual(); + ElementComposition.SetElementChildVisual(v, _solidVisual); + _solidVisual.Color = Colors.Red; + var animation = _solidVisual.Compositor.CreateColorKeyFrameAnimation(); + animation.InsertKeyFrame(0, Colors.Red); + animation.InsertKeyFrame(0.5f, Colors.Blue); + animation.InsertKeyFrame(1, Colors.Green); + animation.Duration = TimeSpan.FromSeconds(5); + animation.IterationBehavior = AnimationIterationBehavior.Forever; + animation.Direction = PlaybackDirection.Alternate; + _solidVisual.StartAnimation("Color", animation); + + _solidVisual.AnchorPoint = new Vector2(0, 0); + + var scale = _solidVisual.Compositor.CreateVector3KeyFrameAnimation(); + scale.Duration = TimeSpan.FromSeconds(5); + scale.IterationBehavior = AnimationIterationBehavior.Forever; + scale.InsertKeyFrame(0, new Vector3(1, 1, 0)); + scale.InsertKeyFrame(0.5f, new Vector3(1.5f, 1.5f, 0)); + scale.InsertKeyFrame(1, new Vector3(1, 1, 0)); + + _solidVisual.StartAnimation("Scale", scale); + + var center = + _solidVisual.Compositor.CreateExpressionAnimation( + "Vector3(this.Target.Size.X * 0.5, this.Target.Size.Y * 0.5, 1)"); + _solidVisual.StartAnimation("CenterPoint", center); + Update(); + }; + v.PropertyChanged += (_, a) => + { + if (a.Property == BoundsProperty) + Update(); + }; + } + + void AttachCustomVisual(Visual v) + { + void Update() + { + if (_customVisual == null) + return; + var h = (float)Math.Min(v.Bounds.Height, v.Bounds.Width / 3); + _customVisual.Size = new Vector2((float)v.Bounds.Width, h); + _customVisual.Offset = new Vector3(0, (float)(v.Bounds.Height - h) / 2, 0); + } + v.AttachedToVisualTree += delegate + { + var compositor = ElementComposition.GetElementVisual(v)?.Compositor; + if(compositor == null || _customVisual?.Compositor == compositor) + return; + _customVisual = compositor.CreateCustomVisual(new CustomVisualHandler()); + ElementComposition.SetElementChildVisual(v, _customVisual); + _customVisual.SendHandlerMessage(CustomVisualHandler.StartMessage); + Update(); + }; + + v.PropertyChanged += (_, a) => + { + if (a.Property == BoundsProperty) + Update(); + }; + } + + class CustomVisualHandler : CompositionCustomVisualHandler + { + private TimeSpan _animationElapsed; + private TimeSpan? _lastServerTime; + private bool _running; + + public static readonly object StopMessage = new(), StartMessage = new(); + + public override void OnRender(ImmediateDrawingContext drawingContext) + { + if (_running) + { + if (_lastServerTime.HasValue) _animationElapsed += (CompositionNow - _lastServerTime.Value); + _lastServerTime = CompositionNow; + } + + const int cnt = 20; + var maxPointSizeX = EffectiveSize.X / (cnt * 1.6); + var maxPointSizeY = EffectiveSize.Y / 4; + var pointSize = Math.Min(maxPointSizeX, maxPointSizeY); + var animationLength = TimeSpan.FromSeconds(4); + var animationStage = _animationElapsed.TotalSeconds / animationLength.TotalSeconds; + + var sinOffset = Math.Cos(_animationElapsed.TotalSeconds) * 1.5; + + for (var c = 0; c < cnt; c++) + { + var stage = (animationStage + (double)c / cnt) % 1; + var colorStage = + (animationStage + (Math.Sin(_animationElapsed.TotalSeconds * 2) + 1) / 2 + (double)c / cnt) % 1; + var posX = (EffectiveSize.X + pointSize * 3) * stage - pointSize; + var posY = (EffectiveSize.Y - pointSize) * (1 + Math.Sin(stage * 3.14 * 3 + sinOffset)) / 2 + pointSize / 2; + var opacity = Math.Sin(stage * 3.14); + + + drawingContext.DrawEllipse(new ImmutableSolidColorBrush(Color.FromArgb( + 255, + (byte)(255 - 255 * colorStage), + (byte)(255 * Math.Abs(0.5 - colorStage) * 2), + (byte)(255 * colorStage) + ), opacity), null, + new Point(posX, posY), pointSize / 2, pointSize / 2); + } + + } + + public override void OnMessage(object message) + { + if (message == StartMessage) + { + _running = true; + _lastServerTime = null; + RegisterForNextAnimationFrameUpdate(); + } + else if (message == StopMessage) + _running = false; + } + + public override void OnAnimationFrameUpdate() + { + if (_running) + { + Invalidate(); + RegisterForNextAnimationFrameUpdate(); + } + } + } + + private void ButtonThreadSleep(object? sender, RoutedEventArgs e) + { + Thread.Sleep(10000); + } + + private void ButtonStartCustomVisual(object? sender, RoutedEventArgs e) + { + _customVisual?.SendHandlerMessage(CustomVisualHandler.StartMessage); + } + + private void ButtonStopCustomVisual(object? sender, RoutedEventArgs e) + { + _customVisual?.SendHandlerMessage(CustomVisualHandler.StopMessage); + } } public class CompositionPageColorItem diff --git a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs index 11f504f4db..7520dabf37 100644 --- a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs @@ -17,7 +17,7 @@ namespace ControlCatalog.Pages this.Get("TimePickerDesc").Text = "Use a TimePicker to let users set a time in your app, for example " + "to set a reminder. The TimePicker displays three controls for hour, minute, and AM / PM(if necessary).These controls " + "are easy to use with touch or mouse, and they can be styled and configured in several different ways. " + - "12 - hour or 24 - hour clock and visiblility of AM / PM is dynamically set based on user time settings, or can be overridden."; + "12 - hour or 24 - hour clock and visibility of AM / PM is dynamically set based on user time settings, or can be overridden."; } diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index 67e9ef4e40..8db6e76dca 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -396,8 +396,8 @@ CanPickFolder: {storageProvider.CanPickFolder}"; return item.TryGetUri(out var uri) ? uri.ToString() : item.Name; } - Window GetWindow() => this.VisualRoot as Window ?? throw new NullReferenceException("Invalid Owner"); - TopLevel GetTopLevel() => this.VisualRoot as TopLevel ?? throw new NullReferenceException("Invalid Owner"); + Window GetWindow() => TopLevel.GetTopLevel(this) as Window ?? throw new NullReferenceException("Invalid Owner"); + TopLevel GetTopLevel() => TopLevel.GetTopLevel(this) ?? throw new NullReferenceException("Invalid Owner"); private void InitializeComponent() { diff --git a/samples/ControlCatalog/Pages/GesturePage.cs b/samples/ControlCatalog/Pages/GesturePage.cs new file mode 100644 index 0000000000..ee10f21317 --- /dev/null +++ b/samples/ControlCatalog/Pages/GesturePage.cs @@ -0,0 +1,212 @@ +using System; +using System.Numerics; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.LogicalTree; +using Avalonia.Markup.Xaml; +using Avalonia.Rendering.Composition; + +namespace ControlCatalog.Pages +{ + public class GesturePage : UserControl + { + private bool _isInit; + private float _currentScale; + + public GesturePage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + if(_isInit) + { + return; + } + + _isInit = true; + + SetPullHandlers(this.Find("TopPullZone"), false); + SetPullHandlers(this.Find("BottomPullZone"), true); + SetPullHandlers(this.Find("RightPullZone"), true); + SetPullHandlers(this.Find("LeftPullZone"), false); + + var image = this.Find("PinchImage"); + SetPinchHandlers(image); + + var reset = this.Find + + diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml.cs b/samples/ControlCatalog/Pages/ImagePage.xaml.cs index 511b01c7ac..5b3169a1b0 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml.cs +++ b/samples/ControlCatalog/Pages/ImagePage.xaml.cs @@ -70,7 +70,7 @@ namespace ControlCatalog.Pages 3 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, 0), cropSize), 4 => new PixelRect(new PixelPoint(0, bitmapHeight - cropSize.Height), cropSize), 5 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, bitmapHeight - cropSize.Height), cropSize), - _ => PixelRect.Empty + _ => default }; } diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml.cs b/samples/ControlCatalog/Pages/MenuPage.xaml.cs index 52c122f2bc..32cfa9b22b 100644 --- a/samples/ControlCatalog/Pages/MenuPage.xaml.cs +++ b/samples/ControlCatalog/Pages/MenuPage.xaml.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Reactive; using System.Threading.Tasks; using System.Windows.Input; using Avalonia.Controls; diff --git a/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs b/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs index bfd49a2c00..6f83e5c366 100644 --- a/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs @@ -27,7 +27,7 @@ namespace ControlCatalog.Pages { base.OnAttachedToVisualTree(e); - _viewModel.NotificationManager = new Avalonia.Controls.Notifications.WindowNotificationManager(VisualRoot as TopLevel); + _viewModel.NotificationManager = new Avalonia.Controls.Notifications.WindowNotificationManager(TopLevel.GetTopLevel(this)); } } } diff --git a/samples/ControlCatalog/Pages/PlatformInfoPage.xaml.cs b/samples/ControlCatalog/Pages/PlatformInfoPage.xaml.cs index 1f37451782..6325af2688 100644 --- a/samples/ControlCatalog/Pages/PlatformInfoPage.xaml.cs +++ b/samples/ControlCatalog/Pages/PlatformInfoPage.xaml.cs @@ -1,5 +1,8 @@ -using Avalonia.Controls; +using System; +using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Media.Immutable; using ControlCatalog.ViewModels; namespace ControlCatalog.Pages diff --git a/samples/ControlCatalog/Pages/PointerCanvas.cs b/samples/ControlCatalog/Pages/PointerCanvas.cs index 7d26da8603..da1ff5442d 100644 --- a/samples/ControlCatalog/Pages/PointerCanvas.cs +++ b/samples/ControlCatalog/Pages/PointerCanvas.cs @@ -24,7 +24,7 @@ public class PointerCanvas : Control { struct CanvasPoint { - public IBrush Brush; + public IBrush? Brush; public Point Point; public double Radius; public double? Pressure; diff --git a/samples/ControlCatalog/Pages/PointerContactsTab.cs b/samples/ControlCatalog/Pages/PointerContactsTab.cs index b6aabebf99..1751b046c0 100644 --- a/samples/ControlCatalog/Pages/PointerContactsTab.cs +++ b/samples/ControlCatalog/Pages/PointerContactsTab.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Linq; using Avalonia; using Avalonia.Controls; diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs index 6af4cf353e..b5b80fb147 100644 --- a/samples/ControlCatalog/Pages/ScreenPage.cs +++ b/samples/ControlCatalog/Pages/ScreenPage.cs @@ -36,44 +36,44 @@ namespace ControlCatalog.Pages var drawBrush = Brushes.Black; Pen p = new Pen(drawBrush); - if (screens != null) - foreach (Screen screen in screens) + + foreach (Screen screen in screens) + { + if (screen.Bounds.X / 10f < _leftMost) { - if (screen.Bounds.X / 10f < _leftMost) - { - _leftMost = screen.Bounds.X / 10f; - Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); - return; - } + _leftMost = screen.Bounds.X / 10f; + Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); + return; + } - Rect boundsRect = new Rect(screen.Bounds.X / 10f + Math.Abs(_leftMost), screen.Bounds.Y / 10f, screen.Bounds.Width / 10f, - screen.Bounds.Height / 10f); - Rect workingAreaRect = new Rect(screen.WorkingArea.X / 10f + Math.Abs(_leftMost), screen.WorkingArea.Y / 10f, screen.WorkingArea.Width / 10f, - screen.WorkingArea.Height / 10f); - - context.DrawRectangle(p, boundsRect); - context.DrawRectangle(p, workingAreaRect); + Rect boundsRect = new Rect(screen.Bounds.X / 10f + Math.Abs(_leftMost), screen.Bounds.Y / 10f, screen.Bounds.Width / 10f, + screen.Bounds.Height / 10f); + Rect workingAreaRect = new Rect(screen.WorkingArea.X / 10f + Math.Abs(_leftMost), screen.WorkingArea.Y / 10f, screen.WorkingArea.Width / 10f, + screen.WorkingArea.Height / 10f); + context.DrawRectangle(p, boundsRect); + context.DrawRectangle(p, workingAreaRect); - var formattedText = CreateFormattedText($"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}"); - context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height)); - formattedText = - CreateFormattedText($"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}"); - context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 20)); + var formattedText = CreateFormattedText($"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}"); + context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height)); - formattedText = CreateFormattedText($"Scaling: {screen.Scaling * 100}%"); - context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 40)); + formattedText = + CreateFormattedText($"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}"); + context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 20)); - formattedText = CreateFormattedText($"IsPrimary: {screen.IsPrimary}"); + formattedText = CreateFormattedText($"Scaling: {screen.Scaling * 100}%"); + context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 40)); - context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 60)); + formattedText = CreateFormattedText($"IsPrimary: {screen.IsPrimary}"); - formattedText = - CreateFormattedText( - $"Current: {screen.Equals(w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))))}"); - context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 80)); - } + context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 60)); + + formattedText = + CreateFormattedText( + $"Current: {screen.Equals(w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))))}"); + context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 80)); + } context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10f, w.Bounds.Width / 10, w.Bounds.Height / 10)); } diff --git a/samples/ControlCatalog/Pages/TabControlPage.xaml.cs b/samples/ControlCatalog/Pages/TabControlPage.xaml.cs index 413b6e1c75..f3f4bf6e93 100644 --- a/samples/ControlCatalog/Pages/TabControlPage.xaml.cs +++ b/samples/ControlCatalog/Pages/TabControlPage.xaml.cs @@ -51,7 +51,7 @@ namespace ControlCatalog.Pages private static IBitmap LoadBitmap(string uri) { - var assets = AvaloniaLocator.Current!.GetService()!; + var assets = AvaloniaLocator.Current.GetRequiredService(); return new Bitmap(assets.Open(new Uri(uri))); } } diff --git a/samples/ControlCatalog/Pages/TextBoxPage.xaml b/samples/ControlCatalog/Pages/TextBoxPage.xaml index 6a4d9b7d0e..7408399873 100644 --- a/samples/ControlCatalog/Pages/TextBoxPage.xaml +++ b/samples/ControlCatalog/Pages/TextBoxPage.xaml @@ -38,7 +38,7 @@ UseFloatingWatermark="True" PasswordChar="*" Text="Password" /> - + - - None - Transparent - Blur - AcrylicBlur - Mica - diff --git a/samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs b/samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs index bbe970afd6..d3e4ea7c31 100644 --- a/samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ComboBoxPageViewModel.cs @@ -1,7 +1,6 @@ using System; using System.Collections.ObjectModel; using System.Linq; -using System.Reactive; using Avalonia.Controls; using Avalonia.Controls.Selection; using MiniMvvm; diff --git a/samples/ControlCatalog/ViewModels/ContextPageViewModel.cs b/samples/ControlCatalog/ViewModels/ContextPageViewModel.cs index 4cfb00625e..f041f32b10 100644 --- a/samples/ControlCatalog/ViewModels/ContextPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ContextPageViewModel.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Reactive; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.VisualTree; @@ -56,12 +55,9 @@ namespace ControlCatalog.ViewModels var result = await window.StorageProvider.OpenFilePickerAsync(new Avalonia.Platform.Storage.FilePickerOpenOptions() { AllowMultiple = true }); - if (result != null) + foreach (var file in result) { - foreach (var file in result) - { - System.Diagnostics.Debug.WriteLine($"Opened: {file.Name}"); - } + System.Diagnostics.Debug.WriteLine($"Opened: {file.Name}"); } } diff --git a/samples/ControlCatalog/ViewModels/CursorPageViewModel.cs b/samples/ControlCatalog/ViewModels/CursorPageViewModel.cs index 8a3f0ba947..f44d927801 100644 --- a/samples/ControlCatalog/ViewModels/CursorPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/CursorPageViewModel.cs @@ -18,7 +18,7 @@ namespace ControlCatalog.ViewModels .Select(x => new StandardCursorModel(x)) .ToList(); - var loader = AvaloniaLocator.Current!.GetService()!; + var loader = AvaloniaLocator.Current.GetRequiredService(); var s = loader.Open(new Uri("avares://ControlCatalog/Assets/avalonia-32.png")); var bitmap = new Bitmap(s); CustomCursor = new Cursor(bitmap, new PixelPoint(16, 16)); diff --git a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs index f89d9d1e20..7f32536b11 100644 --- a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs @@ -1,7 +1,6 @@ using System; using System.Collections.ObjectModel; using System.Linq; -using System.Reactive; using Avalonia.Controls; using Avalonia.Controls.Selection; using ControlCatalog.Pages; diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index b79eff780c..3628a9b8a7 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -1,9 +1,9 @@ -using System.Reactive; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Notifications; using Avalonia.Dialogs; using Avalonia.Platform; +using Avalonia.Reactive; using System; using System.ComponentModel.DataAnnotations; using MiniMvvm; @@ -12,12 +12,9 @@ namespace ControlCatalog.ViewModels { class MainWindowViewModel : ViewModelBase { - private IManagedNotificationManager _notificationManager; - private bool _isMenuItemChecked = true; private WindowState _windowState; private WindowState[] _windowStates = Array.Empty(); - private int _transparencyLevel; private ExtendClientAreaChromeHints _chromeHints = ExtendClientAreaChromeHints.PreferSystemChrome; private bool _extendClientAreaEnabled; private bool _systemTitleBarEnabled; @@ -77,12 +74,6 @@ namespace ControlCatalog.ViewModels TitleBarHeight = -1; } - public int TransparencyLevel - { - get { return _transparencyLevel; } - set { this.RaiseAndSetIfChanged(ref _transparencyLevel, value); } - } - public ExtendClientAreaChromeHints ChromeHints { get { return _chromeHints; } diff --git a/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs b/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs index 16051c3c05..df62ba04cb 100644 --- a/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs @@ -1,6 +1,4 @@ using System.Collections.Generic; -using System.Reactive; -using System.Reactive.Linq; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.VisualTree; diff --git a/samples/ControlCatalog/ViewModels/NotificationViewModel.cs b/samples/ControlCatalog/ViewModels/NotificationViewModel.cs index a31f164a2a..bcbcb345ef 100644 --- a/samples/ControlCatalog/ViewModels/NotificationViewModel.cs +++ b/samples/ControlCatalog/ViewModels/NotificationViewModel.cs @@ -1,5 +1,4 @@ -using System.Reactive; -using Avalonia.Controls.Notifications; +using Avalonia.Controls.Notifications; using MiniMvvm; namespace ControlCatalog.ViewModels diff --git a/samples/ControlCatalog/ViewModels/PlatformInformationViewModel.cs b/samples/ControlCatalog/ViewModels/PlatformInformationViewModel.cs index e4f6c3ac73..60f1f84cc9 100644 --- a/samples/ControlCatalog/ViewModels/PlatformInformationViewModel.cs +++ b/samples/ControlCatalog/ViewModels/PlatformInformationViewModel.cs @@ -1,3 +1,5 @@ +using System; +using System.Runtime.InteropServices; using Avalonia; using Avalonia.Platform; using MiniMvvm; @@ -13,7 +15,7 @@ public class PlatformInformationViewModel : ViewModelBase if (runtimeInfo is { } info) { - if (info.IsBrowser) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER"))) { if (info.IsDesktop) { diff --git a/samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs b/samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs index d4b43043be..aa15d7758b 100644 --- a/samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs +++ b/samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs @@ -1,6 +1,5 @@ using System.Collections.ObjectModel; using System.Linq; -using System.Reactive; using System.Threading.Tasks; using Avalonia.Controls.Notifications; using ControlCatalog.Pages; diff --git a/samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs b/samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs index 93857fd899..4505c11e95 100644 --- a/samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs @@ -19,7 +19,7 @@ namespace ControlCatalog.ViewModels { public TransitioningContentControlPageViewModel() { - var assetLoader = AvaloniaLocator.Current?.GetService()!; + var assetLoader = AvaloniaLocator.Current.GetRequiredService(); var images = new string[] { diff --git a/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs index 80d4844f7a..7c0855e0af 100644 --- a/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs @@ -1,7 +1,6 @@ using System; using System.Collections.ObjectModel; using System.Linq; -using System.Reactive; using Avalonia.Controls; using MiniMvvm; diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props index 471c42ec07..3b14f0ce12 100644 --- a/samples/Directory.Build.props +++ b/samples/Directory.Build.props @@ -2,9 +2,8 @@ false $(MSBuildThisFileDirectory)..\src\tools\Avalonia.Designer.HostApp\bin\Debug\netcoreapp2.0\Avalonia.Designer.HostApp.dll + false + 11 - - false - diff --git a/samples/MiniMvvm/MiniMvvm.csproj b/samples/MiniMvvm/MiniMvvm.csproj index 6535b2bdbd..2a9164624a 100644 --- a/samples/MiniMvvm/MiniMvvm.csproj +++ b/samples/MiniMvvm/MiniMvvm.csproj @@ -2,5 +2,7 @@ netstandard2.0 - + + + diff --git a/samples/MiniMvvm/PropertyChangedExtensions.cs b/samples/MiniMvvm/PropertyChangedExtensions.cs index f1065c7530..f51773810d 100644 --- a/samples/MiniMvvm/PropertyChangedExtensions.cs +++ b/samples/MiniMvvm/PropertyChangedExtensions.cs @@ -1,8 +1,8 @@ using System; using System.ComponentModel; using System.Linq.Expressions; -using System.Reactive.Linq; using System.Reflection; +using Avalonia.Reactive; namespace MiniMvvm { @@ -92,11 +92,13 @@ namespace MiniMvvm Expression> v3, Func cb ) where TModel : INotifyPropertyChanged => - Observable.CombineLatest( - model.WhenAnyValue(v1), - model.WhenAnyValue(v2), - model.WhenAnyValue(v3), - cb); + model.WhenAnyValue(v1) + .CombineLatest( + model.WhenAnyValue(v2), + (l, r) => (l, r)) + .CombineLatest( + model.WhenAnyValue(v3), + (t, r) => cb(t.l, t.r, r)); public static IObservable> WhenAnyValue(this TModel model, Expression> v1, diff --git a/samples/MiniMvvm/ViewModelBase.cs b/samples/MiniMvvm/ViewModelBase.cs index 7256b05cef..8bc398607f 100644 --- a/samples/MiniMvvm/ViewModelBase.cs +++ b/samples/MiniMvvm/ViewModelBase.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.ComponentModel; -using System.Reactive.Joins; using System.Runtime.CompilerServices; namespace MiniMvvm diff --git a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj index 5c743aabdb..5f61a08f3c 100644 --- a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj +++ b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj @@ -11,4 +11,5 @@ + diff --git a/samples/Previewer/Previewer.csproj b/samples/Previewer/Previewer.csproj index 2cc84168dc..76c1ba7b69 100644 --- a/samples/Previewer/Previewer.csproj +++ b/samples/Previewer/Previewer.csproj @@ -12,7 +12,8 @@ - + + diff --git a/samples/ReactiveUIDemo/ReactiveUIDemo.csproj b/samples/ReactiveUIDemo/ReactiveUIDemo.csproj index 94ca4ee809..9650068434 100644 --- a/samples/ReactiveUIDemo/ReactiveUIDemo.csproj +++ b/samples/ReactiveUIDemo/ReactiveUIDemo.csproj @@ -23,6 +23,5 @@ - diff --git a/samples/interop/Direct3DInteropSample/App.paml b/samples/interop/Direct3DInteropSample/App.paml deleted file mode 100644 index e6d77dfaf4..0000000000 --- a/samples/interop/Direct3DInteropSample/App.paml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/samples/interop/Direct3DInteropSample/App.paml.cs b/samples/interop/Direct3DInteropSample/App.paml.cs deleted file mode 100644 index 29365decfe..0000000000 --- a/samples/interop/Direct3DInteropSample/App.paml.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Markup.Xaml; - -namespace Direct3DInteropSample -{ - public class App : Application - { - public override void Initialize() - { - AvaloniaXamlLoader.Load(this); - } - - public override void OnFrameworkInitializationCompleted() - { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - desktop.MainWindow = new MainWindow(); - base.OnFrameworkInitializationCompleted(); - } - } -} diff --git a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj b/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj deleted file mode 100644 index f9ef4693d5..0000000000 --- a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - Exe - net461 - - - - - - %(Filename) - - - Designer - - - - - - - - PreserveNewest - - - - - - - - - - - diff --git a/samples/interop/Direct3DInteropSample/MainWindow.cs b/samples/interop/Direct3DInteropSample/MainWindow.cs deleted file mode 100644 index 6cc3cb9116..0000000000 --- a/samples/interop/Direct3DInteropSample/MainWindow.cs +++ /dev/null @@ -1,283 +0,0 @@ -using System; - -using Avalonia; -using Avalonia.Controls; -using Avalonia.Direct2D1; -using Avalonia.Direct2D1.Media; -using Avalonia.Markup.Xaml; -using Avalonia.Platform; -using Avalonia.Rendering; - -using SharpDX; -using SharpDX.D3DCompiler; -using SharpDX.Direct2D1; -using SharpDX.Direct3D; -using SharpDX.Direct3D11; -using SharpDX.DXGI; - -using AlphaMode = SharpDX.Direct2D1.AlphaMode; -using Buffer = SharpDX.Direct3D11.Buffer; -using DeviceContext = SharpDX.Direct2D1.DeviceContext; -using Factory2 = SharpDX.DXGI.Factory2; -using InputElement = SharpDX.Direct3D11.InputElement; -using Matrix = SharpDX.Matrix; -using PixelFormat = SharpDX.Direct2D1.PixelFormat; -using Resource = SharpDX.Direct3D11.Resource; - -namespace Direct3DInteropSample -{ - public class MainWindow : Window - { - Texture2D _backBuffer; - RenderTargetView _renderView; - Texture2D _depthBuffer; - DepthStencilView _depthView; - private readonly SwapChain _swapChain; - private SwapChainDescription1 _desc; - private Matrix _proj = Matrix.Identity; - private readonly Matrix _view; - private Buffer _contantBuffer; - private DeviceContext _deviceContext; - private readonly MainWindowViewModel _model; - - public MainWindow() - { - DataContext = _model = new MainWindowViewModel(); - - _desc = new SwapChainDescription1() - { - BufferCount = 1, - Width = (int)ClientSize.Width, - Height = (int)ClientSize.Height, - Format = Format.R8G8B8A8_UNorm, - SampleDescription = new SampleDescription(1, 0), - SwapEffect = SwapEffect.Discard, - Usage = Usage.RenderTargetOutput - }; - - using (var factory = Direct2D1Platform.DxgiDevice.Adapter.GetParent()) - { - _swapChain = new SwapChain1(factory, Direct2D1Platform.DxgiDevice, PlatformImpl?.Handle.Handle ?? IntPtr.Zero, ref _desc); - } - - _deviceContext = new DeviceContext(Direct2D1Platform.Direct2D1Device, DeviceContextOptions.None) - { - DotsPerInch = new Size2F(96, 96) - }; - - CreateMesh(); - - _view = Matrix.LookAtLH(new Vector3(0, 0, -5), new Vector3(0, 0, 0), Vector3.UnitY); - - this.GetObservable(ClientSizeProperty).Subscribe(Resize); - - Resize(ClientSize); - - AvaloniaXamlLoader.Load(this); - - Background = Avalonia.Media.Brushes.Transparent; - } - - - protected override void HandlePaint(Rect rect) - { - var viewProj = Matrix.Multiply(_view, _proj); - var context = Direct2D1Platform.Direct3D11Device.ImmediateContext; - - // Clear views - context.ClearDepthStencilView(_depthView, DepthStencilClearFlags.Depth, 1.0f, 0); - context.ClearRenderTargetView(_renderView, Color.White); - - // Update WorldViewProj Matrix - var worldViewProj = Matrix.RotationX((float)_model.RotationX) * Matrix.RotationY((float)_model.RotationY) - * Matrix.RotationZ((float)_model.RotationZ) - * Matrix.Scaling((float)_model.Zoom) - * viewProj; - worldViewProj.Transpose(); - context.UpdateSubresource(ref worldViewProj, _contantBuffer); - - // Draw the cube - context.Draw(36, 0); - base.HandlePaint(rect); - - // Present! - _swapChain.Present(0, PresentFlags.None); - } - - private void CreateMesh() - { - var device = Direct2D1Platform.Direct3D11Device; - - // Compile Vertex and Pixel shaders - var vertexShaderByteCode = ShaderBytecode.CompileFromFile("MiniCube.fx", "VS", "vs_4_0"); - var vertexShader = new VertexShader(device, vertexShaderByteCode); - - var pixelShaderByteCode = ShaderBytecode.CompileFromFile("MiniCube.fx", "PS", "ps_4_0"); - var pixelShader = new PixelShader(device, pixelShaderByteCode); - - var signature = ShaderSignature.GetInputSignature(vertexShaderByteCode); - - var inputElements = new[] - { - new InputElement("POSITION", 0, Format.R32G32B32A32_Float, 0, 0), - new InputElement("COLOR", 0, Format.R32G32B32A32_Float, 16, 0) - }; - - // Layout from VertexShader input signature - var layout = new InputLayout( - device, - signature, - inputElements); - - // Instantiate Vertex buffer from vertex data - var vertices = Buffer.Create( - device, - BindFlags.VertexBuffer, - new[] - { - new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), // Front - new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), - new Vector4( 1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), - new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), - new Vector4( 1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), - new Vector4( 1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), - - new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), // BACK - new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), - new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), - new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), - new Vector4( 1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), - new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), - - new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), // Top - new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), - new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), - new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), - new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), - new Vector4( 1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), - - new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), // Bottom - new Vector4( 1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), - new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), - new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), - new Vector4( 1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), - new Vector4( 1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), - - new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), // Left - new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), - new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), - new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), - new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), - new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), - - new Vector4( 1.0f, -1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), // Right - new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), - new Vector4( 1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), - new Vector4( 1.0f, -1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), - new Vector4( 1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), - new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), - }); - - // Create Constant Buffer - _contantBuffer = new Buffer(device, Utilities.SizeOf(), ResourceUsage.Default, BindFlags.ConstantBuffer, CpuAccessFlags.None, ResourceOptionFlags.None, 0); - - var context = Direct2D1Platform.Direct3D11Device.ImmediateContext; - - // Prepare All the stages - context.InputAssembler.InputLayout = layout; - context.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList; - context.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(vertices, Utilities.SizeOf() * 2, 0)); - context.VertexShader.SetConstantBuffer(0, _contantBuffer); - context.VertexShader.Set(vertexShader); - context.PixelShader.Set(pixelShader); - } - - private void Resize(Size size) - { - Utilities.Dispose(ref _deviceContext); - Utilities.Dispose(ref _backBuffer); - Utilities.Dispose(ref _renderView); - Utilities.Dispose(ref _depthBuffer); - Utilities.Dispose(ref _depthView); - var context = Direct2D1Platform.Direct3D11Device.ImmediateContext; - - // Resize the backbuffer - _swapChain.ResizeBuffers(0, 0, 0, Format.Unknown, SwapChainFlags.None); - - // Get the backbuffer from the swapchain - _backBuffer = Resource.FromSwapChain(_swapChain, 0); - - // Renderview on the backbuffer - _renderView = new RenderTargetView(Direct2D1Platform.Direct3D11Device, _backBuffer); - - // Create the depth buffer - _depthBuffer = new Texture2D( - Direct2D1Platform.Direct3D11Device, - new Texture2DDescription() - { - Format = Format.D32_Float_S8X24_UInt, - ArraySize = 1, - MipLevels = 1, - Width = (int)size.Width, - Height = (int)size.Height, - SampleDescription = new SampleDescription(1, 0), - Usage = ResourceUsage.Default, - BindFlags = BindFlags.DepthStencil, - CpuAccessFlags = CpuAccessFlags.None, - OptionFlags = ResourceOptionFlags.None - }); - - // Create the depth buffer view - _depthView = new DepthStencilView(Direct2D1Platform.Direct3D11Device, _depthBuffer); - - // Setup targets and viewport for rendering - context.Rasterizer.SetViewport(new Viewport(0, 0, (int)size.Width, (int)size.Height, 0.0f, 1.0f)); - context.OutputMerger.SetTargets(_depthView, _renderView); - - // Setup new projection matrix with correct aspect ratio - _proj = Matrix.PerspectiveFovLH((float)Math.PI / 4.0f, (float)(size.Width / size.Height), 0.1f, 100.0f); - - using (var dxgiBackBuffer = _swapChain.GetBackBuffer(0)) - { - var renderTarget = new SharpDX.Direct2D1.RenderTarget( - Direct2D1Platform.Direct2D1Factory, - dxgiBackBuffer, - new RenderTargetProperties - { - DpiX = 96, - DpiY = 96, - Type = RenderTargetType.Default, - PixelFormat = new PixelFormat( - Format.Unknown, - AlphaMode.Premultiplied) - }); - - _deviceContext = renderTarget.QueryInterface(); - - renderTarget.Dispose(); - } - } - - private class D3DRenderTarget : IRenderTarget - { - private readonly MainWindow _window; - - public D3DRenderTarget(MainWindow window) - { - _window = window; - } - - public void Dispose() - { - } - - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) - { - return new DrawingContextImpl(visualBrushRenderer, null, _window._deviceContext); - } - } - - - protected override IRenderTarget CreateRenderTarget() => new D3DRenderTarget(this); - } -} diff --git a/samples/interop/Direct3DInteropSample/MainWindow.paml b/samples/interop/Direct3DInteropSample/MainWindow.paml deleted file mode 100644 index 37c6265836..0000000000 --- a/samples/interop/Direct3DInteropSample/MainWindow.paml +++ /dev/null @@ -1,14 +0,0 @@ - - - - Rotation X - - Rotation Y - - Rotation Z - - Zoom - - - - \ No newline at end of file diff --git a/samples/interop/Direct3DInteropSample/MainWindowViewModel.cs b/samples/interop/Direct3DInteropSample/MainWindowViewModel.cs deleted file mode 100644 index 21679a99c5..0000000000 --- a/samples/interop/Direct3DInteropSample/MainWindowViewModel.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using MiniMvvm; - -namespace Direct3DInteropSample -{ - public class MainWindowViewModel : ViewModelBase - { - private double _rotationX; - - public double RotationX - { - get { return _rotationX; } - set { this.RaiseAndSetIfChanged(ref _rotationX, value); } - } - - private double _rotationY = 1; - - public double RotationY - { - get { return _rotationY; } - set { this.RaiseAndSetIfChanged(ref _rotationY, value); } - } - - private double _rotationZ = 2; - - public double RotationZ - { - get { return _rotationZ; } - set { this.RaiseAndSetIfChanged(ref _rotationZ, value); } - } - - - private double _zoom = 1; - - public double Zoom - { - get { return _zoom; } - set { this.RaiseAndSetIfChanged(ref _zoom, value); } - } - } -} diff --git a/samples/interop/Direct3DInteropSample/MiniCube.fx b/samples/interop/Direct3DInteropSample/MiniCube.fx deleted file mode 100644 index f246421f2d..0000000000 --- a/samples/interop/Direct3DInteropSample/MiniCube.fx +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2010-2013 SharpDX - Alexandre Mutel -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -struct VS_IN -{ - float4 pos : POSITION; - float4 col : COLOR; -}; - -struct PS_IN -{ - float4 pos : SV_POSITION; - float4 col : COLOR; -}; - -float4x4 worldViewProj; - -PS_IN VS( VS_IN input ) -{ - PS_IN output = (PS_IN)0; - - output.pos = mul(input.pos, worldViewProj); - output.col = input.col; - - return output; -} - -float4 PS( PS_IN input ) : SV_Target -{ - return input.col; -} \ No newline at end of file diff --git a/samples/interop/Direct3DInteropSample/Program.cs b/samples/interop/Direct3DInteropSample/Program.cs deleted file mode 100644 index bf8e76d7e4..0000000000 --- a/samples/interop/Direct3DInteropSample/Program.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Avalonia; - -namespace Direct3DInteropSample -{ - class Program - { - public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() - .With(new Win32PlatformOptions { UseDeferredRendering = false }) - .UseWin32() - .UseDirect2D1(); - - public static int Main(string[] args) - => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); - } -} diff --git a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj index 223a53d8c5..1643ca3ee2 100644 --- a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj +++ b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj @@ -15,6 +15,4 @@ ControlCatalog - - diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 75856e4b52..01b48ffd89 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -15,7 +15,7 @@ namespace Avalonia { public static class AndroidApplicationExtensions { - public static T UseAndroid(this T builder) where T : AppBuilderBase, new() + public static AppBuilder UseAndroid(this AppBuilder builder) { return builder .UseWindowingSubsystem(() => AndroidPlatform.Initialize(), "Android") diff --git a/src/Android/Avalonia.Android/AndroidThreadingInterface.cs b/src/Android/Avalonia.Android/AndroidThreadingInterface.cs index de9149e9a1..152076013f 100644 --- a/src/Android/Avalonia.Android/AndroidThreadingInterface.cs +++ b/src/Android/Avalonia.Android/AndroidThreadingInterface.cs @@ -1,10 +1,10 @@ using System; -using System.Reactive.Disposables; using System.Threading; using Android.OS; using Avalonia.Platform; +using Avalonia.Reactive; using Avalonia.Threading; using App = Android.App.Application; diff --git a/src/Android/Avalonia.Android/AvaloniaSplashActivity.cs b/src/Android/Avalonia.Android/AvaloniaSplashActivity.cs index 5b5ebd1bd9..38038ef26c 100644 --- a/src/Android/Avalonia.Android/AvaloniaSplashActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaSplashActivity.cs @@ -1,6 +1,5 @@ using Android.OS; using AndroidX.AppCompat.App; -using AndroidX.Lifecycle; namespace Avalonia.Android { @@ -8,15 +7,22 @@ namespace Avalonia.Android { protected abstract AppBuilder CreateAppBuilder(); + private static AppBuilder s_appBuilder; + protected override void OnCreate(Bundle? savedInstanceState) { base.OnCreate(savedInstanceState); - var builder = CreateAppBuilder(); + if (s_appBuilder == null) + { + var builder = CreateAppBuilder(); + + var lifetime = new SingleViewLifetime(); - var lifetime = new SingleViewLifetime(); + builder.SetupWithLifetime(lifetime); - builder.SetupWithLifetime(lifetime); + s_appBuilder = builder; + } } } diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index 5267843bfc..f7e32f99db 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -24,6 +24,8 @@ namespace Avalonia.Android _root = new EmbeddableControlRoot(_view); _root.Prepare(); + + this.SetBackgroundColor(global::Android.Graphics.Color.Transparent); } internal TopLevelImpl TopLevelImpl => _view; diff --git a/src/Android/Avalonia.Android/ChoreographerTimer.cs b/src/Android/Avalonia.Android/ChoreographerTimer.cs index 19dc7b4ab6..3545ae8fe1 100644 --- a/src/Android/Avalonia.Android/ChoreographerTimer.cs +++ b/src/Android/Avalonia.Android/ChoreographerTimer.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; using System.Threading.Tasks; using Android.OS; using Android.Views; +using Avalonia.Reactive; using Avalonia.Rendering; using Java.Lang; diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs index aabf8160f8..94e5f4bd01 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs @@ -96,12 +96,14 @@ namespace Avalonia.Android.Platform.SkiaPlatform public IntPtr bits; // Do not touch. +#pragma warning disable CA1823 // Avoid unused private fields uint reserved1; uint reserved2; uint reserved3; uint reserved4; uint reserved5; uint reserved6; +#pragma warning restore CA1823 // Avoid unused private fields } } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs index 33501ece06..f205458f0e 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs @@ -22,6 +22,7 @@ namespace Avalonia.Android public InvalidationAwareSurfaceView(Context context) : base(context) { Holder.AddCallback(this); + Holder.SetFormat(global::Android.Graphics.Format.Transparent); _handler = new Handler(context.MainLooper); } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 56dbadca03..1aac567dda 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -26,6 +26,7 @@ using Avalonia.Rendering.Composition; using Java.Lang; using Math = System.Math; using AndroidRect = Android.Graphics.Rect; +using Android.Graphics.Drawables; namespace Avalonia.Android.Platform.SkiaPlatform { @@ -283,7 +284,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public Action LostFocus { get; set; } public Action TransparencyLevelChanged { get; set; } - public WindowTransparencyLevel TransparencyLevel => WindowTransparencyLevel.None; + public WindowTransparencyLevel TransparencyLevel { get; private set; } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1); @@ -301,7 +302,87 @@ namespace Avalonia.Android.Platform.SkiaPlatform public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { - throw new NotImplementedException(); + if (TransparencyLevel != transparencyLevel) + { + bool isBelowR = Build.VERSION.SdkInt < BuildVersionCodes.R; + bool isAboveR = Build.VERSION.SdkInt > BuildVersionCodes.R; + if (_view.Context is AvaloniaMainActivity activity) + { + switch (transparencyLevel) + { + case WindowTransparencyLevel.AcrylicBlur: + case WindowTransparencyLevel.ForceAcrylicBlur: + case WindowTransparencyLevel.Mica: + case WindowTransparencyLevel.None: + if (!isBelowR) + { + activity.SetTranslucent(false); + } + if (isAboveR) + { + activity.Window?.ClearFlags(WindowManagerFlags.BlurBehind); + + var attr = activity.Window?.Attributes; + if (attr != null) + { + attr.BlurBehindRadius = 0; + + activity.Window.Attributes = attr; + } + } + activity.Window.SetBackgroundDrawable(new ColorDrawable(Color.White)); + + if(transparencyLevel != WindowTransparencyLevel.None) + { + return; + } + break; + case WindowTransparencyLevel.Transparent: + if (!isBelowR) + { + activity.SetTranslucent(true); + } + if (isAboveR) + { + activity.Window?.ClearFlags(WindowManagerFlags.BlurBehind); + + var attr = activity.Window?.Attributes; + if (attr != null) + { + attr.BlurBehindRadius = 0; + + activity.Window.Attributes = attr; + } + } + activity.Window.SetBackgroundDrawable(new ColorDrawable(Color.Transparent)); + break; + case WindowTransparencyLevel.Blur: + if (isAboveR) + { + activity.SetTranslucent(true); + activity.Window?.AddFlags(WindowManagerFlags.BlurBehind); + + var attr = activity.Window?.Attributes; + if (attr != null) + { + attr.BlurBehindRadius = 120; + + activity.Window.Attributes = attr; + } + activity.Window.SetBackgroundDrawable(new ColorDrawable(Color.Transparent)); + } + else + { + activity.Window?.ClearFlags(WindowManagerFlags.BlurBehind); + activity.Window.SetBackgroundDrawable(new ColorDrawable(Color.White)); + + return; + } + break; + } + TransparencyLevel = transparencyLevel; + } + } } } diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidMotionEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidMotionEventsHelper.cs index 6d0e6be0ad..3d0f3b2652 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidMotionEventsHelper.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidMotionEventsHelper.cs @@ -38,7 +38,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers return null; } - var eventTime = (ulong)DateTime.Now.Millisecond; + var eventTime = (ulong)e.EventTime; var inputRoot = _view.InputRoot; var actionMasked = e.ActionMasked; var modifiers = GetModifiers(e.MetaState, e.ButtonState); diff --git a/src/Avalonia.Base/Animation/Animation.cs b/src/Avalonia.Base/Animation/Animation.cs index 06087cdd6a..d62acc0d52 100644 --- a/src/Avalonia.Base/Animation/Animation.cs +++ b/src/Avalonia.Base/Animation/Animation.cs @@ -2,8 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Linq; +using Avalonia.Reactive; using System.Threading; using System.Threading.Tasks; diff --git a/src/Avalonia.Base/Animation/AnimationInstance`1.cs b/src/Avalonia.Base/Animation/AnimationInstance`1.cs index 0881fde988..682629c801 100644 --- a/src/Avalonia.Base/Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Base/Animation/AnimationInstance`1.cs @@ -1,11 +1,8 @@ using System; using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Animation.Animators; -using Avalonia.Animation.Utils; using Avalonia.Data; -using Avalonia.Reactive; -using JetBrains.Annotations; namespace Avalonia.Animation { diff --git a/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs b/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs index 3168a67d79..dcce75b31c 100644 --- a/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs +++ b/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs @@ -63,7 +63,7 @@ namespace Avalonia.Animation } else { - return this.Bind(ValueProperty, ObservableEx.SingleValue(value).ToBinding(), targetControl); + return this.Bind(ValueProperty, Observable.SingleValue(value).ToBinding(), targetControl); } } diff --git a/src/Avalonia.Base/Animation/Animators/Animator`1.cs b/src/Avalonia.Base/Animation/Animators/Animator`1.cs index b5d1feb4a7..2db890bd0a 100644 --- a/src/Avalonia.Base/Animation/Animators/Animator`1.cs +++ b/src/Avalonia.Base/Animation/Animators/Animator`1.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Linq; using Avalonia.Animation.Utils; using Avalonia.Collections; using Avalonia.Data; diff --git a/src/Avalonia.Base/Animation/Animators/ColorAnimator.cs b/src/Avalonia.Base/Animation/Animators/ColorAnimator.cs index 72add21d69..7be974b9e5 100644 --- a/src/Avalonia.Base/Animation/Animators/ColorAnimator.cs +++ b/src/Avalonia.Base/Animation/Animators/ColorAnimator.cs @@ -2,7 +2,7 @@ // and adopted from LottieSharp Project (https://github.com/ascora/LottieSharp). using System; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Logging; using Avalonia.Media; diff --git a/src/Avalonia.Base/Animation/Animators/TransformAnimator.cs b/src/Avalonia.Base/Animation/Animators/TransformAnimator.cs index e12ca722f9..8040fb595b 100644 --- a/src/Avalonia.Base/Animation/Animators/TransformAnimator.cs +++ b/src/Avalonia.Base/Animation/Animators/TransformAnimator.cs @@ -1,5 +1,5 @@ using System; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Logging; using Avalonia.Media; using Avalonia.Media.Transformation; diff --git a/src/Avalonia.Base/Animation/Clock.cs b/src/Avalonia.Base/Animation/Clock.cs index 5afd2ae705..f2bce9d3a5 100644 --- a/src/Avalonia.Base/Animation/Clock.cs +++ b/src/Avalonia.Base/Animation/Clock.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Reactive; namespace Avalonia.Animation { diff --git a/src/Avalonia.Base/Animation/CrossFade.cs b/src/Avalonia.Base/Animation/CrossFade.cs index a229bc7ce6..640d6456a3 100644 --- a/src/Avalonia.Base/Animation/CrossFade.cs +++ b/src/Avalonia.Base/Animation/CrossFade.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; +using Avalonia.Reactive; using System.Threading; using System.Threading.Tasks; using Avalonia.Animation.Easings; @@ -108,7 +108,7 @@ namespace Avalonia.Animation } var tasks = new List(); - using (var disposables = new CompositeDisposable()) + using (var disposables = new CompositeDisposable(1)) { if (to != null) { diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 0d3da66f7a..cd122a8b67 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -14,8 +14,6 @@ - - @@ -29,12 +27,22 @@ + + + + + + + + + + @@ -46,8 +54,12 @@ + + + + diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index a3a732428e..dc94dfba40 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -621,7 +621,7 @@ namespace Avalonia /// The old property value. /// The new property value. /// The priority of the binding that produced the value. - private protected void RaisePropertyChanged( + protected void RaisePropertyChanged( DirectPropertyBase property, Optional oldValue, BindingValue newValue, diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 867d6215a5..7b17b9152d 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -1,10 +1,6 @@ using System; -using System.Reactive; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Avalonia.Data; using Avalonia.Reactive; +using Avalonia.Data; namespace Avalonia { @@ -127,108 +123,6 @@ namespace Avalonia property ?? throw new ArgumentNullException(nameof(property))); } - /// - /// Gets a subject for an . - /// - /// The object. - /// The property. - /// - /// The priority with which binding values are written to the object. - /// - /// - /// An which can be used for two-way binding to/from the - /// property. - /// - public static ISubject GetSubject( - this AvaloniaObject o, - AvaloniaProperty property, - BindingPriority priority = BindingPriority.LocalValue) - { - return Subject.Create( - Observer.Create(x => o.SetValue(property, x, priority)), - o.GetObservable(property)); - } - - /// - /// Gets a subject for an . - /// - /// The property type. - /// The object. - /// The property. - /// - /// The priority with which binding values are written to the object. - /// - /// - /// An which can be used for two-way binding to/from the - /// property. - /// - public static ISubject GetSubject( - this AvaloniaObject o, - AvaloniaProperty property, - BindingPriority priority = BindingPriority.LocalValue) - { - return Subject.Create( - Observer.Create(x => o.SetValue(property, x, priority)), - o.GetObservable(property)); - } - - /// - /// Gets a subject for a . - /// - /// The object. - /// The property. - /// - /// The priority with which binding values are written to the object. - /// - /// - /// An which can be used for two-way binding to/from the - /// property. - /// - public static ISubject> GetBindingSubject( - this AvaloniaObject o, - AvaloniaProperty property, - BindingPriority priority = BindingPriority.LocalValue) - { - return Subject.Create>( - Observer.Create>(x => - { - if (x.HasValue) - { - o.SetValue(property, x.Value, priority); - } - }), - o.GetBindingObservable(property)); - } - - /// - /// Gets a subject for a . - /// - /// The property type. - /// The object. - /// The property. - /// - /// The priority with which binding values are written to the object. - /// - /// - /// An which can be used for two-way binding to/from the - /// property. - /// - public static ISubject> GetBindingSubject( - this AvaloniaObject o, - AvaloniaProperty property, - BindingPriority priority = BindingPriority.LocalValue) - { - return Subject.Create>( - Observer.Create>(x => - { - if (x.HasValue) - { - o.SetValue(property, x.Value, priority); - } - }), - o.GetBindingObservable(property)); - } - /// /// Binds an to an observable. /// @@ -407,13 +301,7 @@ namespace Avalonia Action action) where TTarget : AvaloniaObject { - return observable.Subscribe(e => - { - if (e.Sender is TTarget target) - { - action(target, e); - } - }); + return observable.Subscribe(new ClassHandlerObserver(action)); } /// @@ -431,13 +319,7 @@ namespace Avalonia this IObservable> observable, Action> action) where TTarget : AvaloniaObject { - return observable.Subscribe(e => - { - if (e.Sender is TTarget target) - { - action(target, e); - } - }); + return observable.Subscribe(new ClassHandlerObserver(action)); } private class BindingAdaptor : IBinding @@ -458,5 +340,57 @@ namespace Avalonia return InstancedBinding.OneWay(_source); } } + + private class ClassHandlerObserver : IObserver> + { + private readonly Action> _action; + + public ClassHandlerObserver(Action> action) + { + _action = action; + } + + public void OnCompleted() + { + } + + public void OnError(Exception error) + { + } + + public void OnNext(AvaloniaPropertyChangedEventArgs value) + { + if (value.Sender is TTarget target) + { + _action(target, value); + } + } + } + + private class ClassHandlerObserver : IObserver + { + private readonly Action _action; + + public ClassHandlerObserver(Action action) + { + _action = action; + } + + public void OnCompleted() + { + } + + public void OnError(Exception error) + { + } + + public void OnNext(AvaloniaPropertyChangedEventArgs value) + { + if (value.Sender is TTarget target) + { + _action(target, value); + } + } + } } } diff --git a/src/Avalonia.Base/AvaloniaProperty`1.cs b/src/Avalonia.Base/AvaloniaProperty`1.cs index 53444ee475..f8c062a176 100644 --- a/src/Avalonia.Base/AvaloniaProperty`1.cs +++ b/src/Avalonia.Base/AvaloniaProperty`1.cs @@ -1,7 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Reactive.Subjects; using Avalonia.Data; +using Avalonia.Reactive; using Avalonia.Utilities; namespace Avalonia @@ -12,7 +12,7 @@ namespace Avalonia /// The value type of the property. public abstract class AvaloniaProperty : AvaloniaProperty { - private readonly Subject> _changed; + private readonly LightweightSubject> _changed; /// /// Initializes a new instance of the class. @@ -28,7 +28,7 @@ namespace Avalonia Action? notifying = null) : base(name, typeof(TValue), ownerType, metadata, notifying) { - _changed = new Subject>(); + _changed = new LightweightSubject>(); } /// diff --git a/src/Avalonia.Base/ClassBindingManager.cs b/src/Avalonia.Base/ClassBindingManager.cs index 589b9b2d01..a9726cb86e 100644 --- a/src/Avalonia.Base/ClassBindingManager.cs +++ b/src/Avalonia.Base/ClassBindingManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Avalonia.Data; +using Avalonia.Reactive; namespace Avalonia { diff --git a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs index c4684960d6..2178577eb7 100644 --- a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs +++ b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs @@ -3,7 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; -using System.Reactive.Disposables; +using Avalonia.Reactive; namespace Avalonia.Collections { diff --git a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs index 689fcc89a4..68863ea257 100644 --- a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs +++ b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Specialized; -using System.Reactive.Linq; using Avalonia.Reactive; using Avalonia.Utilities; diff --git a/src/Avalonia.Base/Compatibility/OperatingSystem.cs b/src/Avalonia.Base/Compatibility/OperatingSystem.cs new file mode 100644 index 0000000000..838f7da8b2 --- /dev/null +++ b/src/Avalonia.Base/Compatibility/OperatingSystem.cs @@ -0,0 +1,26 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Compatibility +{ + internal sealed class OperatingSystemEx + { +#if NET6_0_OR_GREATER + public static bool IsWindows() => OperatingSystem.IsWindows(); + public static bool IsMacOS() => OperatingSystem.IsMacOS(); + public static bool IsLinux() => OperatingSystem.IsLinux(); + public static bool IsAndroid() => OperatingSystem.IsAndroid(); + public static bool IsIOS() => OperatingSystem.IsIOS(); + public static bool IsBrowser() => OperatingSystem.IsBrowser(); + public static bool IsOSPlatform(string platform) => OperatingSystem.IsOSPlatform(platform); +#else + public static bool IsWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + public static bool IsMacOS() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + public static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + public static bool IsAndroid() => IsOSPlatform("ANDROID"); + public static bool IsIOS() => IsOSPlatform("IOS"); + public static bool IsBrowser() => IsOSPlatform("BROWSER"); + public static bool IsOSPlatform(string platform) => RuntimeInformation.IsOSPlatform(OSPlatform.Create(platform)); +#endif + } +} diff --git a/src/Avalonia.Base/Contract.cs b/src/Avalonia.Base/Contract.cs deleted file mode 100644 index 27427700ac..0000000000 --- a/src/Avalonia.Base/Contract.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using JetBrains.Annotations; - -namespace Avalonia -{ - /// - /// A stub of Code Contract's Contract class. - /// - /// - /// It would be nice to use Code Contracts on Avalonia but last time I tried it slowed things - /// to a crawl and often crashed. Instead use the same signature for checking preconditions - /// in the hope that it might become usable at some point. - /// - public static class Contract - { - /// - /// Specifies a precondition. - /// - /// - /// The exception to throw if is false. - /// - /// The precondition. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [ContractAnnotation("condition:false=>stop")] - public static void Requires(bool condition) where TException : Exception, new() - { - if (!condition) - { - throw new TException(); - } - } - } -} diff --git a/src/Avalonia.Base/Controls/NameScopeExtensions.cs b/src/Avalonia.Base/Controls/NameScopeExtensions.cs index 3895b6ceb9..0abd5e56f7 100644 --- a/src/Avalonia.Base/Controls/NameScopeExtensions.cs +++ b/src/Avalonia.Base/Controls/NameScopeExtensions.cs @@ -25,13 +25,18 @@ namespace Avalonia.Controls var result = nameScope.Find(name); - if (result != null && !(result is T)) + if (result == null) { - throw new InvalidOperationException( - $"Expected control '{name}' to be '{typeof(T)} but it was '{result.GetType()}'."); + return null; } - return (T?)result; + if (result is T typed) + { + return typed; + } + + throw new InvalidOperationException( + $"Expected control '{name}' to be '{typeof(T)} but it was '{result.GetType()}'."); } /// @@ -74,13 +79,13 @@ namespace Avalonia.Controls throw new KeyNotFoundException($"Could not find control '{name}'."); } - if (!(result is T)) + if (result is T typed) { - throw new InvalidOperationException( - $"Expected control '{name}' to be '{typeof(T)} but it was '{result.GetType()}'."); + return typed; } - return (T)result; + throw new InvalidOperationException( + $"Expected control '{name}' to be '{typeof(T)} but it was '{result.GetType()}'."); } /// diff --git a/src/Avalonia.Base/Controls/NameScopeLocator.cs b/src/Avalonia.Base/Controls/NameScopeLocator.cs index f0ce7f8a5b..371931f971 100644 --- a/src/Avalonia.Base/Controls/NameScopeLocator.cs +++ b/src/Avalonia.Base/Controls/NameScopeLocator.cs @@ -1,5 +1,5 @@ using System; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Utilities; namespace Avalonia.Controls diff --git a/src/Avalonia.Base/CornerRadius.cs b/src/Avalonia.Base/CornerRadius.cs index d56a0ef19d..1666fac2e1 100644 --- a/src/Avalonia.Base/CornerRadius.cs +++ b/src/Avalonia.Base/CornerRadius.cs @@ -61,9 +61,13 @@ namespace Avalonia public double BottomLeft { get; } /// - /// Gets a value indicating whether all corner radii are set to 0. + /// Gets a value indicating whether the instance has default values (all corner radii are set to 0). /// - public bool IsEmpty => TopLeft.Equals(0) && IsUniform; + public bool IsDefault => TopLeft == 0 && TopRight == 0 && BottomLeft == 0 && BottomRight == 0; + + /// + [Obsolete("Use IsDefault instead.")] + public bool IsEmpty => IsDefault; /// /// Gets a value indicating whether all corner radii are equal. @@ -79,7 +83,6 @@ namespace Avalonia { // ReSharper disable CompareOfFloatsByEqualityOperator return TopLeft == other.TopLeft && - TopRight == other.TopRight && BottomRight == other.BottomRight && BottomLeft == other.BottomLeft; diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index ceb3f71285..0b737dd959 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -1,6 +1,5 @@ using System; -using System.Reactive.Disposables; -using System.Reactive.Linq; +using Avalonia.Reactive; namespace Avalonia.Data { @@ -46,15 +45,15 @@ namespace Avalonia.Data throw new InvalidOperationException("InstancedBinding does not contain an observable."); return target.Bind(property, binding.Observable, binding.Priority); case BindingMode.TwoWay: + if (binding.Observable is null) + throw new InvalidOperationException("InstancedBinding does not contain an observable."); if (binding.Subject is null) throw new InvalidOperationException("InstancedBinding does not contain a subject."); return new TwoWayBindingDisposable( - target.Bind(property, binding.Subject, binding.Priority), + target.Bind(property, binding.Observable, binding.Priority), target.GetObservable(property).Subscribe(binding.Subject)); case BindingMode.OneTime: - var source = binding.Subject ?? binding.Observable; - - if (source != null) + if (binding.Observable is {} source) { // Perf: Avoid allocating closure in the outer scope. var targetCopy = target; diff --git a/src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs b/src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs index 2577cac743..5084d8e822 100644 --- a/src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs @@ -38,7 +38,7 @@ namespace Avalonia.Data.Converters } else if (Equals(obj, default(TIn))) { - yield return default(TIn); + yield return default; } } } diff --git a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs index e340966983..536c14dcf9 100644 --- a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs @@ -1,5 +1,4 @@ using System; -using System.Reactive.Linq; using Avalonia.Reactive; namespace Avalonia.Data.Core diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index f60b4722d9..55caf8070e 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -1,11 +1,9 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Reactive.Linq; -using System.Reactive.Subjects; +using Avalonia.Reactive; using Avalonia.Data.Converters; using Avalonia.Logging; -using Avalonia.Reactive; using Avalonia.Utilities; namespace Avalonia.Data.Core @@ -15,7 +13,7 @@ namespace Avalonia.Data.Core /// that are sent and received. /// [RequiresUnreferencedCode(TrimmingMessages.TypeConvertionRequiresUnreferencedCodeMessage)] - public class BindingExpression : LightweightObservableBase, ISubject, IDescription + public class BindingExpression : LightweightObservableBase, IAvaloniaSubject, IDescription { private readonly ExpressionObserver _inner; private readonly Type _targetType; diff --git a/src/Avalonia.Base/Data/Core/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNode.cs index e4b833176c..5fb2bb5c13 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNode.cs @@ -4,8 +4,6 @@ namespace Avalonia.Data.Core { public abstract class ExpressionNode { - private static readonly object CacheInvalid = new object(); - protected static readonly WeakReference UnsetReference = new WeakReference(AvaloniaProperty.UnsetValue); diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 0a9f834aeb..0818b5fa62 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -2,8 +2,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; -using System.Reactive; -using System.Reactive.Linq; using Avalonia.Data.Core.Parsers; using Avalonia.Data.Core.Plugins; using Avalonia.Reactive; @@ -49,8 +47,6 @@ namespace Avalonia.Data.Core new TaskStreamPlugin(), new ObservableStreamPlugin(), }; - - private static readonly object UninitializedValue = new object(); private readonly ExpressionNode _node; private object? _root; private Func? _rootGetter; @@ -101,14 +97,14 @@ namespace Avalonia.Data.Core /// /// A function which gets the root object. /// The expression. - /// An observable which triggers a re-read of the getter. + /// An observable which triggers a re-read of the getter. Generic argument value is not used. /// /// A description of the expression. /// public ExpressionObserver( Func rootGetter, ExpressionNode node, - IObservable update, + IObservable update, string? description) { Description = description; @@ -166,7 +162,7 @@ namespace Avalonia.Data.Core /// /// A function which gets the root object. /// The expression. - /// An observable which triggers a re-read of the getter. + /// An observable which triggers a re-read of the getter. Generic argument value is not used. /// Whether or not to track data validation /// /// A description of the expression. If null, 's string representation will be used. @@ -175,7 +171,7 @@ namespace Avalonia.Data.Core public static ExpressionObserver Create( Func rootGetter, Expression> expression, - IObservable update, + IObservable update, bool enableDataValidation = false, string? description = null) { @@ -298,9 +294,10 @@ namespace Avalonia.Data.Core if (_root is IObservable observable) { _rootSubscription = observable.Subscribe( - x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null), - x => PublishCompleted(), - () => PublishCompleted()); + new AnonymousObserver( + x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null), + x => PublishCompleted(), + PublishCompleted)); } else { diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs index a808827896..2fad96701d 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs @@ -1,48 +1,47 @@ using System; using System.Collections; -using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Utilities; namespace Avalonia.Data.Core { - public abstract class IndexerNodeBase : SettableNode + public abstract class IndexerNodeBase : SettableNode, + IWeakEventSubscriber, + IWeakEventSubscriber { - private IDisposable? _subscription; - protected override void StartListeningCore(WeakReference reference) { reference.TryGetTarget(out var target); - var incc = target as INotifyCollectionChanged; - var inpc = target as INotifyPropertyChanged; - var inputs = new List>(); - - if (incc != null) + if (target is INotifyCollectionChanged incc) { - inputs.Add(WeakObservable.FromEventPattern( - incc, WeakEvents.CollectionChanged) - .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) - .Select(_ => GetValue(target))); + WeakEvents.CollectionChanged.Subscribe(incc, this); } - if (inpc != null) + if (target is INotifyPropertyChanged inpc) { - inputs.Add(WeakObservable.FromEventPattern( - inpc, WeakEvents.PropertyChanged) - .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) - .Select(_ => GetValue(target))); + WeakEvents.PropertyChanged.Subscribe(inpc, this); } - - _subscription = Observable.Merge(inputs).StartWith(GetValue(target)).Subscribe(ValueChanged); + + ValueChanged(GetValue(target)); } protected override void StopListeningCore() { - _subscription?.Dispose(); + if (Target.TryGetTarget(out var target)) + { + if (target is INotifyCollectionChanged incc) + { + WeakEvents.CollectionChanged.Unsubscribe(incc, this); + } + + if (target is INotifyPropertyChanged inpc) + { + WeakEvents.PropertyChanged.Unsubscribe(inpc, this); + } + } } protected abstract object? GetValue(object? target); @@ -83,5 +82,21 @@ namespace Avalonia.Data.Core } protected abstract bool ShouldUpdate(object? sender, PropertyChangedEventArgs e); + + void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, NotifyCollectionChangedEventArgs e) + { + if (ShouldUpdate(sender, e)) + { + ValueChanged(GetValue(sender)); + } + } + + void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, PropertyChangedEventArgs e) + { + if (ShouldUpdate(sender, e)) + { + ValueChanged(GetValue(sender)); + } + } } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs index ebee4586db..b40628fd35 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs @@ -1,7 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using System.Reflection; namespace Avalonia.Data.Core.Plugins @@ -12,8 +12,15 @@ namespace Avalonia.Data.Core.Plugins [UnconditionalSuppressMessage("Trimming", "IL3050", Justification = TrimmingMessages.IgnoreNativeAotSupressWarningMessage)] public class ObservableStreamPlugin : IStreamPlugin { - static MethodInfo? observableSelect; + private static MethodInfo? s_observableGeneric; + private static MethodInfo? s_observableSelect; + [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicProperties, "Avalonia.Data.Core.Plugins.ObservableStreamPlugin", "Avalonia.Base")] + public ObservableStreamPlugin() + { + + } + /// /// Checks whether this plugin handles the specified value. /// @@ -54,56 +61,32 @@ namespace Avalonia.Data.Core.Plugins x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IObservable<>)).GetGenericArguments()[0]; - // Get the Observable.Select method. - var select = GetObservableSelect(sourceType); - - // Make a Box<> delegate of the correct type. - var funcType = typeof(Func<,>).MakeGenericType(sourceType, typeof(object)); - var box = GetType().GetMethod(nameof(Box), BindingFlags.Static | BindingFlags.NonPublic)! - .MakeGenericMethod(sourceType) - .CreateDelegate(funcType); + // Get the BoxObservable method. + var select = GetBoxObservable(sourceType); - // Call Observable.Select(target, box); + // Call BoxObservable(target); return (IObservable)select.Invoke( null, - new object[] { target, box })!; + new[] { target })!; } [RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)] - private static MethodInfo GetObservableSelect(Type source) + private static MethodInfo GetBoxObservable(Type source) { - return GetObservableSelect().MakeGenericMethod(source, typeof(object)); + return (s_observableGeneric ??= GetBoxObservable()).MakeGenericMethod(source); } - private static MethodInfo GetObservableSelect() + [RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)] + private static MethodInfo GetBoxObservable() { - if (observableSelect == null) - { - observableSelect = typeof(Observable).GetRuntimeMethods().First(x => - { - if (x.Name == nameof(Observable.Select) && - x.ContainsGenericParameters && - x.GetGenericArguments().Length == 2) - { - var parameters = x.GetParameters(); - - if (parameters.Length == 2 && - parameters[0].ParameterType.IsConstructedGenericType && - parameters[0].ParameterType.GetGenericTypeDefinition() == typeof(IObservable<>) && - parameters[1].ParameterType.IsConstructedGenericType && - parameters[1].ParameterType.GetGenericTypeDefinition() == typeof(Func<,>)) - { - return true; - } - } - - return false; - }); - } - - return observableSelect; + return s_observableSelect + ??= typeof(ObservableStreamPlugin).GetMethod(nameof(BoxObservable), BindingFlags.Static | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("BoxObservable method was not found."); } - private static object? Box(T value) => (object?)value; + private static IObservable BoxObservable(IObservable source) + { + return source.Select(v => (object?)v); + } } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs index 5203aa9f57..715f4604cf 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs @@ -1,9 +1,8 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Reactive.Linq; -using System.Reactive.Subjects; using System.Reflection; using System.Threading.Tasks; +using Avalonia.Reactive; namespace Avalonia.Data.Core.Plugins { @@ -50,7 +49,7 @@ namespace Avalonia.Data.Core.Plugins case TaskStatus.Faulted: return HandleCompleted(task); default: - var subject = new Subject(); + var subject = new LightweightSubject(); task.ContinueWith( x => HandleCompleted(task).Subscribe(subject), TaskScheduler.FromCurrentSynchronizationContext()) diff --git a/src/Avalonia.Base/Data/Core/StreamNode.cs b/src/Avalonia.Base/Data/Core/StreamNode.cs index 6dc6d07184..ba18a2173b 100644 --- a/src/Avalonia.Base/Data/Core/StreamNode.cs +++ b/src/Avalonia.Base/Data/Core/StreamNode.cs @@ -1,7 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Reactive.Linq; using Avalonia.Data.Core.Plugins; +using Avalonia.Reactive; namespace Avalonia.Data.Core { diff --git a/src/Avalonia.Base/Data/IndexerBinding.cs b/src/Avalonia.Base/Data/IndexerBinding.cs index fcd179b9b2..83ef8f76b4 100644 --- a/src/Avalonia.Base/Data/IndexerBinding.cs +++ b/src/Avalonia.Base/Data/IndexerBinding.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Data +using Avalonia.Reactive; + +namespace Avalonia.Data { public class IndexerBinding : IBinding { @@ -22,7 +24,10 @@ object? anchor = null, bool enableDataValidation = false) { - return new InstancedBinding(Source.GetSubject(Property), Mode, BindingPriority.LocalValue); + var subject = new CombinedSubject( + new AnonymousObserver(x => Source.SetValue(Property, x, BindingPriority.LocalValue)), + Source.GetObservable(Property)); + return new InstancedBinding(subject, Mode, BindingPriority.LocalValue); } } } diff --git a/src/Avalonia.Base/Data/IndexerDescriptor.cs b/src/Avalonia.Base/Data/IndexerDescriptor.cs index c823630d3c..3eb5a6ef12 100644 --- a/src/Avalonia.Base/Data/IndexerDescriptor.cs +++ b/src/Avalonia.Base/Data/IndexerDescriptor.cs @@ -1,12 +1,11 @@ using System; -using System.Reactive; namespace Avalonia.Data { /// /// Holds a description of a binding for 's [] operator. /// - public class IndexerDescriptor : ObservableBase, IDescription + public class IndexerDescriptor : IObservable, IDescription { /// /// Gets or sets the binding mode. @@ -104,7 +103,7 @@ namespace Avalonia.Data } /// - protected override IDisposable SubscribeCore(IObserver observer) + public IDisposable Subscribe(IObserver observer) { if (SourceObservable is null && Source is null) throw new InvalidOperationException("Cannot subscribe to IndexerDescriptor."); diff --git a/src/Avalonia.Base/Data/InstancedBinding.cs b/src/Avalonia.Base/Data/InstancedBinding.cs index a349486bf8..a60c1d72ec 100644 --- a/src/Avalonia.Base/Data/InstancedBinding.cs +++ b/src/Avalonia.Base/Data/InstancedBinding.cs @@ -1,5 +1,5 @@ using System; -using System.Reactive.Subjects; +using Avalonia.Reactive; namespace Avalonia.Data { @@ -14,28 +14,7 @@ namespace Avalonia.Data /// public class InstancedBinding { - /// - /// Initializes a new instance of the class. - /// - /// The binding source. - /// The binding mode. - /// 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 - /// 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. - /// - public InstancedBinding(ISubject subject, BindingMode mode, BindingPriority priority) - { - Contract.Requires(subject != null); - - Mode = mode; - Priority = priority; - Value = subject; - } - - private InstancedBinding(object? value, BindingMode mode, BindingPriority priority) + internal InstancedBinding(object? value, BindingMode mode, BindingPriority priority) { Mode = mode; Priority = priority; @@ -63,9 +42,14 @@ namespace Avalonia.Data public IObservable? Observable => Value as IObservable; /// - /// Gets the as a subject. + /// Gets the as an observer. /// - public ISubject? Subject => Value as ISubject; + public IObserver? Observer => Value as IObserver; + + /// + /// Gets the as an subject. + /// + internal IAvaloniaSubject? Subject => Value as IAvaloniaSubject; /// /// Creates a new one-time binding with a fixed value. @@ -113,30 +97,34 @@ namespace Avalonia.Data /// /// Creates a new one-way to source binding. /// - /// The binding source. + /// The binding source. /// The priority of the binding. /// An instance. public static InstancedBinding OneWayToSource( - ISubject subject, + IObserver observer, BindingPriority priority = BindingPriority.LocalValue) { - _ = subject ?? throw new ArgumentNullException(nameof(subject)); + _ = observer ?? throw new ArgumentNullException(nameof(observer)); - return new InstancedBinding(subject, BindingMode.OneWayToSource, priority); + return new InstancedBinding(observer, BindingMode.OneWayToSource, priority); } /// /// Creates a new two-way binding. /// - /// The binding source. + /// The binding source. + /// The binding source. /// The priority of the binding. /// An instance. public static InstancedBinding TwoWay( - ISubject subject, + IObservable observable, + IObserver observer, BindingPriority priority = BindingPriority.LocalValue) { - _ = subject ?? throw new ArgumentNullException(nameof(subject)); + _ = observable ?? throw new ArgumentNullException(nameof(observable)); + _ = observer ?? throw new ArgumentNullException(nameof(observer)); + var subject = new CombinedSubject(observer, observable); return new InstancedBinding(subject, BindingMode.TwoWay, priority); } diff --git a/src/Avalonia.Base/Input/Cursor.cs b/src/Avalonia.Base/Input/Cursor.cs index 8e79206f93..c555087879 100644 --- a/src/Avalonia.Base/Input/Cursor.cs +++ b/src/Avalonia.Base/Input/Cursor.cs @@ -71,8 +71,7 @@ namespace Avalonia.Input private static ICursorFactory GetCursorFactory() { - return AvaloniaLocator.Current.GetService() ?? - throw new Exception("Could not create Cursor: ICursorFactory not registered."); + return AvaloniaLocator.Current.GetRequiredService(); } } } diff --git a/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs new file mode 100644 index 0000000000..eea7c3b7d1 --- /dev/null +++ b/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs @@ -0,0 +1,128 @@ +using Avalonia.Input.GestureRecognizers; + +namespace Avalonia.Input +{ + public class PinchGestureRecognizer : StyledElement, IGestureRecognizer + { + private IInputElement? _target; + private IGestureRecognizerActionsDispatcher? _actions; + private float _initialDistance; + private IPointer? _firstContact; + private Point _firstPoint; + private IPointer? _secondContact; + private Point _secondPoint; + private Point _origin; + + public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions) + { + _target = target; + _actions = actions; + } + + private void OnPointerPressed(object? sender, PointerPressedEventArgs e) + { + PointerPressed(e); + } + + private void OnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + PointerReleased(e); + } + + public void PointerCaptureLost(IPointer pointer) + { + RemoveContact(pointer); + } + + public void PointerMoved(PointerEventArgs e) + { + if (_target != null && _target is Visual visual) + { + if(_firstContact == e.Pointer) + { + _firstPoint = e.GetPosition(visual); + } + else if (_secondContact == e.Pointer) + { + _secondPoint = e.GetPosition(visual); + } + else + { + return; + } + + if (_firstContact != null && _secondContact != null) + { + var distance = GetDistance(_firstPoint, _secondPoint); + + var scale = distance / _initialDistance; + + _target?.RaiseEvent(new PinchEventArgs(scale, _origin)); + } + } + } + + public void PointerPressed(PointerPressedEventArgs e) + { + if (_target != null && _target is Visual visual && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) + { + if (_firstContact == null) + { + _firstContact = e.Pointer; + _firstPoint = e.GetPosition(visual); + + return; + } + else if (_secondContact == null && _firstContact != e.Pointer) + { + _secondContact = e.Pointer; + _secondPoint = e.GetPosition(visual); + } + else + { + return; + } + + if (_firstContact != null && _secondContact != null) + { + _initialDistance = GetDistance(_firstPoint, _secondPoint); + + _origin = new Point((_firstPoint.X + _secondPoint.X) / 2.0f, (_firstPoint.Y + _secondPoint.Y) / 2.0f); + + _actions!.Capture(_firstContact, this); + _actions!.Capture(_secondContact, this); + } + } + } + + public void PointerReleased(PointerReleasedEventArgs e) + { + RemoveContact(e.Pointer); + } + + private void RemoveContact(IPointer pointer) + { + if (_firstContact == pointer || _secondContact == pointer) + { + if (_secondContact == pointer) + { + _secondContact = null; + } + + if (_firstContact == pointer) + { + _firstContact = _secondContact; + + _secondContact = null; + } + _target?.RaiseEvent(new PinchEndedEventArgs()); + } + } + + private float GetDistance(Point a, Point b) + { + var length = _secondPoint - _firstPoint; + return (float)new Vector(length.X, length.Y).Length; + } + } +} diff --git a/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs index fedd07ec32..23bab13fc8 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs @@ -1,15 +1,19 @@ -using Avalonia.Input.GestureRecognizers; +using System; +using Avalonia.Input.GestureRecognizers; namespace Avalonia.Input { public class PullGestureRecognizer : StyledElement, IGestureRecognizer { + internal static int MinPullDetectionSize = 50; + private IInputElement? _target; private IGestureRecognizerActionsDispatcher? _actions; private Point _initialPosition; private int _gestureId; private IPointer? _tracking; private PullDirection _pullDirection; + private bool _pullInProgress; /// /// Defines the property. @@ -31,23 +35,12 @@ namespace Avalonia.Input PullDirection = pullDirection; } + public PullGestureRecognizer() { } + public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions) { _target = target; _actions = actions; - - _target?.AddHandler(InputElement.PointerPressedEvent, OnPointerPressed, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble); - _target?.AddHandler(InputElement.PointerReleasedEvent, OnPointerReleased, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble); - } - - private void OnPointerPressed(object? sender, PointerPressedEventArgs e) - { - PointerPressed(e); - } - - private void OnPointerReleased(object? sender, PointerReleasedEventArgs e) - { - PointerReleased(e); } public void PointerCaptureLost(IPointer pointer) @@ -94,6 +87,7 @@ namespace Avalonia.Input break; } + _pullInProgress = true; _target?.RaiseEvent(new PullGestureEventArgs(_gestureId, delta, PullDirection)); } } @@ -111,16 +105,16 @@ namespace Avalonia.Input switch (PullDirection) { case PullDirection.TopToBottom: - canPull = position.Y < bounds.Height * 0.1; + canPull = position.Y < Math.Max(MinPullDetectionSize, bounds.Height * 0.1); break; case PullDirection.BottomToTop: - canPull = position.Y > bounds.Height - (bounds.Height * 0.1); + canPull = position.Y > Math.Min(bounds.Height - MinPullDetectionSize, bounds.Height - (bounds.Height * 0.1)); break; case PullDirection.LeftToRight: - canPull = position.X < bounds.Width * 0.1; + canPull = position.X < Math.Max(MinPullDetectionSize, bounds.Width * 0.1); break; case PullDirection.RightToLeft: - canPull = position.X > bounds.Width - (bounds.Width * 0.1); + canPull = position.X > Math.Min(bounds.Width - MinPullDetectionSize, bounds.Width - (bounds.Width * 0.1)); break; } @@ -135,7 +129,7 @@ namespace Avalonia.Input public void PointerReleased(PointerReleasedEventArgs e) { - if (_tracking == e.Pointer) + if (_tracking == e.Pointer && _pullInProgress) { EndPull(); } @@ -145,6 +139,7 @@ namespace Avalonia.Input { _tracking = null; _initialPosition = default; + _pullInProgress = false; _target?.RaiseEvent(new PullGestureEndedEventArgs(_gestureId, PullDirection)); } diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs index 7bcb81767d..790439245a 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -16,7 +16,10 @@ namespace Avalonia.Input.GestureRecognizers private bool _canHorizontallyScroll; private bool _canVerticallyScroll; private int _gestureId; - + private int _scrollStartDistance = 30; + private Point _pointerPressedPoint; + private VelocityTracker? _velocityTracker; + // Movement per second private Vector _inertia; private ulong? _lastMoveTimestamp; @@ -38,6 +41,15 @@ namespace Avalonia.Input.GestureRecognizers nameof(CanVerticallyScroll), o => o.CanVerticallyScroll, (o, v) => o.CanVerticallyScroll = v); + + /// + /// Defines the property. + /// + public static readonly DirectProperty ScrollStartDistanceProperty = + AvaloniaProperty.RegisterDirect( + nameof(ScrollStartDistance), + o => o.ScrollStartDistance, + (o, v) => o.ScrollStartDistance = v); /// /// Gets or sets a value indicating whether the content can be scrolled horizontally. @@ -56,6 +68,15 @@ namespace Avalonia.Input.GestureRecognizers get => _canVerticallyScroll; set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value); } + + /// + /// Gets or sets a value indicating the distance the pointer moves before scrolling is started + /// + public int ScrollStartDistance + { + get => _scrollStartDistance; + set => SetAndRaise(ScrollStartDistanceProperty, ref _scrollStartDistance, value); + } public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions) @@ -72,12 +93,9 @@ namespace Avalonia.Input.GestureRecognizers EndGesture(); _tracking = e.Pointer; _gestureId = ScrollGestureEventArgs.GetNextFreeId(); - _trackedRootPoint = e.GetPosition((Visual?)_target); + _trackedRootPoint = _pointerPressedPoint = e.GetPosition((Visual?)_target); } } - - // Arbitrary chosen value, probably need to move that to platform settings or something - private const double ScrollStartDistance = 30; // Pixels per second speed that is considered to be the stop of inertial scroll private const double InertialScrollSpeedEnd = 5; @@ -95,6 +113,13 @@ namespace Avalonia.Input.GestureRecognizers _scrolling = true; if (_scrolling) { + _velocityTracker = new VelocityTracker(); + + // Correct _trackedRootPoint with ScrollStartDistance, so scrolling does not start with a skip of ScrollStartDistance + _trackedRootPoint = new Point( + _trackedRootPoint.X - (_trackedRootPoint.X >= rootPoint.X ? _scrollStartDistance : -_scrollStartDistance), + _trackedRootPoint.Y - (_trackedRootPoint.Y >= rootPoint.Y ? _scrollStartDistance : -_scrollStartDistance)); + _actions!.Capture(e.Pointer, this); } } @@ -102,14 +127,11 @@ namespace Avalonia.Input.GestureRecognizers if (_scrolling) { var vector = _trackedRootPoint - rootPoint; - var elapsed = _lastMoveTimestamp.HasValue && _lastMoveTimestamp < e.Timestamp ? - TimeSpan.FromMilliseconds(e.Timestamp - _lastMoveTimestamp.Value) : - TimeSpan.Zero; - + + _velocityTracker?.AddPosition(TimeSpan.FromMilliseconds(e.Timestamp), _pointerPressedPoint - rootPoint); + _lastMoveTimestamp = e.Timestamp; _trackedRootPoint = rootPoint; - if (elapsed.TotalSeconds > 0) - _inertia = vector / elapsed.TotalSeconds; _target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector)); e.Handled = true; } @@ -134,12 +156,14 @@ namespace Avalonia.Input.GestureRecognizers } } - - + + public void PointerReleased(PointerReleasedEventArgs e) { if (e.Pointer == _tracking && _scrolling) { + _inertia = _velocityTracker?.GetFlingVelocity().PixelsPerSecond ?? Vector.Zero; + e.Handled = true; if (_inertia == default || e.Timestamp == 0 @@ -165,11 +189,27 @@ namespace Avalonia.Input.GestureRecognizers var speed = _inertia * Math.Pow(0.15, st.Elapsed.TotalSeconds); var distance = speed * elapsedSinceLastTick.TotalSeconds; - _target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance)); - + var scrollGestureEventArgs = new ScrollGestureEventArgs(_gestureId, distance); + _target!.RaiseEvent(scrollGestureEventArgs); + if (!scrollGestureEventArgs.Handled || scrollGestureEventArgs.ShouldEndScrollGesture) + { + EndGesture(); + return false; + } - if (Math.Abs(speed.X) < InertialScrollSpeedEnd || Math.Abs(speed.Y) <= InertialScrollSpeedEnd) + // EndGesture using InertialScrollSpeedEnd only in the direction of scrolling + if (CanVerticallyScroll && CanHorizontallyScroll && Math.Abs(speed.X) < InertialScrollSpeedEnd && Math.Abs(speed.Y) <= InertialScrollSpeedEnd) + { + EndGesture(); + return false; + } + else if (CanVerticallyScroll && Math.Abs(speed.Y) <= InertialScrollSpeedEnd) + { + EndGesture(); + return false; + } + else if (CanHorizontallyScroll && Math.Abs(speed.X) < InertialScrollSpeedEnd) { EndGesture(); return false; diff --git a/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs b/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs new file mode 100644 index 0000000000..ce41aa6308 --- /dev/null +++ b/src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs @@ -0,0 +1,375 @@ +// Code in this file is derived from +// https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/gestures/velocity_tracker.dart + +using System; +using System.Diagnostics; +using Avalonia.Utilities; + +namespace Avalonia.Input.GestureRecognizers +{ + // Possible enhancement: add Flutter's 'IOSScrollViewFlingVelocityTracker' and 'MacOSScrollViewFlingVelocityTracker'? + + internal readonly record struct Velocity(Vector PixelsPerSecond) + { + public Velocity ClampMagnitude(double minValue, double maxValue) + { + Debug.Assert(minValue >= 0.0); + Debug.Assert(maxValue >= 0.0 && maxValue >= minValue); + double valueSquared = PixelsPerSecond.SquaredLength; + if (valueSquared > maxValue * maxValue) + { + double length = PixelsPerSecond.Length; + return new Velocity(length != 0.0 ? (PixelsPerSecond / length) * maxValue : Vector.Zero); + // preventing double.NaN in Vector PixelsPerSecond is important -- if a NaN eventually gets into a + // ScrollGestureEventArgs it results in runtime errors. + } + if (valueSquared < minValue * minValue) + { + double length = PixelsPerSecond.Length; + return new Velocity(length != 0.0 ? (PixelsPerSecond / length) * minValue : Vector.Zero); + } + return this; + } + } + + /// A two dimensional velocity estimate. + /// + /// VelocityEstimates are computed by [VelocityTracker.getVelocityEstimate]. An + /// estimate's [confidence] measures how well the velocity tracker's position + /// data fit a straight line, [duration] is the time that elapsed between the + /// first and last position sample used to compute the velocity, and [offset] + /// is similarly the difference between the first and last positions. + /// + /// See also: + /// + /// * [VelocityTracker], which computes [VelocityEstimate]s. + /// * [Velocity], which encapsulates (just) a velocity vector and provides some + /// useful velocity operations. + internal record VelocityEstimate(Vector PixelsPerSecond, double Confidence, TimeSpan Duration, Vector Offset); + + internal record struct PointAtTime(bool Valid, Vector Point, TimeSpan Time); + + /// Computes a pointer's velocity based on data from [PointerMoveEvent]s. + /// + /// The input data is provided by calling [addPosition]. Adding data is cheap. + /// + /// To obtain a velocity, call [getVelocity] or [getVelocityEstimate]. This will + /// compute the velocity based on the data added so far. Only call these when + /// you need to use the velocity, as they are comparatively expensive. + /// + /// The quality of the velocity estimation will be better if more data points + /// have been received. + internal class VelocityTracker + { + private const int AssumePointerMoveStoppedMilliseconds = 40; + private const int HistorySize = 20; + private const int HorizonMilliseconds = 100; + private const int MinSampleSize = 3; + private const double MinFlingVelocity = 50.0; // Logical pixels / second + private const double MaxFlingVelocity = 8000.0; + + private readonly PointAtTime[] _samples = new PointAtTime[HistorySize]; + private int _index = 0; + + /// + /// Adds a position as the given time to the tracker. + /// + /// + /// + public void AddPosition(TimeSpan time, Vector position) + { + _index++; + if (_index == HistorySize) + { + _index = 0; + } + _samples[_index] = new PointAtTime(true, position, time); + } + + /// Returns an estimate of the velocity of the object being tracked by the + /// tracker given the current information available to the tracker. + /// + /// Information is added using [addPosition]. + /// + /// Returns null if there is no data on which to base an estimate. + protected virtual VelocityEstimate? GetVelocityEstimate() + { + Span x = stackalloc double[HistorySize]; + Span y = stackalloc double[HistorySize]; + Span w = stackalloc double[HistorySize]; + Span time = stackalloc double[HistorySize]; + int sampleCount = 0; + int index = _index; + + var newestSample = _samples[index]; + if (!newestSample.Valid) + { + return null; + } + + var previousSample = newestSample; + var oldestSample = newestSample; + + // Starting with the most recent PointAtTime sample, iterate backwards while + // the samples represent continuous motion. + do + { + var sample = _samples[index]; + if (!sample.Valid) + { + break; + } + + double age = (newestSample.Time - sample.Time).TotalMilliseconds; + double delta = Math.Abs((sample.Time - previousSample.Time).TotalMilliseconds); + previousSample = sample; + if (age > HorizonMilliseconds || delta > AssumePointerMoveStoppedMilliseconds) + { + break; + } + + oldestSample = sample; + var position = sample.Point; + x[sampleCount] = position.X; + y[sampleCount] = position.Y; + w[sampleCount] = 1.0; + time[sampleCount] = -age; + index = (index == 0 ? HistorySize : index) - 1; + + sampleCount++; + } while (sampleCount < HistorySize); + + if (sampleCount >= MinSampleSize) + { + var xFit = LeastSquaresSolver.Solve(2, time.Slice(0, sampleCount), x.Slice(0, sampleCount), w.Slice(0, sampleCount)); + if (xFit != null) + { + var yFit = LeastSquaresSolver.Solve(2, time.Slice(0, sampleCount), y.Slice(0, sampleCount), w.Slice(0, sampleCount)); + if (yFit != null) + { + return new VelocityEstimate( // convert from pixels/ms to pixels/s + PixelsPerSecond: new Vector(xFit.Coefficients[1] * 1000, yFit.Coefficients[1] * 1000), + Confidence: xFit.Confidence * yFit.Confidence, + Duration: newestSample.Time - oldestSample.Time, + Offset: newestSample.Point - oldestSample.Point + ); + } + } + } + + // We're unable to make a velocity estimate but we did have at least one + // valid pointer position. + return new VelocityEstimate( + PixelsPerSecond: Vector.Zero, + Confidence: 1.0, + Duration: newestSample.Time - oldestSample.Time, + Offset: newestSample.Point - oldestSample.Point + ); + } + + /// + /// Computes the velocity of the pointer at the time of the last + /// provided data point. + /// + /// This can be expensive. Only call this when you need the velocity. + /// + /// Returns [Velocity.zero] if there is no data from which to compute an + /// estimate or if the estimated velocity is zero./// + /// + /// + internal Velocity GetVelocity() + { + var estimate = GetVelocityEstimate(); + if (estimate == null || estimate.PixelsPerSecond.IsDefault) + { + return new Velocity(Vector.Zero); + } + return new Velocity(estimate.PixelsPerSecond); + } + + internal virtual Velocity GetFlingVelocity() + { + return GetVelocity().ClampMagnitude(MinFlingVelocity, MaxFlingVelocity); + } + } + + /// An nth degree polynomial fit to a dataset. + internal class PolynomialFit + { + /// Creates a polynomial fit of the given degree. + /// + /// There are n + 1 coefficients in a fit of degree n. + internal PolynomialFit(int degree) + { + Coefficients = new double[degree + 1]; + } + + /// The polynomial coefficients of the fit. + public double[] Coefficients { get; } + + /// An indicator of the quality of the fit. + /// + /// Larger values indicate greater quality. + public double Confidence { get; set; } + } + + internal class LeastSquaresSolver + { + private const double PrecisionErrorTolerance = 1e-10; + + /// + /// Fits a polynomial of the given degree to the data points. + /// When there is not enough data to fit a curve null is returned. + /// + public static PolynomialFit? Solve(int degree, ReadOnlySpan x, ReadOnlySpan y, ReadOnlySpan w) + { + if (degree > x.Length) + { + // Not enough data to fit a curve. + return null; + } + + PolynomialFit result = new PolynomialFit(degree); + + // Shorthands for the purpose of notation equivalence to original C++ code. + int m = x.Length; + int n = degree + 1; + + // Expand the X vector to a matrix A, pre-multiplied by the weights. + _Matrix a = new _Matrix(m, stackalloc double[n * m]); + for (int h = 0; h < m; h += 1) + { + a[0, h] = w[h]; + for (int i = 1; i < n; i += 1) + { + a[i, h] = a[i - 1, h] * x[h]; + } + } + + // Apply the Gram-Schmidt process to A to obtain its QR decomposition. + + // Orthonormal basis, column-major order Vector. + _Matrix q = new _Matrix(m, stackalloc double[n * m]); + // Upper triangular matrix, row-major order. + _Matrix r = new _Matrix(n, stackalloc double[n * n]); + for (int j = 0; j < n; j += 1) + { + for (int h = 0; h < m; h += 1) + { + q[j, h] = a[j, h]; + } + for (int i = 0; i < j; i += 1) + { + double dot = Multiply(q.GetRow(j), q.GetRow(i)); + for (int h = 0; h < m; h += 1) + { + q[j, h] = q[j, h] - dot * q[i, h]; + } + } + + double norm = Norm(q.GetRow(j)); + if (norm < PrecisionErrorTolerance) + { + // Vectors are linearly dependent or zero so no solution. + return null; + } + + double inverseNorm = 1.0 / norm; + for (int h = 0; h < m; h += 1) + { + q[j, h] = q[j, h] * inverseNorm; + } + for (int i = 0; i < n; i += 1) + { + r[j, i] = i < j ? 0.0 : Multiply(q.GetRow(j), a.GetRow(i)); + } + } + + // Solve R B = Qt W Y to find B. This is easy because R is upper triangular. + // We just work from bottom-right to top-left calculating B's coefficients. + // "m" isn't expected to be bigger than HistorySize=20, so allocation on stack is safe. + Span wy = stackalloc double[m]; + for (int h = 0; h < m; h += 1) + { + wy[h] = y[h] * w[h]; + } + for (int i = n - 1; i >= 0; i -= 1) + { + result.Coefficients[i] = Multiply(q.GetRow(i), wy); + for (int j = n - 1; j > i; j -= 1) + { + result.Coefficients[i] -= r[i, j] * result.Coefficients[j]; + } + result.Coefficients[i] /= r[i, i]; + } + + // Calculate the coefficient of determination (confidence) as: + // 1 - (sumSquaredError / sumSquaredTotal) + // ...where sumSquaredError is the residual sum of squares (variance of the + // error), and sumSquaredTotal is the total sum of squares (variance of the + // data) where each has been weighted. + double yMean = 0.0; + for (int h = 0; h < m; h += 1) + { + yMean += y[h]; + } + yMean /= m; + + double sumSquaredError = 0.0; + double sumSquaredTotal = 0.0; + for (int h = 0; h < m; h += 1) + { + double term = 1.0; + double err = y[h] - result.Coefficients[0]; + for (int i = 1; i < n; i += 1) + { + term *= x[h]; + err -= term * result.Coefficients[i]; + } + sumSquaredError += w[h] * w[h] * err * err; + double v = y[h] - yMean; + sumSquaredTotal += w[h] * w[h] * v * v; + } + + result.Confidence = sumSquaredTotal <= PrecisionErrorTolerance ? 1.0 : + 1.0 - (sumSquaredError / sumSquaredTotal); + + return result; + } + + private static double Multiply(Span v1, Span v2) + { + double result = 0.0; + for (int i = 0; i < v1.Length; i += 1) + { + result += v1[i] * v2[i]; + } + return result; + } + + private static double Norm(Span v) + { + return Math.Sqrt(Multiply(v, v)); + } + + private readonly ref struct _Matrix + { + private readonly int _columns; + private readonly Span _elements; + + internal _Matrix(int cols, Span elements) + { + _columns = cols; + _elements = elements; + } + + public double this[int row, int col] + { + get => _elements[row * _columns + col]; + set => _elements[row * _columns + col] = value; + } + + public Span GetRow(int row) => _elements.Slice(row * _columns, _columns); + } + } +} diff --git a/src/Avalonia.Base/Input/Gestures.cs b/src/Avalonia.Base/Input/Gestures.cs index 1ea88fe824..a9e42c2374 100644 --- a/src/Avalonia.Base/Input/Gestures.cs +++ b/src/Avalonia.Base/Input/Gestures.cs @@ -1,6 +1,9 @@ using System; +using System.Threading; using Avalonia.Interactivity; using Avalonia.Platform; +using Avalonia.Threading; +using Avalonia.Reactive; using Avalonia.VisualTree; namespace Avalonia.Input @@ -8,6 +11,21 @@ namespace Avalonia.Input public static class Gestures { private static bool s_isDoubleTapped = false; + private static bool s_isHolding; + private static CancellationTokenSource? s_holdCancellationToken; + + /// + /// Defines the IsHoldingEnabled attached property. + /// + public static readonly AttachedProperty IsHoldingEnabledProperty = + AvaloniaProperty.RegisterAttached("IsHoldingEnabled", typeof(Gestures), true); + + /// + /// Defines the IsHoldWithMouseEnabled attached property. + /// + public static readonly AttachedProperty IsHoldWithMouseEnabledProperty = + AvaloniaProperty.RegisterAttached("IsHoldWithMouseEnabled", typeof(Gestures), false); + public static readonly RoutedEvent TappedEvent = RoutedEvent.Register( "Tapped", RoutingStrategies.Bubble, @@ -45,19 +63,54 @@ namespace Avalonia.Input private static readonly WeakReference s_lastPress = new WeakReference(null); private static Point s_lastPressPoint; + private static IPointer? s_lastPointer; + + public static readonly RoutedEvent PinchEvent = + RoutedEvent.Register( + "PinchEvent", RoutingStrategies.Bubble, typeof(Gestures)); + + public static readonly RoutedEvent PinchEndedEvent = + RoutedEvent.Register( + "PinchEndedEvent", RoutingStrategies.Bubble, typeof(Gestures)); public static readonly RoutedEvent PullGestureEvent = RoutedEvent.Register( "PullGesture", RoutingStrategies.Bubble, typeof(Gestures)); + /// + /// Occurs when a user performs a press and hold gesture (with a single touch, mouse, or pen/stylus contact). + /// + public static readonly RoutedEvent HoldingEvent = + RoutedEvent.Register( + "Holding", RoutingStrategies.Bubble, typeof(Gestures)); + public static readonly RoutedEvent PullGestureEndedEvent = RoutedEvent.Register( "PullGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); + public static bool GetIsHoldingEnabled(StyledElement element) + { + return element.GetValue(IsHoldingEnabledProperty); + } + public static void SetIsHoldingEnabled(StyledElement element, bool value) + { + element.SetValue(IsHoldingEnabledProperty, value); + } + + public static bool GetIsHoldWithMouseEnabled(StyledElement element) + { + return element.GetValue(IsHoldWithMouseEnabledProperty); + } + public static void SetIsHoldWithMouseEnabled(StyledElement element, bool value) + { + element.SetValue(IsHoldWithMouseEnabledProperty, value); + } + static Gestures() { InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed); InputElement.PointerReleasedEvent.RouteFinished.Subscribe(PointerReleased); + InputElement.PointerMovedEvent.RouteFinished.Subscribe(PointerMoved); } public static void AddTappedHandler(Interactive element, EventHandler handler) @@ -102,11 +155,42 @@ namespace Avalonia.Input var e = (PointerPressedEventArgs)ev; var visual = (Visual)ev.Source; + if(s_lastPointer != null) + { + if(s_isHolding && ev.Source is Interactive i) + { + i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Cancelled, s_lastPressPoint, s_lastPointer.Type)); + } + s_holdCancellationToken?.Cancel(); + s_holdCancellationToken?.Dispose(); + s_holdCancellationToken = null; + + s_lastPointer = null; + } + + s_isHolding = false; + if (e.ClickCount % 2 == 1) { s_isDoubleTapped = false; s_lastPress.SetTarget(ev.Source); + s_lastPointer = e.Pointer; s_lastPressPoint = e.GetPosition((Visual)ev.Source); + s_holdCancellationToken = new CancellationTokenSource(); + var token = s_holdCancellationToken.Token; + var settings = AvaloniaLocator.Current.GetService(); + + if (settings != null) + { + DispatcherTimer.RunOnce(() => + { + if (!token.IsCancellationRequested && e.Source is InputElement i && GetIsHoldingEnabled(i) && (e.Pointer.Type != PointerType.Mouse || GetIsHoldWithMouseEnabled(i))) + { + s_isHolding = true; + i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Started, s_lastPressPoint, s_lastPointer.Type)); + } + }, settings.HoldWaitDuration); + } } else if (e.ClickCount % 2 == 0 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) { @@ -140,7 +224,12 @@ namespace Avalonia.Input if (tapRect.ContainsExclusive(point.Position)) { - if (e.InitialPressMouseButton == MouseButton.Right) + if(s_isHolding) + { + s_isHolding = false; + i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Completed, s_lastPressPoint, s_lastPointer!.Type)); + } + else if (e.InitialPressMouseButton == MouseButton.Right) { i.RaiseEvent(new TappedEventArgs(RightTappedEvent, e)); } @@ -152,6 +241,45 @@ namespace Avalonia.Input } } } + + s_holdCancellationToken?.Cancel(); + s_holdCancellationToken?.Dispose(); + s_holdCancellationToken = null; + s_lastPointer = null; + } + } + + private static void PointerMoved(RoutedEventArgs ev) + { + if (ev.Route == RoutingStrategies.Bubble) + { + var e = (PointerEventArgs)ev; + if (s_lastPress.TryGetTarget(out var target)) + { + if (e.Pointer == s_lastPointer) + { + var point = e.GetCurrentPoint((Visual)target); + var settings = AvaloniaLocator.Current.GetService(); + var tapSize = settings?.GetTapSize(point.Pointer.Type) ?? new Size(4, 4); + var tapRect = new Rect(s_lastPressPoint, new Size()) + .Inflate(new Thickness(tapSize.Width, tapSize.Height)); + + if (tapRect.ContainsExclusive(point.Position)) + { + return; + } + + if (s_isHolding && ev.Source is Interactive i) + { + i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Cancelled, s_lastPressPoint, s_lastPointer!.Type)); + } + } + } + + s_holdCancellationToken?.Cancel(); + s_holdCancellationToken?.Dispose(); + s_holdCancellationToken = null; + s_isHolding = false; } } } diff --git a/src/Avalonia.Base/Input/HoldingRoutedEventArgs.cs b/src/Avalonia.Base/Input/HoldingRoutedEventArgs.cs new file mode 100644 index 0000000000..b9a877b2ed --- /dev/null +++ b/src/Avalonia.Base/Input/HoldingRoutedEventArgs.cs @@ -0,0 +1,51 @@ +using System; +using Avalonia.Interactivity; + +namespace Avalonia.Input +{ + public class HoldingRoutedEventArgs : RoutedEventArgs + { + /// + /// Gets the state of the event. + /// + public HoldingState HoldingState { get; } + + /// + /// Gets the location of the touch, mouse, or pen/stylus contact. + /// + public Point Position { get; } + + /// + /// Gets the pointer type of the input source. + /// + public PointerType PointerType { get; } + + /// + /// Initializes a new instance of the class. + /// + public HoldingRoutedEventArgs(HoldingState holdingState, Point position, PointerType pointerType) : base(Gestures.HoldingEvent) + { + HoldingState = holdingState; + Position = position; + PointerType = pointerType; + } + } + + public enum HoldingState + { + /// + /// A single contact has been detected and a time threshold is crossed without the contact being lifted, another contact detected, or another gesture started. + /// + Started, + + /// + /// The single contact is lifted. + /// + Completed, + + /// + /// An additional contact is detected or a subsequent gesture (such as a slide) is detected. + /// + Cancelled, + } +} diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs index fa755277cc..962c7aa334 100644 --- a/src/Avalonia.Base/Input/InputElement.cs +++ b/src/Avalonia.Base/Input/InputElement.cs @@ -7,6 +7,7 @@ using Avalonia.Data; using Avalonia.Input.GestureRecognizers; using Avalonia.Input.TextInput; using Avalonia.Interactivity; +using Avalonia.Reactive; using Avalonia.VisualTree; #nullable enable @@ -188,11 +189,16 @@ namespace Avalonia.Input /// public static readonly RoutedEvent TappedEvent = Gestures.TappedEvent; + /// + /// Defines the event. + /// + public static readonly RoutedEvent HoldingEvent = Gestures.HoldingEvent; + /// /// Defines the event. /// public static readonly RoutedEvent DoubleTappedEvent = Gestures.DoubleTappedEvent; - + private bool _isEffectivelyEnabled = true; private bool _isFocused; private bool _isKeyboardFocusWithin; @@ -352,6 +358,15 @@ namespace Avalonia.Input add { AddHandler(TappedEvent, value); } remove { RemoveHandler(TappedEvent, value); } } + + /// + /// Occurs when a hold gesture occurs on the control. + /// + public event EventHandler? Holding + { + add { AddHandler(HoldingEvent, value); } + remove { RemoveHandler(HoldingEvent, value); } + } /// /// Occurs when a double-tap gesture occurs on the control. diff --git a/src/Avalonia.Base/Input/InputManager.cs b/src/Avalonia.Base/Input/InputManager.cs index f604ff8e74..e87665fd54 100644 --- a/src/Avalonia.Base/Input/InputManager.cs +++ b/src/Avalonia.Base/Input/InputManager.cs @@ -1,6 +1,6 @@ using System; -using System.Reactive.Subjects; using Avalonia.Input.Raw; +using Avalonia.Reactive; namespace Avalonia.Input { @@ -10,9 +10,9 @@ namespace Avalonia.Input /// public class InputManager : IInputManager { - private readonly Subject _preProcess = new Subject(); - private readonly Subject _process = new Subject(); - private readonly Subject _postProcess = new Subject(); + private readonly LightweightSubject _preProcess = new(); + private readonly LightweightSubject _process = new(); + private readonly LightweightSubject _postProcess = new(); /// /// Gets the global instance of the input manager. diff --git a/src/Avalonia.Base/Input/KeyEventArgs.cs b/src/Avalonia.Base/Input/KeyEventArgs.cs index 39c9766105..35fa549995 100644 --- a/src/Avalonia.Base/Input/KeyEventArgs.cs +++ b/src/Avalonia.Base/Input/KeyEventArgs.cs @@ -5,7 +5,7 @@ namespace Avalonia.Input { public class KeyEventArgs : RoutedEventArgs { - internal KeyEventArgs() + public KeyEventArgs() { } diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs index 9b17e336d3..e1c42c4ead 100644 --- a/src/Avalonia.Base/Input/MouseDevice.cs +++ b/src/Avalonia.Base/Input/MouseDevice.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Avalonia.Reactive; using Avalonia.Input.Raw; using Avalonia.Platform; using Avalonia.Utilities; diff --git a/src/Avalonia.Base/Input/PinchEventArgs.cs b/src/Avalonia.Base/Input/PinchEventArgs.cs new file mode 100644 index 0000000000..31c760eb51 --- /dev/null +++ b/src/Avalonia.Base/Input/PinchEventArgs.cs @@ -0,0 +1,24 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Input +{ + public class PinchEventArgs : RoutedEventArgs + { + public PinchEventArgs(double scale, Point scaleOrigin) : base(Gestures.PinchEvent) + { + Scale = scale; + ScaleOrigin = scaleOrigin; + } + + public double Scale { get; } = 1; + + public Point ScaleOrigin { get; } + } + + public class PinchEndedEventArgs : RoutedEventArgs + { + public PinchEndedEventArgs() : base(Gestures.PinchEndedEvent) + { + } + } +} diff --git a/src/Avalonia.Base/Input/Raw/RawInputEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawInputEventArgs.cs index 3a5ae1340f..cf4d37b6da 100644 --- a/src/Avalonia.Base/Input/Raw/RawInputEventArgs.cs +++ b/src/Avalonia.Base/Input/Raw/RawInputEventArgs.cs @@ -21,11 +21,9 @@ namespace Avalonia.Input.Raw /// The root from which the event originates. public RawInputEventArgs(IInputDevice device, ulong timestamp, IInputRoot root) { - device = device ?? throw new ArgumentNullException(nameof(device)); - - Device = device; + Device = device ?? throw new ArgumentNullException(nameof(device)); Timestamp = timestamp; - Root = root; + Root = root ?? throw new ArgumentNullException(nameof(root)); } /// diff --git a/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs index 854dd4b83b..ebedc9e6e8 100644 --- a/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs @@ -53,9 +53,6 @@ namespace Avalonia.Input.Raw RawInputModifiers inputModifiers) : base(device, timestamp, root) { - Contract.Requires(device != null); - Contract.Requires(root != null); - Point = new RawPointerPoint(); Position = position; Type = type; @@ -80,9 +77,6 @@ namespace Avalonia.Input.Raw RawInputModifiers inputModifiers) : base(device, timestamp, root) { - Contract.Requires(device != null); - Contract.Requires(root != null); - Point = point; Type = type; InputModifiers = inputModifiers; diff --git a/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs b/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs index fd1d0f42c3..f1a0887b60 100644 --- a/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs +++ b/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs @@ -6,6 +6,10 @@ namespace Avalonia.Input { public int Id { get; } public Vector Delta { get; } + /// + /// When set the ScrollGestureRecognizer should stop its current active scroll gesture. + /// + public bool ShouldEndScrollGesture { get; set; } private static int _nextId = 1; public static int GetNextFreeId() => _nextId++; diff --git a/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs b/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs index 8a3f5b968e..9b5668bf98 100644 --- a/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs +++ b/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs @@ -1,5 +1,5 @@ using System; -using Avalonia.VisualTree; +using Avalonia.Reactive; namespace Avalonia.Input.TextInput { diff --git a/src/Avalonia.Base/Input/TextInputEventArgs.cs b/src/Avalonia.Base/Input/TextInputEventArgs.cs index 787bf1abd3..a027bec0c6 100644 --- a/src/Avalonia.Base/Input/TextInputEventArgs.cs +++ b/src/Avalonia.Base/Input/TextInputEventArgs.cs @@ -4,7 +4,7 @@ namespace Avalonia.Input { public class TextInputEventArgs : RoutedEventArgs { - internal TextInputEventArgs() + public TextInputEventArgs() { } diff --git a/src/Avalonia.Base/Interactivity/InteractiveExtensions.cs b/src/Avalonia.Base/Interactivity/InteractiveExtensions.cs index b12cf64fdf..5264b96679 100644 --- a/src/Avalonia.Base/Interactivity/InteractiveExtensions.cs +++ b/src/Avalonia.Base/Interactivity/InteractiveExtensions.cs @@ -1,6 +1,5 @@ using System; -using System.Reactive.Disposables; -using System.Reactive.Linq; +using Avalonia.Reactive; namespace Avalonia.Interactivity { diff --git a/src/Avalonia.Base/Interactivity/RoutedEvent.cs b/src/Avalonia.Base/Interactivity/RoutedEvent.cs index 7046871538..79a4dc60e4 100644 --- a/src/Avalonia.Base/Interactivity/RoutedEvent.cs +++ b/src/Avalonia.Base/Interactivity/RoutedEvent.cs @@ -1,5 +1,5 @@ using System; -using System.Reactive.Subjects; +using Avalonia.Reactive; namespace Avalonia.Interactivity { @@ -13,8 +13,8 @@ namespace Avalonia.Interactivity public class RoutedEvent { - private readonly Subject<(object, RoutedEventArgs)> _raised = new Subject<(object, RoutedEventArgs)>(); - private readonly Subject _routeFinished = new Subject(); + private readonly LightweightSubject<(object, RoutedEventArgs)> _raised = new(); + private readonly LightweightSubject _routeFinished = new(); public RoutedEvent( string name, diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index d185148894..775b8adddd 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -1,6 +1,6 @@ using System; using Avalonia.Logging; -using Avalonia.Styling; +using Avalonia.Reactive; using Avalonia.VisualTree; #nullable enable @@ -470,14 +470,12 @@ namespace Avalonia.Layout protected static void AffectsMeasure(params AvaloniaProperty[] properties) where T : Layoutable { - void Invalidate(AvaloniaPropertyChangedEventArgs e) - { - (e.Sender as T)?.InvalidateMeasure(); - } + var invalidateObserver = new AnonymousObserver( + static e => (e.Sender as T)?.InvalidateMeasure()); foreach (var property in properties) { - property.Changed.Subscribe(Invalidate); + property.Changed.Subscribe(invalidateObserver); } } @@ -493,14 +491,12 @@ namespace Avalonia.Layout protected static void AffectsArrange(params AvaloniaProperty[] properties) where T : Layoutable { - void Invalidate(AvaloniaPropertyChangedEventArgs e) - { - (e.Sender as T)?.InvalidateArrange(); - } + var invalidate = new AnonymousObserver( + static e => (e.Sender as T)?.InvalidateArrange()); foreach (var property in properties) { - property.Changed.Subscribe(Invalidate); + property.Changed.Subscribe(invalidate); } } diff --git a/src/Avalonia.Base/Layout/WrapLayout/UvMeasure.cs b/src/Avalonia.Base/Layout/WrapLayout/UvMeasure.cs index 91fa459acb..fa298b53a3 100644 --- a/src/Avalonia.Base/Layout/WrapLayout/UvMeasure.cs +++ b/src/Avalonia.Base/Layout/WrapLayout/UvMeasure.cs @@ -7,7 +7,7 @@ namespace Avalonia.Layout { internal struct UvMeasure { - internal static readonly UvMeasure Zero = default(UvMeasure); + internal static readonly UvMeasure Zero = default; internal double U { get; set; } diff --git a/src/Avalonia.Base/Logging/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs index 972a9a1e9d..f15e87da1b 100644 --- a/src/Avalonia.Base/Logging/LogArea.cs +++ b/src/Avalonia.Base/Logging/LogArea.cs @@ -35,6 +35,11 @@ namespace Avalonia.Logging /// public const string Control = nameof(Control); + /// + /// The log event comes from Win32 Platform. + /// + public const string Platform = nameof(Platform); + /// /// The log event comes from Win32 Platform. /// diff --git a/src/Avalonia.Base/Media/BoxShadow.cs b/src/Avalonia.Base/Media/BoxShadow.cs index fa94dd83eb..dd2c23f4ae 100644 --- a/src/Avalonia.Base/Media/BoxShadow.cs +++ b/src/Avalonia.Base/Media/BoxShadow.cs @@ -45,7 +45,14 @@ namespace Avalonia.Media } } - public bool IsEmpty => OffsetX == 0 && OffsetY == 0 && Blur == 0 && Spread == 0; + /// + /// Gets a value indicating whether the instance has default values. + /// + public bool IsDefault => OffsetX == 0 && OffsetY == 0 && Blur == 0 && Spread == 0; + + /// + [Obsolete("Use IsDefault instead.")] + public bool IsEmpty => IsDefault; private readonly static char[] s_Separator = new char[] { ' ', '\t' }; @@ -82,7 +89,7 @@ namespace Avalonia.Media { var sb = StringBuilderCache.Acquire(); - if (IsEmpty) + if (IsDefault) { return "none"; } diff --git a/src/Avalonia.Base/Media/BoxShadows.cs b/src/Avalonia.Base/Media/BoxShadows.cs index 4265e4efbc..50ae7699dd 100644 --- a/src/Avalonia.Base/Media/BoxShadows.cs +++ b/src/Avalonia.Base/Media/BoxShadows.cs @@ -21,7 +21,7 @@ namespace Avalonia.Media { _first = shadow; _list = null; - Count = _first.IsEmpty ? 0 : 1; + Count = _first.IsDefault ? 0 : 1; } public BoxShadows(BoxShadow first, BoxShadow[] rest) @@ -120,7 +120,7 @@ namespace Avalonia.Media get { foreach(var boxShadow in this) - if (!boxShadow.IsEmpty && boxShadow.IsInset) + if (!boxShadow.IsDefault && boxShadow.IsInset) return true; return false; } diff --git a/src/Avalonia.Base/Media/Brush.cs b/src/Avalonia.Base/Media/Brush.cs index 8d531e9394..b9a560ad8f 100644 --- a/src/Avalonia.Base/Media/Brush.cs +++ b/src/Avalonia.Base/Media/Brush.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using Avalonia.Animation; using Avalonia.Animation.Animators; using Avalonia.Media.Immutable; +using Avalonia.Reactive; namespace Avalonia.Media { @@ -10,7 +11,7 @@ namespace Avalonia.Media /// Describes how an area is painted. /// [TypeConverter(typeof(BrushConverter))] - public abstract class Brush : Animatable, IMutableBrush + public abstract class Brush : Animatable { /// /// Defines the property. @@ -92,9 +93,6 @@ namespace Avalonia.Media throw new FormatException($"Invalid brush string: '{s}'."); } - /// - public abstract IBrush ToImmutable(); - /// /// Marks a property as affecting the brush's visual representation. /// @@ -106,14 +104,12 @@ namespace Avalonia.Media protected static void AffectsRender(params AvaloniaProperty[] properties) where T : Brush { - static void Invalidate(AvaloniaPropertyChangedEventArgs e) - { - (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty); - } + var invalidateObserver = new AnonymousObserver( + static e => (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty)); foreach (var property in properties) { - property.Changed.Subscribe(e => Invalidate(e)); + property.Changed.Subscribe(invalidateObserver); } } diff --git a/src/Avalonia.Base/Media/BrushExtensions.cs b/src/Avalonia.Base/Media/BrushExtensions.cs index 2fc8778a5e..d0208c187a 100644 --- a/src/Avalonia.Base/Media/BrushExtensions.cs +++ b/src/Avalonia.Base/Media/BrushExtensions.cs @@ -16,11 +16,11 @@ namespace Avalonia.Media /// The result of calling if the brush is mutable, /// otherwise . /// - public static IBrush ToImmutable(this IBrush brush) + public static IImmutableBrush ToImmutable(this IBrush brush) { _ = brush ?? throw new ArgumentNullException(nameof(brush)); - return (brush as IMutableBrush)?.ToImmutable() ?? brush; + return (brush as IMutableBrush)?.ToImmutable() ?? (IImmutableBrush)brush; } /// diff --git a/src/Avalonia.Base/Media/Brushes.cs b/src/Avalonia.Base/Media/Brushes.cs index 5957775f39..7ecba6df3e 100644 --- a/src/Avalonia.Base/Media/Brushes.cs +++ b/src/Avalonia.Base/Media/Brushes.cs @@ -8,706 +8,706 @@ namespace Avalonia.Media /// /// Gets an colored brush. /// - public static ISolidColorBrush AliceBlue => KnownColor.AliceBlue.ToBrush(); + public static IImmutableSolidColorBrush AliceBlue => KnownColor.AliceBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush AntiqueWhite => KnownColor.AntiqueWhite.ToBrush(); + public static IImmutableSolidColorBrush AntiqueWhite => KnownColor.AntiqueWhite.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Aqua => KnownColor.Aqua.ToBrush(); + public static IImmutableSolidColorBrush Aqua => KnownColor.Aqua.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Aquamarine => KnownColor.Aquamarine.ToBrush(); + public static IImmutableSolidColorBrush Aquamarine => KnownColor.Aquamarine.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Azure => KnownColor.Azure.ToBrush(); + public static IImmutableSolidColorBrush Azure => KnownColor.Azure.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Beige => KnownColor.Beige.ToBrush(); + public static IImmutableSolidColorBrush Beige => KnownColor.Beige.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Bisque => KnownColor.Bisque.ToBrush(); + public static IImmutableSolidColorBrush Bisque => KnownColor.Bisque.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Black => KnownColor.Black.ToBrush(); + public static IImmutableSolidColorBrush Black => KnownColor.Black.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush BlanchedAlmond => KnownColor.BlanchedAlmond.ToBrush(); + public static IImmutableSolidColorBrush BlanchedAlmond => KnownColor.BlanchedAlmond.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Blue => KnownColor.Blue.ToBrush(); + public static IImmutableSolidColorBrush Blue => KnownColor.Blue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush BlueViolet => KnownColor.BlueViolet.ToBrush(); + public static IImmutableSolidColorBrush BlueViolet => KnownColor.BlueViolet.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Brown => KnownColor.Brown.ToBrush(); + public static IImmutableSolidColorBrush Brown => KnownColor.Brown.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush BurlyWood => KnownColor.BurlyWood.ToBrush(); + public static IImmutableSolidColorBrush BurlyWood => KnownColor.BurlyWood.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush CadetBlue => KnownColor.CadetBlue.ToBrush(); + public static IImmutableSolidColorBrush CadetBlue => KnownColor.CadetBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Chartreuse => KnownColor.Chartreuse.ToBrush(); + public static IImmutableSolidColorBrush Chartreuse => KnownColor.Chartreuse.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Chocolate => KnownColor.Chocolate.ToBrush(); + public static IImmutableSolidColorBrush Chocolate => KnownColor.Chocolate.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Coral => KnownColor.Coral.ToBrush(); + public static IImmutableSolidColorBrush Coral => KnownColor.Coral.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush CornflowerBlue => KnownColor.CornflowerBlue.ToBrush(); + public static IImmutableSolidColorBrush CornflowerBlue => KnownColor.CornflowerBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Cornsilk => KnownColor.Cornsilk.ToBrush(); + public static IImmutableSolidColorBrush Cornsilk => KnownColor.Cornsilk.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Crimson => KnownColor.Crimson.ToBrush(); + public static IImmutableSolidColorBrush Crimson => KnownColor.Crimson.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Cyan => KnownColor.Cyan.ToBrush(); + public static IImmutableSolidColorBrush Cyan => KnownColor.Cyan.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkBlue => KnownColor.DarkBlue.ToBrush(); + public static IImmutableSolidColorBrush DarkBlue => KnownColor.DarkBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkCyan => KnownColor.DarkCyan.ToBrush(); + public static IImmutableSolidColorBrush DarkCyan => KnownColor.DarkCyan.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkGoldenrod => KnownColor.DarkGoldenrod.ToBrush(); + public static IImmutableSolidColorBrush DarkGoldenrod => KnownColor.DarkGoldenrod.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkGray => KnownColor.DarkGray.ToBrush(); + public static IImmutableSolidColorBrush DarkGray => KnownColor.DarkGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkGreen => KnownColor.DarkGreen.ToBrush(); + public static IImmutableSolidColorBrush DarkGreen => KnownColor.DarkGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkKhaki => KnownColor.DarkKhaki.ToBrush(); + public static IImmutableSolidColorBrush DarkKhaki => KnownColor.DarkKhaki.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkMagenta => KnownColor.DarkMagenta.ToBrush(); + public static IImmutableSolidColorBrush DarkMagenta => KnownColor.DarkMagenta.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkOliveGreen => KnownColor.DarkOliveGreen.ToBrush(); + public static IImmutableSolidColorBrush DarkOliveGreen => KnownColor.DarkOliveGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkOrange => KnownColor.DarkOrange.ToBrush(); + public static IImmutableSolidColorBrush DarkOrange => KnownColor.DarkOrange.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkOrchid => KnownColor.DarkOrchid.ToBrush(); + public static IImmutableSolidColorBrush DarkOrchid => KnownColor.DarkOrchid.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkRed => KnownColor.DarkRed.ToBrush(); + public static IImmutableSolidColorBrush DarkRed => KnownColor.DarkRed.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkSalmon => KnownColor.DarkSalmon.ToBrush(); + public static IImmutableSolidColorBrush DarkSalmon => KnownColor.DarkSalmon.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkSeaGreen => KnownColor.DarkSeaGreen.ToBrush(); + public static IImmutableSolidColorBrush DarkSeaGreen => KnownColor.DarkSeaGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkSlateBlue => KnownColor.DarkSlateBlue.ToBrush(); + public static IImmutableSolidColorBrush DarkSlateBlue => KnownColor.DarkSlateBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkSlateGray => KnownColor.DarkSlateGray.ToBrush(); + public static IImmutableSolidColorBrush DarkSlateGray => KnownColor.DarkSlateGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkTurquoise => KnownColor.DarkTurquoise.ToBrush(); + public static IImmutableSolidColorBrush DarkTurquoise => KnownColor.DarkTurquoise.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkViolet => KnownColor.DarkViolet.ToBrush(); + public static IImmutableSolidColorBrush DarkViolet => KnownColor.DarkViolet.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DeepPink => KnownColor.DeepPink.ToBrush(); + public static IImmutableSolidColorBrush DeepPink => KnownColor.DeepPink.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DeepSkyBlue => KnownColor.DeepSkyBlue.ToBrush(); + public static IImmutableSolidColorBrush DeepSkyBlue => KnownColor.DeepSkyBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DimGray => KnownColor.DimGray.ToBrush(); + public static IImmutableSolidColorBrush DimGray => KnownColor.DimGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DodgerBlue => KnownColor.DodgerBlue.ToBrush(); + public static IImmutableSolidColorBrush DodgerBlue => KnownColor.DodgerBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Firebrick => KnownColor.Firebrick.ToBrush(); + public static IImmutableSolidColorBrush Firebrick => KnownColor.Firebrick.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush FloralWhite => KnownColor.FloralWhite.ToBrush(); + public static IImmutableSolidColorBrush FloralWhite => KnownColor.FloralWhite.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush ForestGreen => KnownColor.ForestGreen.ToBrush(); + public static IImmutableSolidColorBrush ForestGreen => KnownColor.ForestGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Fuchsia => KnownColor.Fuchsia.ToBrush(); + public static IImmutableSolidColorBrush Fuchsia => KnownColor.Fuchsia.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Gainsboro => KnownColor.Gainsboro.ToBrush(); + public static IImmutableSolidColorBrush Gainsboro => KnownColor.Gainsboro.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush GhostWhite => KnownColor.GhostWhite.ToBrush(); + public static IImmutableSolidColorBrush GhostWhite => KnownColor.GhostWhite.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Gold => KnownColor.Gold.ToBrush(); + public static IImmutableSolidColorBrush Gold => KnownColor.Gold.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Goldenrod => KnownColor.Goldenrod.ToBrush(); + public static IImmutableSolidColorBrush Goldenrod => KnownColor.Goldenrod.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Gray => KnownColor.Gray.ToBrush(); + public static IImmutableSolidColorBrush Gray => KnownColor.Gray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Green => KnownColor.Green.ToBrush(); + public static IImmutableSolidColorBrush Green => KnownColor.Green.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush GreenYellow => KnownColor.GreenYellow.ToBrush(); + public static IImmutableSolidColorBrush GreenYellow => KnownColor.GreenYellow.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Honeydew => KnownColor.Honeydew.ToBrush(); + public static IImmutableSolidColorBrush Honeydew => KnownColor.Honeydew.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush HotPink => KnownColor.HotPink.ToBrush(); + public static IImmutableSolidColorBrush HotPink => KnownColor.HotPink.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush IndianRed => KnownColor.IndianRed.ToBrush(); + public static IImmutableSolidColorBrush IndianRed => KnownColor.IndianRed.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Indigo => KnownColor.Indigo.ToBrush(); + public static IImmutableSolidColorBrush Indigo => KnownColor.Indigo.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Ivory => KnownColor.Ivory.ToBrush(); + public static IImmutableSolidColorBrush Ivory => KnownColor.Ivory.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Khaki => KnownColor.Khaki.ToBrush(); + public static IImmutableSolidColorBrush Khaki => KnownColor.Khaki.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Lavender => KnownColor.Lavender.ToBrush(); + public static IImmutableSolidColorBrush Lavender => KnownColor.Lavender.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LavenderBlush => KnownColor.LavenderBlush.ToBrush(); + public static IImmutableSolidColorBrush LavenderBlush => KnownColor.LavenderBlush.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LawnGreen => KnownColor.LawnGreen.ToBrush(); + public static IImmutableSolidColorBrush LawnGreen => KnownColor.LawnGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LemonChiffon => KnownColor.LemonChiffon.ToBrush(); + public static IImmutableSolidColorBrush LemonChiffon => KnownColor.LemonChiffon.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightBlue => KnownColor.LightBlue.ToBrush(); + public static IImmutableSolidColorBrush LightBlue => KnownColor.LightBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightCoral => KnownColor.LightCoral.ToBrush(); + public static IImmutableSolidColorBrush LightCoral => KnownColor.LightCoral.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightCyan => KnownColor.LightCyan.ToBrush(); + public static IImmutableSolidColorBrush LightCyan => KnownColor.LightCyan.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightGoldenrodYellow => KnownColor.LightGoldenrodYellow.ToBrush(); + public static IImmutableSolidColorBrush LightGoldenrodYellow => KnownColor.LightGoldenrodYellow.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightGray => KnownColor.LightGray.ToBrush(); + public static IImmutableSolidColorBrush LightGray => KnownColor.LightGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightGreen => KnownColor.LightGreen.ToBrush(); + public static IImmutableSolidColorBrush LightGreen => KnownColor.LightGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightPink => KnownColor.LightPink.ToBrush(); + public static IImmutableSolidColorBrush LightPink => KnownColor.LightPink.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightSalmon => KnownColor.LightSalmon.ToBrush(); + public static IImmutableSolidColorBrush LightSalmon => KnownColor.LightSalmon.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightSeaGreen => KnownColor.LightSeaGreen.ToBrush(); + public static IImmutableSolidColorBrush LightSeaGreen => KnownColor.LightSeaGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightSkyBlue => KnownColor.LightSkyBlue.ToBrush(); + public static IImmutableSolidColorBrush LightSkyBlue => KnownColor.LightSkyBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightSlateGray => KnownColor.LightSlateGray.ToBrush(); + public static IImmutableSolidColorBrush LightSlateGray => KnownColor.LightSlateGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightSteelBlue => KnownColor.LightSteelBlue.ToBrush(); + public static IImmutableSolidColorBrush LightSteelBlue => KnownColor.LightSteelBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightYellow => KnownColor.LightYellow.ToBrush(); + public static IImmutableSolidColorBrush LightYellow => KnownColor.LightYellow.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Lime => KnownColor.Lime.ToBrush(); + public static IImmutableSolidColorBrush Lime => KnownColor.Lime.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LimeGreen => KnownColor.LimeGreen.ToBrush(); + public static IImmutableSolidColorBrush LimeGreen => KnownColor.LimeGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Linen => KnownColor.Linen.ToBrush(); + public static IImmutableSolidColorBrush Linen => KnownColor.Linen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Magenta => KnownColor.Magenta.ToBrush(); + public static IImmutableSolidColorBrush Magenta => KnownColor.Magenta.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Maroon => KnownColor.Maroon.ToBrush(); + public static IImmutableSolidColorBrush Maroon => KnownColor.Maroon.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumAquamarine => KnownColor.MediumAquamarine.ToBrush(); + public static IImmutableSolidColorBrush MediumAquamarine => KnownColor.MediumAquamarine.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumBlue => KnownColor.MediumBlue.ToBrush(); + public static IImmutableSolidColorBrush MediumBlue => KnownColor.MediumBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumOrchid => KnownColor.MediumOrchid.ToBrush(); + public static IImmutableSolidColorBrush MediumOrchid => KnownColor.MediumOrchid.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumPurple => KnownColor.MediumPurple.ToBrush(); + public static IImmutableSolidColorBrush MediumPurple => KnownColor.MediumPurple.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumSeaGreen => KnownColor.MediumSeaGreen.ToBrush(); + public static IImmutableSolidColorBrush MediumSeaGreen => KnownColor.MediumSeaGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumSlateBlue => KnownColor.MediumSlateBlue.ToBrush(); + public static IImmutableSolidColorBrush MediumSlateBlue => KnownColor.MediumSlateBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumSpringGreen => KnownColor.MediumSpringGreen.ToBrush(); + public static IImmutableSolidColorBrush MediumSpringGreen => KnownColor.MediumSpringGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumTurquoise => KnownColor.MediumTurquoise.ToBrush(); + public static IImmutableSolidColorBrush MediumTurquoise => KnownColor.MediumTurquoise.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumVioletRed => KnownColor.MediumVioletRed.ToBrush(); + public static IImmutableSolidColorBrush MediumVioletRed => KnownColor.MediumVioletRed.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MidnightBlue => KnownColor.MidnightBlue.ToBrush(); + public static IImmutableSolidColorBrush MidnightBlue => KnownColor.MidnightBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MintCream => KnownColor.MintCream.ToBrush(); + public static IImmutableSolidColorBrush MintCream => KnownColor.MintCream.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MistyRose => KnownColor.MistyRose.ToBrush(); + public static IImmutableSolidColorBrush MistyRose => KnownColor.MistyRose.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Moccasin => KnownColor.Moccasin.ToBrush(); + public static IImmutableSolidColorBrush Moccasin => KnownColor.Moccasin.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush NavajoWhite => KnownColor.NavajoWhite.ToBrush(); + public static IImmutableSolidColorBrush NavajoWhite => KnownColor.NavajoWhite.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Navy => KnownColor.Navy.ToBrush(); + public static IImmutableSolidColorBrush Navy => KnownColor.Navy.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush OldLace => KnownColor.OldLace.ToBrush(); + public static IImmutableSolidColorBrush OldLace => KnownColor.OldLace.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Olive => KnownColor.Olive.ToBrush(); + public static IImmutableSolidColorBrush Olive => KnownColor.Olive.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush OliveDrab => KnownColor.OliveDrab.ToBrush(); + public static IImmutableSolidColorBrush OliveDrab => KnownColor.OliveDrab.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Orange => KnownColor.Orange.ToBrush(); + public static IImmutableSolidColorBrush Orange => KnownColor.Orange.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush OrangeRed => KnownColor.OrangeRed.ToBrush(); + public static IImmutableSolidColorBrush OrangeRed => KnownColor.OrangeRed.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Orchid => KnownColor.Orchid.ToBrush(); + public static IImmutableSolidColorBrush Orchid => KnownColor.Orchid.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PaleGoldenrod => KnownColor.PaleGoldenrod.ToBrush(); + public static IImmutableSolidColorBrush PaleGoldenrod => KnownColor.PaleGoldenrod.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PaleGreen => KnownColor.PaleGreen.ToBrush(); + public static IImmutableSolidColorBrush PaleGreen => KnownColor.PaleGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PaleTurquoise => KnownColor.PaleTurquoise.ToBrush(); + public static IImmutableSolidColorBrush PaleTurquoise => KnownColor.PaleTurquoise.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PaleVioletRed => KnownColor.PaleVioletRed.ToBrush(); + public static IImmutableSolidColorBrush PaleVioletRed => KnownColor.PaleVioletRed.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PapayaWhip => KnownColor.PapayaWhip.ToBrush(); + public static IImmutableSolidColorBrush PapayaWhip => KnownColor.PapayaWhip.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PeachPuff => KnownColor.PeachPuff.ToBrush(); + public static IImmutableSolidColorBrush PeachPuff => KnownColor.PeachPuff.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Peru => KnownColor.Peru.ToBrush(); + public static IImmutableSolidColorBrush Peru => KnownColor.Peru.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Pink => KnownColor.Pink.ToBrush(); + public static IImmutableSolidColorBrush Pink => KnownColor.Pink.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Plum => KnownColor.Plum.ToBrush(); + public static IImmutableSolidColorBrush Plum => KnownColor.Plum.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PowderBlue => KnownColor.PowderBlue.ToBrush(); + public static IImmutableSolidColorBrush PowderBlue => KnownColor.PowderBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Purple => KnownColor.Purple.ToBrush(); + public static IImmutableSolidColorBrush Purple => KnownColor.Purple.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Red => KnownColor.Red.ToBrush(); + public static IImmutableSolidColorBrush Red => KnownColor.Red.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush RosyBrown => KnownColor.RosyBrown.ToBrush(); + public static IImmutableSolidColorBrush RosyBrown => KnownColor.RosyBrown.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush RoyalBlue => KnownColor.RoyalBlue.ToBrush(); + public static IImmutableSolidColorBrush RoyalBlue => KnownColor.RoyalBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SaddleBrown => KnownColor.SaddleBrown.ToBrush(); + public static IImmutableSolidColorBrush SaddleBrown => KnownColor.SaddleBrown.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Salmon => KnownColor.Salmon.ToBrush(); + public static IImmutableSolidColorBrush Salmon => KnownColor.Salmon.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SandyBrown => KnownColor.SandyBrown.ToBrush(); + public static IImmutableSolidColorBrush SandyBrown => KnownColor.SandyBrown.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SeaGreen => KnownColor.SeaGreen.ToBrush(); + public static IImmutableSolidColorBrush SeaGreen => KnownColor.SeaGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SeaShell => KnownColor.SeaShell.ToBrush(); + public static IImmutableSolidColorBrush SeaShell => KnownColor.SeaShell.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Sienna => KnownColor.Sienna.ToBrush(); + public static IImmutableSolidColorBrush Sienna => KnownColor.Sienna.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Silver => KnownColor.Silver.ToBrush(); + public static IImmutableSolidColorBrush Silver => KnownColor.Silver.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SkyBlue => KnownColor.SkyBlue.ToBrush(); + public static IImmutableSolidColorBrush SkyBlue => KnownColor.SkyBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SlateBlue => KnownColor.SlateBlue.ToBrush(); + public static IImmutableSolidColorBrush SlateBlue => KnownColor.SlateBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SlateGray => KnownColor.SlateGray.ToBrush(); + public static IImmutableSolidColorBrush SlateGray => KnownColor.SlateGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Snow => KnownColor.Snow.ToBrush(); + public static IImmutableSolidColorBrush Snow => KnownColor.Snow.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SpringGreen => KnownColor.SpringGreen.ToBrush(); + public static IImmutableSolidColorBrush SpringGreen => KnownColor.SpringGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SteelBlue => KnownColor.SteelBlue.ToBrush(); + public static IImmutableSolidColorBrush SteelBlue => KnownColor.SteelBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Tan => KnownColor.Tan.ToBrush(); + public static IImmutableSolidColorBrush Tan => KnownColor.Tan.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Teal => KnownColor.Teal.ToBrush(); + public static IImmutableSolidColorBrush Teal => KnownColor.Teal.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Thistle => KnownColor.Thistle.ToBrush(); + public static IImmutableSolidColorBrush Thistle => KnownColor.Thistle.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Tomato => KnownColor.Tomato.ToBrush(); + public static IImmutableSolidColorBrush Tomato => KnownColor.Tomato.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Transparent => KnownColor.Transparent.ToBrush(); + public static IImmutableSolidColorBrush Transparent => KnownColor.Transparent.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Turquoise => KnownColor.Turquoise.ToBrush(); + public static IImmutableSolidColorBrush Turquoise => KnownColor.Turquoise.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Violet => KnownColor.Violet.ToBrush(); + public static IImmutableSolidColorBrush Violet => KnownColor.Violet.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Wheat => KnownColor.Wheat.ToBrush(); + public static IImmutableSolidColorBrush Wheat => KnownColor.Wheat.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush White => KnownColor.White.ToBrush(); + public static IImmutableSolidColorBrush White => KnownColor.White.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush WhiteSmoke => KnownColor.WhiteSmoke.ToBrush(); + public static IImmutableSolidColorBrush WhiteSmoke => KnownColor.WhiteSmoke.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Yellow => KnownColor.Yellow.ToBrush(); + public static IImmutableSolidColorBrush Yellow => KnownColor.Yellow.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush YellowGreen => KnownColor.YellowGreen.ToBrush(); + public static IImmutableSolidColorBrush YellowGreen => KnownColor.YellowGreen.ToBrush(); } } diff --git a/src/Avalonia.Base/Media/ConicGradientBrush.cs b/src/Avalonia.Base/Media/ConicGradientBrush.cs index 4b50019ddc..bce7a5af19 100644 --- a/src/Avalonia.Base/Media/ConicGradientBrush.cs +++ b/src/Avalonia.Base/Media/ConicGradientBrush.cs @@ -47,7 +47,7 @@ namespace Avalonia.Media } /// - public override IBrush ToImmutable() + public override IImmutableBrush ToImmutable() { return new ImmutableConicGradientBrush(this); } diff --git a/src/Avalonia.Base/Media/DashStyle.cs b/src/Avalonia.Base/Media/DashStyle.cs index 3a30b2d32f..4749bfa401 100644 --- a/src/Avalonia.Base/Media/DashStyle.cs +++ b/src/Avalonia.Base/Media/DashStyle.cs @@ -4,6 +4,7 @@ using System.Collections.Specialized; using Avalonia.Animation; using Avalonia.Collections; using Avalonia.Media.Immutable; +using Avalonia.Reactive; #nullable enable @@ -17,8 +18,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty> DashesProperty = - AvaloniaProperty.Register>(nameof(Dashes)); + public static readonly StyledProperty?> DashesProperty = + AvaloniaProperty.Register?>(nameof(Dashes)); /// /// Defines the property. @@ -51,13 +52,11 @@ namespace Avalonia.Media static DashStyle() { - void RaiseInvalidated(AvaloniaPropertyChangedEventArgs e) - { - ((DashStyle)e.Sender).Invalidated?.Invoke(e.Sender, EventArgs.Empty); - } + var invalidateObserver = new AnonymousObserver( + static e => ((DashStyle)e.Sender).Invalidated?.Invoke(e.Sender, EventArgs.Empty)); - DashesProperty.Changed.Subscribe(RaiseInvalidated); - OffsetProperty.Changed.Subscribe(RaiseInvalidated); + DashesProperty.Changed.Subscribe(invalidateObserver); + OffsetProperty.Changed.Subscribe(invalidateObserver); } /// @@ -83,7 +82,7 @@ namespace Avalonia.Media /// /// Gets or sets the length of alternating dashes and gaps. /// - public AvaloniaList Dashes + public AvaloniaList? Dashes { get => GetValue(DashesProperty); set => SetValue(DashesProperty, value); @@ -98,7 +97,7 @@ namespace Avalonia.Media set => SetValue(OffsetProperty, value); } - IReadOnlyList IDashStyle.Dashes => Dashes; + IReadOnlyList? IDashStyle.Dashes => Dashes; /// /// Raised when the dash style changes. diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs index eabd7c8274..077816c645 100644 --- a/src/Avalonia.Base/Media/DrawingContext.cs +++ b/src/Avalonia.Base/Media/DrawingContext.cs @@ -279,7 +279,7 @@ namespace Avalonia.Media OpacityMask, } - public PushedState(DrawingContext context, PushedStateType type, Matrix matrix = default(Matrix)) + public PushedState(DrawingContext context, PushedStateType type, Matrix matrix = default) { if (context._states is null) throw new ObjectDisposedException(nameof(DrawingContext)); diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index e71f568207..7d3b4c056e 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -76,8 +76,8 @@ namespace Avalonia.Media { using (context.PushPreTransform(Transform?.Value ?? Matrix.Identity)) using (context.PushOpacity(Opacity)) - using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default(DrawingContext.PushedState)) - using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, GetBounds()) : default(DrawingContext.PushedState)) + using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default) + using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, GetBounds()) : default) { foreach (var drawing in Children) { @@ -461,9 +461,10 @@ namespace Avalonia.Media if (_rootDrawing == null) { - // When a DrawingGroup is set, it should be made the root if - // a root drawing didnt exist. - Contract.Requires(_currentDrawingGroup == null); + if (_currentDrawingGroup != null) + { + throw new NotSupportedException("When a DrawingGroup is set, it should be made the root if a root drawing didnt exist."); + } // If this is the first Drawing being added, avoid creating a DrawingGroup // and set this drawing as the root drawing. This optimizes the common diff --git a/src/Avalonia.Base/Media/ExperimentalAcrylicMaterial.cs b/src/Avalonia.Base/Media/ExperimentalAcrylicMaterial.cs index 0e485d0db8..22d5a29870 100644 --- a/src/Avalonia.Base/Media/ExperimentalAcrylicMaterial.cs +++ b/src/Avalonia.Base/Media/ExperimentalAcrylicMaterial.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Reactive; namespace Avalonia.Media { @@ -274,14 +275,12 @@ namespace Avalonia.Media protected static void AffectsRender(params AvaloniaProperty[] properties) where T : ExperimentalAcrylicMaterial { - static void Invalidate(AvaloniaPropertyChangedEventArgs e) - { - (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty); - } + var invalidateObserver = new AnonymousObserver( + static e => (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty)); foreach (var property in properties) { - property.Changed.Subscribe(e => Invalidate(e)); + property.Changed.Subscribe(invalidateObserver); } } diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index d92d003c2a..e82d5b7ba5 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -47,10 +47,7 @@ namespace Avalonia.Media return current; } - var fontManagerImpl = AvaloniaLocator.Current.GetService(); - - if (fontManagerImpl == null) - throw new InvalidOperationException("No font manager implementation was registered."); + var fontManagerImpl = AvaloniaLocator.Current.GetRequiredService(); current = new FontManager(fontManagerImpl); diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs index 138e8b79eb..774580415a 100644 --- a/src/Avalonia.Base/Media/FormattedText.cs +++ b/src/Avalonia.Base/Media/FormattedText.cs @@ -1379,7 +1379,7 @@ namespace Avalonia.Media } } - if (accumulatedBounds?.PlatformImpl == null || accumulatedBounds.PlatformImpl.Bounds.IsEmpty) + if (accumulatedBounds?.PlatformImpl == null || accumulatedBounds.PlatformImpl.Bounds.IsDefault) { return null; } diff --git a/src/Avalonia.Base/Media/Geometry.cs b/src/Avalonia.Base/Media/Geometry.cs index c31a6699c2..ab1bb3b9fa 100644 --- a/src/Avalonia.Base/Media/Geometry.cs +++ b/src/Avalonia.Base/Media/Geometry.cs @@ -1,11 +1,16 @@ using System; using Avalonia.Platform; +using System.ComponentModel; +using System.Globalization; +using Avalonia.Reactive; + namespace Avalonia.Media { /// /// Defines a geometric shape. - /// + /// + [TypeConverter(typeof(GeometryTypeConverter))] public abstract class Geometry : AvaloniaObject { /// @@ -30,7 +35,7 @@ namespace Avalonia.Media /// /// Gets the geometry's bounding rectangle. /// - public Rect Bounds => PlatformImpl?.Bounds ?? Rect.Empty; + public Rect Bounds => PlatformImpl?.Bounds ?? default; /// /// Gets the platform-specific implementation of the geometry. @@ -84,7 +89,7 @@ namespace Avalonia.Media /// /// The stroke thickness. /// The bounding rectangle. - public Rect GetRenderBounds(IPen pen) => PlatformImpl?.GetRenderBounds(pen) ?? Rect.Empty; + public Rect GetRenderBounds(IPen pen) => PlatformImpl?.GetRenderBounds(pen) ?? default; /// /// Indicates whether the geometry's fill contains the specified point. @@ -117,9 +122,10 @@ namespace Avalonia.Media /// protected static void AffectsGeometry(params AvaloniaProperty[] properties) { + var invalidateObserver = new AnonymousObserver(AffectsGeometryInvalidate); foreach (var property in properties) { - property.Changed.Subscribe(AffectsGeometryInvalidate); + property.Changed.Subscribe(invalidateObserver); } } @@ -199,4 +205,32 @@ namespace Avalonia.Media return new CombinedGeometry(combineMode, geometry1, geometry2, transform); } } + + public class GeometryTypeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + { + if (sourceType == typeof(string)) + { + return true; + } + return base.CanConvertFrom(context, sourceType); + } + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value is null) + { + throw GetConvertFromException(value); + } + string? source = value as string; + + if (source != null) + { + return Geometry.Parse(source); + } + + return base.ConvertFrom(context, culture, value); + } + } } diff --git a/src/Avalonia.Base/Media/GeometryDrawing.cs b/src/Avalonia.Base/Media/GeometryDrawing.cs index 7df7d25954..26cc2c3cab 100644 --- a/src/Avalonia.Base/Media/GeometryDrawing.cs +++ b/src/Avalonia.Base/Media/GeometryDrawing.cs @@ -69,7 +69,7 @@ namespace Avalonia.Media public override Rect GetBounds() { IPen pen = Pen ?? s_boundsPen; - return Geometry?.GetRenderBounds(pen) ?? Rect.Empty; + return Geometry?.GetRenderBounds(pen) ?? default; } } } diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index af9e458a28..811479fde8 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -918,7 +918,7 @@ namespace Avalonia.Media _glyphRunImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets); } - void IDisposable.Dispose() + public void Dispose() { _glyphRunImpl?.Dispose(); } diff --git a/src/Avalonia.Base/Media/GlyphRunDrawing.cs b/src/Avalonia.Base/Media/GlyphRunDrawing.cs index 7e0d5c3c81..242b9913fa 100644 --- a/src/Avalonia.Base/Media/GlyphRunDrawing.cs +++ b/src/Avalonia.Base/Media/GlyphRunDrawing.cs @@ -32,7 +32,7 @@ public override Rect GetBounds() { - return GlyphRun != null ? new Rect(GlyphRun.Size) : Rect.Empty; + return GlyphRun != null ? new Rect(GlyphRun.Size) : default; } } } diff --git a/src/Avalonia.Base/Media/GradientBrush.cs b/src/Avalonia.Base/Media/GradientBrush.cs index c84413ecbb..e1654a01b2 100644 --- a/src/Avalonia.Base/Media/GradientBrush.cs +++ b/src/Avalonia.Base/Media/GradientBrush.cs @@ -6,13 +6,14 @@ using System.ComponentModel; using Avalonia.Animation.Animators; using Avalonia.Collections; using Avalonia.Metadata; +using Avalonia.Reactive; namespace Avalonia.Media { /// /// Base class for brushes that draw with a gradient. /// - public abstract class GradientBrush : Brush, IGradientBrush + public abstract class GradientBrush : Brush, IGradientBrush, IMutableBrush { /// /// Defines the property. @@ -92,5 +93,7 @@ namespace Avalonia.Media { RaiseInvalidated(EventArgs.Empty); } + + public abstract IImmutableBrush ToImmutable(); } } diff --git a/src/Avalonia.Base/Media/IDashStyle.cs b/src/Avalonia.Base/Media/IDashStyle.cs index 7208216603..b988ad210a 100644 --- a/src/Avalonia.Base/Media/IDashStyle.cs +++ b/src/Avalonia.Base/Media/IDashStyle.cs @@ -12,7 +12,7 @@ namespace Avalonia.Media /// /// Gets or sets the length of alternating dashes and gaps. /// - IReadOnlyList Dashes { get; } + IReadOnlyList? Dashes { get; } /// /// Gets or sets how far in the dash sequence the stroke will start. diff --git a/src/Avalonia.Base/Media/IImmutableBrush.cs b/src/Avalonia.Base/Media/IImmutableBrush.cs new file mode 100644 index 0000000000..5781b0117a --- /dev/null +++ b/src/Avalonia.Base/Media/IImmutableBrush.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Media; + +/// +/// Represents an immutable brush which can be safely used with various threading contexts +/// +public interface IImmutableBrush : IBrush +{ + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/IMutableBrush.cs b/src/Avalonia.Base/Media/IMutableBrush.cs index fef124ba36..f128aba4df 100644 --- a/src/Avalonia.Base/Media/IMutableBrush.cs +++ b/src/Avalonia.Base/Media/IMutableBrush.cs @@ -7,12 +7,12 @@ namespace Avalonia.Media /// Represents a mutable brush which can return an immutable clone of itself. /// [NotClientImplementable] - public interface IMutableBrush : IBrush, IAffectsRender + internal interface IMutableBrush : IBrush, IAffectsRender { /// /// Creates an immutable clone of the brush. /// /// The immutable clone. - IBrush ToImmutable(); + internal IImmutableBrush ToImmutable(); } } diff --git a/src/Avalonia.Base/Media/ISolidColorBrush.cs b/src/Avalonia.Base/Media/ISolidColorBrush.cs index 29e11210f1..f58768ef09 100644 --- a/src/Avalonia.Base/Media/ISolidColorBrush.cs +++ b/src/Avalonia.Base/Media/ISolidColorBrush.cs @@ -13,4 +13,13 @@ namespace Avalonia.Media /// Color Color { get; } } + + /// + /// Fills an area with a solid color. + /// + [NotClientImplementable] + public interface IImmutableSolidColorBrush : ISolidColorBrush, IImmutableBrush + { + + } } diff --git a/src/Avalonia.Base/Media/ImageBrush.cs b/src/Avalonia.Base/Media/ImageBrush.cs index 19dcf00901..2f2a0fb627 100644 --- a/src/Avalonia.Base/Media/ImageBrush.cs +++ b/src/Avalonia.Base/Media/ImageBrush.cs @@ -6,7 +6,7 @@ namespace Avalonia.Media /// /// Paints an area with an . /// - public class ImageBrush : TileBrush, IImageBrush + public class ImageBrush : TileBrush, IImageBrush, IMutableBrush { /// /// Defines the property. @@ -45,7 +45,7 @@ namespace Avalonia.Media } /// - public override IBrush ToImmutable() + public IImmutableBrush ToImmutable() { return new ImmutableImageBrush(this); } diff --git a/src/Avalonia.Base/Media/ImageDrawing.cs b/src/Avalonia.Base/Media/ImageDrawing.cs index 82f97b52b4..d3e5c4841b 100644 --- a/src/Avalonia.Base/Media/ImageDrawing.cs +++ b/src/Avalonia.Base/Media/ImageDrawing.cs @@ -42,7 +42,7 @@ namespace Avalonia.Media var imageSource = ImageSource; var rect = Rect; - if (imageSource is object && !rect.IsEmpty) + if (imageSource is object && !rect.IsDefault) { context.DrawImage(imageSource, rect); } diff --git a/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs b/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs index 70f9fbf567..525a543b70 100644 --- a/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs @@ -78,8 +78,8 @@ namespace Avalonia.Media.Imaging get { if (Source is not IBitmap bmp) - return Size.Empty; - if (SourceRect.IsEmpty) + return default; + if (SourceRect.IsDefault) return Source.Size; return SourceRect.Size.ToSizeWithDpi(bmp.Dpi); } diff --git a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs new file mode 100644 index 0000000000..eb6f105680 --- /dev/null +++ b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs @@ -0,0 +1,373 @@ +using System; +using System.Collections.Generic; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Threading; +using Avalonia.Utilities; +using Avalonia.Media.Imaging; +using Avalonia.Media.Immutable; + +namespace Avalonia.Media +{ + public sealed class ImmediateDrawingContext : IDisposable, IOptionalFeatureProvider + { + private readonly bool _ownsImpl; + private int _currentLevel; + + private static ThreadSafeObjectPool> StateStackPool { get; } = + ThreadSafeObjectPool>.Default; + + private static ThreadSafeObjectPool> TransformStackPool { get; } = + ThreadSafeObjectPool>.Default; + + private Stack? _states = StateStackPool.Get(); + + private Stack? _transformContainers = TransformStackPool.Get(); + + readonly struct TransformContainer + { + public readonly Matrix LocalTransform; + public readonly Matrix ContainerTransform; + + public TransformContainer(Matrix localTransform, Matrix containerTransform) + { + LocalTransform = localTransform; + ContainerTransform = containerTransform; + } + } + + internal ImmediateDrawingContext(IDrawingContextImpl impl, bool ownsImpl) + { + _ownsImpl = ownsImpl; + PlatformImpl = impl; + _currentContainerTransform = impl.Transform; + } + + public IDrawingContextImpl PlatformImpl { get; } + + private Matrix _currentTransform = Matrix.Identity; + + private Matrix _currentContainerTransform; + + /// + /// Gets the current transform of the drawing context. + /// + public Matrix CurrentTransform + { + get { return _currentTransform; } + private set + { + _currentTransform = value; + var transform = _currentTransform * _currentContainerTransform; + PlatformImpl.Transform = transform; + } + } + + /// + /// Draws an bitmap. + /// + /// The bitmap. + /// The rect in the output to draw to. + public void DrawBitmap(IBitmap source, Rect rect) + { + _ = source ?? throw new ArgumentNullException(nameof(source)); + DrawBitmap(source, new Rect(source.Size), rect); + } + + /// + /// Draws an image. + /// + /// The bitmap. + /// The rect in the image to draw. + /// The rect in the output to draw to. + /// The bitmap interpolation mode. + public void DrawBitmap(IBitmap source, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default) + { + _ = source ?? throw new ArgumentNullException(nameof(source)); + PlatformImpl.DrawBitmap(source.PlatformImpl, 1, sourceRect, destRect, bitmapInterpolationMode); + } + + /// + /// Draws a line. + /// + /// The stroke pen. + /// The first point of the line. + /// The second point of the line. + public void DrawLine(ImmutablePen pen, Point p1, Point p2) + { + if (PenIsVisible(pen)) + { + PlatformImpl.DrawLine(pen, p1, p2); + } + } + + /// + /// Draws a rectangle with the specified Brush and Pen. + /// + /// The brush used to fill the rectangle, or null for no fill. + /// The pen used to stroke the rectangle, or null for no stroke. + /// The rectangle bounds. + /// The radius in the X dimension of the rounded corners. + /// This value will be clamped to the range of 0 to Width/2 + /// + /// The radius in the Y dimension of the rounded corners. + /// This value will be clamped to the range of 0 to Height/2 + /// + /// Box shadow effect parameters + /// + /// The brush and the pen can both be null. If the brush is null, then no fill is performed. + /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. + /// + public void DrawRectangle(IImmutableBrush? brush, ImmutablePen? pen, Rect rect, double radiusX = 0, double radiusY = 0, + BoxShadows boxShadows = default) + { + if (brush == null && !PenIsVisible(pen)) + { + return; + } + + if (!MathUtilities.IsZero(radiusX)) + { + radiusX = Math.Min(radiusX, rect.Width / 2); + } + + if (!MathUtilities.IsZero(radiusY)) + { + radiusY = Math.Min(radiusY, rect.Height / 2); + } + + PlatformImpl.DrawRectangle(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadows); + } + + /// + /// Draws the outline of a rectangle. + /// + /// The pen. + /// The rectangle bounds. + /// The corner radius. + public void DrawRectangle(ImmutablePen pen, Rect rect, float cornerRadius = 0.0f) + { + DrawRectangle(null, pen, rect, cornerRadius, cornerRadius); + } + + /// + /// Draws an ellipse with the specified Brush and Pen. + /// + /// The brush used to fill the ellipse, or null for no fill. + /// The pen used to stroke the ellipse, or null for no stroke. + /// The location of the center of the ellipse. + /// The horizontal radius of the ellipse. + /// The vertical radius of the ellipse. + /// + /// The brush and the pen can both be null. If the brush is null, then no fill is performed. + /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. + /// + public void DrawEllipse(IImmutableBrush? brush, ImmutablePen? pen, Point center, double radiusX, double radiusY) + { + if (brush == null && !PenIsVisible(pen)) + { + return; + } + + var originX = center.X - radiusX; + var originY = center.Y - radiusY; + var width = radiusX * 2; + var height = radiusY * 2; + + PlatformImpl.DrawEllipse(brush, pen, new Rect(originX, originY, width, height)); + } + + /// + /// Draws a glyph run. + /// + /// The foreground brush. + /// The glyph run. + public void DrawGlyphRun(IImmutableBrush foreground, GlyphRun glyphRun) + { + _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun)); + + PlatformImpl.DrawGlyphRun(foreground, glyphRun); + } + + /// + /// Draws a filled rectangle. + /// + /// The brush. + /// The rectangle bounds. + /// The corner radius. + public void FillRectangle(IImmutableBrush brush, Rect rect, float cornerRadius = 0.0f) + { + DrawRectangle(brush, null, rect, cornerRadius, cornerRadius); + } + + public readonly record struct PushedState : IDisposable + { + private readonly int _level; + private readonly ImmediateDrawingContext _context; + private readonly Matrix _matrix; + private readonly PushedStateType _type; + + public enum PushedStateType + { + None, + Matrix, + Opacity, + Clip, + MatrixContainer, + GeometryClip, + OpacityMask, + } + + internal PushedState(ImmediateDrawingContext context, PushedStateType type, Matrix matrix = default) + { + if (context._states is null) + throw new ObjectDisposedException(nameof(ImmediateDrawingContext)); + + _context = context; + _type = type; + _matrix = matrix; + _level = context._currentLevel += 1; + context._states.Push(this); + } + + public void Dispose() + { + if (_type == PushedStateType.None) + return; + if (_context._states is null || _context._transformContainers is null) + throw new ObjectDisposedException(nameof(DrawingContext)); + if (_context._currentLevel != _level) + throw new InvalidOperationException("Wrong Push/Pop state order"); + _context._currentLevel--; + _context._states.Pop(); + if (_type == PushedStateType.Matrix) + _context.CurrentTransform = _matrix; + else if (_type == PushedStateType.Clip) + _context.PlatformImpl.PopClip(); + else if (_type == PushedStateType.Opacity) + _context.PlatformImpl.PopOpacity(); + else if (_type == PushedStateType.GeometryClip) + _context.PlatformImpl.PopGeometryClip(); + else if (_type == PushedStateType.OpacityMask) + _context.PlatformImpl.PopOpacityMask(); + else if (_type == PushedStateType.MatrixContainer) + { + var cont = _context._transformContainers.Pop(); + _context._currentContainerTransform = cont.ContainerTransform; + _context.CurrentTransform = cont.LocalTransform; + } + } + } + + + public PushedState PushClip(RoundedRect clip) + { + PlatformImpl.PushClip(clip); + return new PushedState(this, PushedState.PushedStateType.Clip); + } + + /// + /// Pushes a clip rectangle. + /// + /// The clip rectangle. + /// A disposable used to undo the clip rectangle. + public PushedState PushClip(Rect clip) + { + PlatformImpl.PushClip(clip); + return new PushedState(this, PushedState.PushedStateType.Clip); + } + + /// + /// Pushes an opacity value. + /// + /// The opacity. + /// A disposable used to undo the opacity. + public PushedState PushOpacity(double opacity) + //TODO: Eliminate platform-specific push opacity call + { + PlatformImpl.PushOpacity(opacity); + return new PushedState(this, PushedState.PushedStateType.Opacity); + } + + /// + /// Pushes an opacity mask. + /// + /// The opacity mask. + /// + /// The size of the brush's target area. TODO: Are we sure this is needed? + /// + /// A disposable to undo the opacity mask. + public PushedState PushOpacityMask(IImmutableBrush mask, Rect bounds) + { + PlatformImpl.PushOpacityMask(mask, bounds); + return new PushedState(this, PushedState.PushedStateType.OpacityMask); + } + + /// + /// Pushes a matrix post-transformation. + /// + /// The matrix + /// A disposable used to undo the transformation. + public PushedState PushPostTransform(Matrix matrix) => PushSetTransform(CurrentTransform * matrix); + + /// + /// Pushes a matrix pre-transformation. + /// + /// The matrix + /// A disposable used to undo the transformation. + public PushedState PushPreTransform(Matrix matrix) => PushSetTransform(matrix * CurrentTransform); + + /// + /// Sets the current matrix transformation. + /// + /// The matrix + /// A disposable used to undo the transformation. + public PushedState PushSetTransform(Matrix matrix) + { + var oldMatrix = CurrentTransform; + CurrentTransform = matrix; + + return new PushedState(this, PushedState.PushedStateType.Matrix, oldMatrix); + } + + /// + /// Pushes a new transform context. + /// + /// A disposable used to undo the transformation. + public PushedState PushTransformContainer() + { + if (_transformContainers is null) + throw new ObjectDisposedException(nameof(DrawingContext)); + _transformContainers.Push(new TransformContainer(CurrentTransform, _currentContainerTransform)); + _currentContainerTransform = CurrentTransform * _currentContainerTransform; + _currentTransform = Matrix.Identity; + return new PushedState(this, PushedState.PushedStateType.MatrixContainer); + } + + /// + /// Disposes of any resources held by the . + /// + public void Dispose() + { + if (_states is null || _transformContainers is null) + throw new ObjectDisposedException(nameof(DrawingContext)); + while (_states.Count != 0) + _states.Peek().Dispose(); + StateStackPool.Return(_states); + _states = null; + if (_transformContainers.Count != 0) + throw new InvalidOperationException("Transform container stack is non-empty"); + TransformStackPool.Return(_transformContainers); + _transformContainers = null; + if (_ownsImpl) + PlatformImpl.Dispose(); + } + + private static bool PenIsVisible(IPen? pen) + { + return pen?.Brush != null && pen.Thickness > 0; + } + + public object? TryGetFeature(Type type) => PlatformImpl.GetFeature(type); + } +} diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs b/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs index 82485c13b0..1f53f06955 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs @@ -17,7 +17,7 @@ namespace Avalonia.Media.Immutable /// /// The dashes collection. /// The dash sequence offset. - public ImmutableDashStyle(IEnumerable dashes, double offset) + public ImmutableDashStyle(IEnumerable? dashes, double offset) { _dashes = dashes?.ToArray() ?? Array.Empty(); Offset = offset; @@ -69,7 +69,7 @@ namespace Avalonia.Media.Immutable return hashCode; } - private static bool SequenceEqual(IReadOnlyList left, IReadOnlyList right) + private static bool SequenceEqual(IReadOnlyList left, IReadOnlyList? right) { if (ReferenceEquals(left, right)) { diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableGradientBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableGradientBrush.cs index 1e95acbf22..c86d86d20a 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableGradientBrush.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableGradientBrush.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media.Immutable /// /// A brush that draws with a gradient. /// - public abstract class ImmutableGradientBrush : IGradientBrush + public abstract class ImmutableGradientBrush : IGradientBrush, IImmutableBrush { /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Base/Media/Immutable/ImmutablePen.cs b/src/Avalonia.Base/Media/Immutable/ImmutablePen.cs index 8a53eaf7b8..ef4468fccf 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutablePen.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutablePen.cs @@ -38,15 +38,13 @@ namespace Avalonia.Media.Immutable /// The line join. /// The miter limit. public ImmutablePen( - IBrush? brush, + IImmutableBrush? brush, double thickness = 1.0, ImmutableDashStyle? dashStyle = null, PenLineCap lineCap = PenLineCap.Flat, PenLineJoin lineJoin = PenLineJoin.Miter, double miterLimit = 10.0) { - Debug.Assert(!(brush is IMutableBrush)); - Brush = brush; Thickness = thickness; LineCap = lineCap; diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableSolidColorBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableSolidColorBrush.cs index 6755dfd236..4e623f02c5 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableSolidColorBrush.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableSolidColorBrush.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media.Immutable /// /// Fills an area with a solid color. /// - public class ImmutableSolidColorBrush : ISolidColorBrush, IEquatable + public class ImmutableSolidColorBrush : IImmutableSolidColorBrush, IEquatable { /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs index 6df27872fc..1ee52365e0 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media.Immutable /// /// A brush which displays a repeating image. /// - public abstract class ImmutableTileBrush : ITileBrush + public abstract class ImmutableTileBrush : ITileBrush, IImmutableBrush { /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Base/Media/KnownColors.cs b/src/Avalonia.Base/Media/KnownColors.cs index ae2fca5a60..64cf1ef2f1 100644 --- a/src/Avalonia.Base/Media/KnownColors.cs +++ b/src/Avalonia.Base/Media/KnownColors.cs @@ -10,7 +10,7 @@ namespace Avalonia.Media private static readonly IReadOnlyDictionary _knownColorNames; private static readonly IReadOnlyDictionary _knownColors; #if !BUILDTASK - private static readonly Dictionary _knownBrushes; + private static readonly Dictionary _knownBrushes; #endif [GenerateEnumValueDictionary()] @@ -39,7 +39,7 @@ namespace Avalonia.Media _knownColors = knownColors; #if !BUILDTASK - _knownBrushes = new Dictionary(); + _knownBrushes = new (); #endif } @@ -72,7 +72,7 @@ namespace Avalonia.Media } #if !BUILDTASK - public static ISolidColorBrush ToBrush(this KnownColor color) + public static IImmutableSolidColorBrush ToBrush(this KnownColor color) { lock (_knownBrushes) { diff --git a/src/Avalonia.Base/Media/LinearGradientBrush.cs b/src/Avalonia.Base/Media/LinearGradientBrush.cs index a51adf0949..9b9ce5e4b7 100644 --- a/src/Avalonia.Base/Media/LinearGradientBrush.cs +++ b/src/Avalonia.Base/Media/LinearGradientBrush.cs @@ -47,7 +47,7 @@ namespace Avalonia.Media } /// - public override IBrush ToImmutable() + public override IImmutableBrush ToImmutable() { return new ImmutableLinearGradientBrush(this); } diff --git a/src/Avalonia.Base/Media/MatrixTransform.cs b/src/Avalonia.Base/Media/MatrixTransform.cs index c61acb730c..22f39b1ee2 100644 --- a/src/Avalonia.Base/Media/MatrixTransform.cs +++ b/src/Avalonia.Base/Media/MatrixTransform.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Reactive; using Avalonia.VisualTree; namespace Avalonia.Media diff --git a/src/Avalonia.Base/Media/RadialGradientBrush.cs b/src/Avalonia.Base/Media/RadialGradientBrush.cs index 16c367c0d0..f803051676 100644 --- a/src/Avalonia.Base/Media/RadialGradientBrush.cs +++ b/src/Avalonia.Base/Media/RadialGradientBrush.cs @@ -67,7 +67,7 @@ namespace Avalonia.Media } /// - public override IBrush ToImmutable() + public override IImmutableBrush ToImmutable() { return new ImmutableRadialGradientBrush(this); } diff --git a/src/Avalonia.Base/Media/RotateTransform.cs b/src/Avalonia.Base/Media/RotateTransform.cs index 3bd409149c..ab3ea6770f 100644 --- a/src/Avalonia.Base/Media/RotateTransform.cs +++ b/src/Avalonia.Base/Media/RotateTransform.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Reactive; using Avalonia.VisualTree; namespace Avalonia.Media diff --git a/src/Avalonia.Base/Media/ScaleTransform.cs b/src/Avalonia.Base/Media/ScaleTransform.cs index d4c1a7f993..fca2f4bf2a 100644 --- a/src/Avalonia.Base/Media/ScaleTransform.cs +++ b/src/Avalonia.Base/Media/ScaleTransform.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Reactive; using Avalonia.VisualTree; namespace Avalonia.Media @@ -25,8 +26,6 @@ namespace Avalonia.Media /// public ScaleTransform() { - this.GetObservable(ScaleXProperty).Subscribe(_ => RaiseChanged()); - this.GetObservable(ScaleYProperty).Subscribe(_ => RaiseChanged()); } /// @@ -63,5 +62,15 @@ namespace Avalonia.Media /// Gets the transform's . /// public override Matrix Value => Matrix.CreateScale(ScaleX, ScaleY); + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ScaleXProperty || change.Property == ScaleYProperty) + { + RaiseChanged(); + } + } } } diff --git a/src/Avalonia.Base/Media/SkewTransform.cs b/src/Avalonia.Base/Media/SkewTransform.cs index 066f5371c3..d268bee778 100644 --- a/src/Avalonia.Base/Media/SkewTransform.cs +++ b/src/Avalonia.Base/Media/SkewTransform.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Reactive; using Avalonia.VisualTree; namespace Avalonia.Media @@ -25,8 +26,6 @@ namespace Avalonia.Media /// public SkewTransform() { - this.GetObservable(AngleXProperty).Subscribe(_ => RaiseChanged()); - this.GetObservable(AngleYProperty).Subscribe(_ => RaiseChanged()); } /// @@ -62,5 +61,15 @@ namespace Avalonia.Media /// Gets the transform's . /// public override Matrix Value => Matrix.CreateSkew(Matrix.ToRadians(AngleX), Matrix.ToRadians(AngleY)); + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == AngleXProperty || change.Property == AngleYProperty) + { + RaiseChanged(); + } + } } } diff --git a/src/Avalonia.Base/Media/SolidColorBrush.cs b/src/Avalonia.Base/Media/SolidColorBrush.cs index 962819a1a1..d8e25fe748 100644 --- a/src/Avalonia.Base/Media/SolidColorBrush.cs +++ b/src/Avalonia.Base/Media/SolidColorBrush.cs @@ -6,7 +6,7 @@ namespace Avalonia.Media /// /// Fills an area with a solid color. /// - public class SolidColorBrush : Brush, ISolidColorBrush + public class SolidColorBrush : Brush, ISolidColorBrush, IMutableBrush { /// /// Defines the property. @@ -80,7 +80,7 @@ namespace Avalonia.Media } /// - public override IBrush ToImmutable() + public IImmutableBrush ToImmutable() { return new ImmutableSolidColorBrush(this); } diff --git a/src/Avalonia.Base/Media/TextDecoration.cs b/src/Avalonia.Base/Media/TextDecoration.cs index 0a7328125a..d6b2841214 100644 --- a/src/Avalonia.Base/Media/TextDecoration.cs +++ b/src/Avalonia.Base/Media/TextDecoration.cs @@ -1,5 +1,10 @@ -using Avalonia.Collections; +using System; +using System.Collections.Generic; +using Avalonia.Collections; +using Avalonia.Collections.Pooled; using Avalonia.Media.TextFormatting; +using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Media { @@ -209,6 +214,51 @@ namespace Avalonia.Media var pen = new Pen(Stroke ?? defaultBrush, thickness, new DashStyle(StrokeDashArray, StrokeDashOffset), StrokeLineCap); + if (Location != TextDecorationLocation.Strikethrough) + { + var offsetY = glyphRun.BaselineOrigin.Y - origin.Y; + + var intersections = glyphRun.GlyphRunImpl.GetIntersections((float)(thickness * 0.5d - offsetY), (float)(thickness * 1.5d - offsetY)); + + if (intersections != null && intersections.Count > 0) + { + var last = baselineOrigin.X; + var finalPos = last + glyphRun.Size.Width; + var end = last; + + var points = new List(); + + //math is taken from chrome's source code. + for (var i = 0; i < intersections.Count; i += 2) + { + var start = intersections[i] - thickness; + end = intersections[i + 1] + thickness; + if (start > last && last + textMetrics.FontRenderingEmSize / 12 < start) + { + points.Add(last); + points.Add(start); + } + last = end; + } + + if (end < finalPos) + { + points.Add(end); + points.Add(finalPos); + } + + for (var i = 0; i < points.Count; i += 2) + { + var a = new Point(points[i], origin.Y); + var b = new Point(points[i + 1], origin.Y); + + drawingContext.DrawLine(pen, a, b); + } + + return; + } + } + drawingContext.DrawLine(pen, origin, origin + new Point(glyphRun.Metrics.Width, 0)); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs index d76f212f26..499026e8b3 100644 --- a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs +++ b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs @@ -140,7 +140,7 @@ namespace Avalonia.Media.TextFormatting throw new ArgumentOutOfRangeException(nameof(index)); } #endif - return Span[index]; + return CharacterBuffer.Span[CharacterBufferReference.OffsetToFirstChar + index]; } } @@ -157,33 +157,23 @@ namespace Avalonia.Media.TextFormatting /// /// Gets a span from the character buffer range /// - public ReadOnlySpan Span => - CharacterBufferReference.CharacterBuffer.Span.Slice(CharacterBufferReference.OffsetToFirstChar, Length); + public ReadOnlySpan Span => CharacterBuffer.Span.Slice(OffsetToFirstChar, Length); /// /// Gets the character memory buffer /// - internal ReadOnlyMemory CharacterBuffer - { - get { return CharacterBufferReference.CharacterBuffer; } - } + internal ReadOnlyMemory CharacterBuffer => CharacterBufferReference.CharacterBuffer; /// /// Gets the character offset relative to the beginning of buffer to /// the first character of the run /// - internal int OffsetToFirstChar - { - get { return CharacterBufferReference.OffsetToFirstChar; } - } + internal int OffsetToFirstChar => CharacterBufferReference.OffsetToFirstChar; /// /// Indicate whether the character buffer range is empty /// - internal bool IsEmpty - { - get { return CharacterBufferReference.CharacterBuffer.Length == 0 || Length <= 0; } - } + internal bool IsEmpty => CharacterBuffer.Length == 0 || Length <= 0; internal CharacterBufferRange Take(int length) { @@ -217,9 +207,7 @@ namespace Avalonia.Media.TextFormatting return new CharacterBufferRange(new CharacterBufferReference(), 0); } - var characterBufferReference = new CharacterBufferReference( - CharacterBufferReference.CharacterBuffer, - CharacterBufferReference.OffsetToFirstChar + length); + var characterBufferReference = new CharacterBufferReference(CharacterBuffer, OffsetToFirstChar + length); return new CharacterBufferRange(characterBufferReference, Length - length); } @@ -280,14 +268,8 @@ namespace Avalonia.Media.TextFormatting int IReadOnlyCollection.Count => Length; - public IEnumerator GetEnumerator() - { - return new ImmutableReadOnlyListStructEnumerator(this); - } + public IEnumerator GetEnumerator() => new ImmutableReadOnlyListStructEnumerator(this); - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs index e745a873a2..49d94b511d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs +++ b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs @@ -8,7 +8,6 @@ namespace Avalonia.Media.TextFormatting internal readonly struct FormattedTextSource : ITextSource { private readonly CharacterBufferRange _text; - private readonly int length; private readonly TextRunProperties _defaultProperties; private readonly IReadOnlyList>? _textModifier; @@ -133,7 +132,7 @@ namespace Avalonia.Media.TextFormatting { var grapheme = graphemeEnumerator.Current; - finalLength += grapheme.Text.Length; + finalLength += grapheme.Length; if (finalLength >= length) { diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index 3c3a46c209..21e8ce089a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -91,7 +91,7 @@ namespace Avalonia.Media.TextFormatting continue; } - if (textRun is ShapedTextCharacters shapedText) + if (textRun is ShapedTextRun shapedText) { var glyphRun = shapedText.GlyphRun; var shapedBuffer = shapedText.ShapedBuffer; diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs index 902b897240..af896b426d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs @@ -1,17 +1,24 @@ using System; +using System.Buffers; using System.Collections.Generic; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { - public sealed class ShapedBuffer : IList + public sealed class ShapedBuffer : IList, IDisposable { private static readonly IComparer s_clusterComparer = new CompareClusters(); + private bool _bufferRented; public ShapedBuffer(CharacterBufferRange characterBufferRange, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) : - this(characterBufferRange, new GlyphInfo[bufferLength], glyphTypeface, fontRenderingEmSize, bidiLevel) + this(characterBufferRange, + new ArraySlice(ArrayPool.Shared.Rent(bufferLength), 0, bufferLength), + glyphTypeface, + fontRenderingEmSize, + bidiLevel) { - + _bufferRented = true; + Length = bufferLength; } internal ShapedBuffer(CharacterBufferRange characterBufferRange, ArraySlice glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) @@ -21,11 +28,12 @@ namespace Avalonia.Media.TextFormatting GlyphTypeface = glyphTypeface; FontRenderingEmSize = fontRenderingEmSize; BidiLevel = bidiLevel; + Length = GlyphInfos.Length; } internal ArraySlice GlyphInfos { get; } - public int Length => GlyphInfos.Length; + public int Length { get; } public IGlyphTypeface GlyphTypeface { get; } @@ -260,6 +268,23 @@ namespace Avalonia.Media.TextFormatting System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); } + + public void Dispose() + { + GC.SuppressFinalize(this); + if (_bufferRented) + { + GlyphInfos.ReturnRent(); + } + } + + ~ShapedBuffer() + { + if (_bufferRented) + { + GlyphInfos.ReturnRent(); + } + } } public readonly record struct GlyphInfo diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs similarity index 90% rename from src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs rename to src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index 3035eb7b18..665723b284 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -6,11 +6,11 @@ namespace Avalonia.Media.TextFormatting /// /// A text run that holds shaped characters. /// - public sealed class ShapedTextCharacters : DrawableTextRun + public sealed class ShapedTextRun : DrawableTextRun, IDisposable { private GlyphRun? _glyphRun; - public ShapedTextCharacters(ShapedBuffer shapedBuffer, TextRunProperties properties) + public ShapedTextRun(ShapedBuffer shapedBuffer, TextRunProperties properties) { ShapedBuffer = shapedBuffer; CharacterBufferReference = shapedBuffer.CharacterBufferRange.CharacterBufferReference; @@ -155,7 +155,7 @@ namespace Avalonia.Media.TextFormatting return length > 0; } - internal SplitResult Split(int length) + internal SplitResult Split(int length) { if (IsReversed) { @@ -171,7 +171,7 @@ namespace Avalonia.Media.TextFormatting var splitBuffer = ShapedBuffer.Split(length); - var first = new ShapedTextCharacters(splitBuffer.First, Properties); + var first = new ShapedTextRun(splitBuffer.First, Properties); #if DEBUG @@ -182,9 +182,9 @@ namespace Avalonia.Media.TextFormatting #endif - var second = new ShapedTextCharacters(splitBuffer.Second!, Properties); + var second = new ShapedTextRun(splitBuffer.Second!, Properties); - return new SplitResult(first, second); + return new SplitResult(first, second); } internal GlyphRun CreateGlyphRun() @@ -199,5 +199,11 @@ namespace Avalonia.Media.TextFormatting ShapedBuffer.GlyphClusters, BidiLevel); } + + public void Dispose() + { + _glyphRun?.Dispose(); + ShapedBuffer.Dispose(); + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 0be753bd04..1a48151834 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -91,12 +91,12 @@ namespace Avalonia.Media.TextFormatting public override TextRunProperties Properties { get; } /// - /// Gets a list of . + /// Gets a list of . /// /// The shapeable text characters. - internal IReadOnlyList GetShapeableCharacters(CharacterBufferRange characterBufferRange, sbyte biDiLevel, ref TextRunProperties? previousProperties) + internal IReadOnlyList GetShapeableCharacters(CharacterBufferRange characterBufferRange, sbyte biDiLevel, ref TextRunProperties? previousProperties) { - var shapeableCharacters = new List(2); + var shapeableCharacters = new List(2); while (characterBufferRange.Length > 0) { @@ -120,7 +120,7 @@ namespace Avalonia.Media.TextFormatting /// The bidi level of the run. /// /// A list of shapeable text runs. - private static ShapeableTextCharacters CreateShapeableRun(CharacterBufferRange characterBufferRange, + private static UnshapedTextRun CreateShapeableRun(CharacterBufferRange characterBufferRange, TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties) { var defaultTypeface = defaultProperties.Typeface; @@ -133,12 +133,12 @@ namespace Avalonia.Media.TextFormatting { if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, null, out var fallbackCount, out _)) { - return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, fallbackCount, + return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, fallbackCount, defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); } } - return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface), + return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface), biDiLevel); } @@ -146,7 +146,7 @@ namespace Avalonia.Media.TextFormatting { if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, defaultTypeface, out count, out _)) { - return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, + return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); } } @@ -176,7 +176,7 @@ namespace Avalonia.Media.TextFormatting if (matchFound && TryGetShapeableLength(characterBufferRange, currentTypeface, defaultTypeface, out count, out _)) { //Fallback found - return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface), + return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface), biDiLevel); } @@ -196,10 +196,10 @@ namespace Avalonia.Media.TextFormatting break; } - count += grapheme.Text.Length; + count += grapheme.Length; } - return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties, biDiLevel); + return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties, biDiLevel); } /// @@ -264,7 +264,7 @@ namespace Avalonia.Media.TextFormatting } } - length += currentGrapheme.Text.Length; + length += currentGrapheme.Length; } return length > 0; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs index f677617b14..01804e1ce3 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs @@ -21,6 +21,6 @@ namespace Avalonia.Media.TextFormatting /// Collapses given text line. /// /// Text line to collapse. - public abstract List? Collapse(TextLine textLine); + public abstract List? Collapse(TextLine textLine); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index a1b8985b43..9c201bda22 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -5,9 +5,11 @@ namespace Avalonia.Media.TextFormatting { internal static class TextEllipsisHelper { - public static List? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis) + public static List? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis) { - if (textLine.TextRuns is not List textRuns || textRuns.Count == 0) + var textRuns = textLine.TextRuns; + + if (textRuns == null || textRuns.Count == 0) { return null; } @@ -20,7 +22,7 @@ namespace Avalonia.Media.TextFormatting if (properties.Width < shapedSymbol.GlyphRun.Size.Width) { //Not enough space to fit in the symbol - return new List(0); + return new List(0); } var availableWidth = properties.Width - shapedSymbol.Size.Width; @@ -31,7 +33,7 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { - case ShapedTextCharacters shapedRun: + case ShapedTextRun shapedRun: { currentWidth += shapedRun.Size.Width; @@ -70,11 +72,11 @@ namespace Avalonia.Media.TextFormatting collapsedLength += measuredLength; - var collapsedRuns = new List(textRuns.Count); + var collapsedRuns = new List(textRuns.Count); if (collapsedLength > 0) { - var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength); + var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength); collapsedRuns.AddRange(splitResult.First); } @@ -84,22 +86,21 @@ namespace Avalonia.Media.TextFormatting return collapsedRuns; } - availableWidth -= currentRun.Size.Width; - + availableWidth -= shapedRun.Size.Width; break; } - case { } drawableRun: + case DrawableTextRun drawableRun: { //The whole run needs to fit into available space if (currentWidth + drawableRun.Size.Width > availableWidth) { - var collapsedRuns = new List(textRuns.Count); + var collapsedRuns = new List(textRuns.Count); if (collapsedLength > 0) { - var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength); + var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength); collapsedRuns.AddRange(splitResult.First); } @@ -109,6 +110,8 @@ namespace Avalonia.Media.TextFormatting return collapsedRuns; } + availableWidth -= drawableRun.Size.Width; + break; } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 93eb4811b9..517372648f 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utilities; @@ -17,20 +16,20 @@ namespace Avalonia.Media.TextFormatting var textWrapping = paragraphProperties.TextWrapping; FlowDirection resolvedFlowDirection; TextLineBreak? nextLineBreak = null; - List drawableTextRuns; + IReadOnlyList textRuns; - var textRuns = FetchTextRuns(textSource, firstTextSourceIndex, + var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, out var textEndOfLine, out var textSourceLength); if (previousLineBreak?.RemainingRuns != null) { resolvedFlowDirection = previousLineBreak.FlowDirection; - drawableTextRuns = previousLineBreak.RemainingRuns.ToList(); + textRuns = previousLineBreak.RemainingRuns; nextLineBreak = previousLineBreak; } else { - drawableTextRuns = ShapeTextRuns(textRuns, paragraphProperties, out resolvedFlowDirection); + textRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, out resolvedFlowDirection); if (nextLineBreak == null && textEndOfLine != null) { @@ -44,7 +43,7 @@ namespace Avalonia.Media.TextFormatting { case TextWrapping.NoWrap: { - textLine = new TextLineImpl(drawableTextRuns, firstTextSourceIndex, textSourceLength, + textLine = new TextLineImpl(textRuns, firstTextSourceIndex, textSourceLength, paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); textLine.FinalizeLine(); @@ -54,7 +53,7 @@ namespace Avalonia.Media.TextFormatting case TextWrapping.WrapWithOverflow: case TextWrapping.Wrap: { - textLine = PerformTextWrapping(drawableTextRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties, + textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); break; } @@ -71,7 +70,7 @@ namespace Avalonia.Media.TextFormatting /// The text run's. /// The length to split at. /// The split text runs. - internal static SplitResult> SplitDrawableRuns(List textRuns, int length) + internal static SplitResult> SplitTextRuns(IReadOnlyList textRuns, int length) { var currentLength = 0; @@ -88,7 +87,7 @@ namespace Avalonia.Media.TextFormatting var firstCount = currentRun.Length >= 1 ? i + 1 : i; - var first = new List(firstCount); + var first = new List(firstCount); if (firstCount > 1) { @@ -102,7 +101,7 @@ namespace Avalonia.Media.TextFormatting if (currentLength + currentRun.Length == length) { - var second = secondCount > 0 ? new List(secondCount) : null; + var second = secondCount > 0 ? new List(secondCount) : null; if (second != null) { @@ -116,33 +115,33 @@ namespace Avalonia.Media.TextFormatting first.Add(currentRun); - return new SplitResult>(first, second); + return new SplitResult>(first, second); } else { secondCount++; - var second = new List(secondCount); + var second = new List(secondCount); - if (currentRun is ShapedTextCharacters shapedTextCharacters) + if (currentRun is ShapedTextRun shapedTextCharacters) { var split = shapedTextCharacters.Split(length - currentLength); first.Add(split.First); second.Add(split.Second!); - } + } for (var j = 1; j < secondCount; j++) { second.Add(textRuns[i + j]); } - return new SplitResult>(first, second); + return new SplitResult>(first, second); } } - return new SplitResult>(textRuns, null); + return new SplitResult>(textRuns, null); } /// @@ -154,12 +153,12 @@ namespace Avalonia.Media.TextFormatting /// /// A list of shaped text characters. /// - private static List ShapeTextRuns(List textRuns, TextParagraphProperties paragraphProperties, + private static List ShapeTextRuns(List textRuns, TextParagraphProperties paragraphProperties, out FlowDirection resolvedFlowDirection) { var flowDirection = paragraphProperties.FlowDirection; - var drawableTextRuns = new List(); - var biDiData = new BidiData((sbyte)flowDirection); + var shapedRuns = new List(); + using var biDiData = new BidiData((sbyte)flowDirection); foreach (var textRun in textRuns) { @@ -177,7 +176,7 @@ namespace Avalonia.Media.TextFormatting } } - var biDi = new BidiAlgorithm(); + using var biDi = new BidiAlgorithm(); biDi.Process(biDiData); @@ -188,10 +187,7 @@ namespace Avalonia.Media.TextFormatting var processedRuns = new List(textRuns.Count); - foreach (var coalescedRuns in CoalesceLevels(textRuns, biDi.ResolvedLevels)) - { - processedRuns.AddRange(coalescedRuns); - } + CoalesceLevels(textRuns, biDi.ResolvedLevels, processedRuns); for (var index = 0; index < processedRuns.Count; index++) { @@ -199,23 +195,16 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { - case DrawableTextRun drawableRun: - { - drawableTextRuns.Add(drawableRun); - - break; - } - - case ShapeableTextCharacters shapeableRun: + case UnshapedTextRun shapeableRun: { - var groupedRuns = new List(2) { shapeableRun }; + var groupedRuns = new List(2) { shapeableRun }; var characterBufferReference = currentRun.CharacterBufferReference; var length = currentRun.Length; var offsetToFirstCharacter = characterBufferReference.OffsetToFirstChar; while (index + 1 < processedRuns.Count) { - if (processedRuns[index + 1] is not ShapeableTextCharacters nextRun) + if (processedRuns[index + 1] is not UnshapedTextRun nextRun) { break; } @@ -245,23 +234,29 @@ namespace Avalonia.Media.TextFormatting var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface, currentRun.Properties.FontRenderingEmSize, - shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, + shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); - drawableTextRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions)); + shapedRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions)); + + break; + } + default: + { + shapedRuns.Add(currentRun); break; } } } - return drawableTextRuns; + return shapedRuns; } - private static IReadOnlyList ShapeTogether( - IReadOnlyList textRuns, CharacterBufferReference text, int length, TextShaperOptions options) + private static IReadOnlyList ShapeTogether( + IReadOnlyList textRuns, CharacterBufferReference text, int length, TextShaperOptions options) { - var shapedRuns = new List(textRuns.Count); + var shapedRuns = new List(textRuns.Count); var shapedBuffer = TextShaper.Current.ShapeText(text, length, options); @@ -271,7 +266,7 @@ namespace Avalonia.Media.TextFormatting var splitResult = shapedBuffer.Split(currentRun.Length); - shapedRuns.Add(new ShapedTextCharacters(splitResult.First, currentRun.Properties)); + shapedRuns.Add(new ShapedTextRun(splitResult.First, currentRun.Properties)); shapedBuffer = splitResult.Second!; } @@ -280,16 +275,18 @@ namespace Avalonia.Media.TextFormatting } /// - /// Coalesces ranges of the same bidi level to form + /// Coalesces ranges of the same bidi level to form /// - /// The text characters to form from. + /// The text characters to form from. /// The bidi levels. + /// /// - private static IEnumerable> CoalesceLevels(IReadOnlyList textCharacters, ArraySlice levels) + private static void CoalesceLevels(IReadOnlyList textCharacters, ArraySlice levels, + List processedRuns) { if (levels.Length == 0) { - yield break; + return; } var levelIndex = 0; @@ -308,7 +305,7 @@ namespace Avalonia.Media.TextFormatting { var drawableRun = textCharacters[i]; - yield return new[] { drawableRun }; + processedRuns.Add(drawableRun); levelIndex += drawableRun.Length; @@ -331,7 +328,7 @@ namespace Avalonia.Media.TextFormatting if (j == runText.Length) { - yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties); + processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties)); runLevel = levels[levelIndex]; @@ -344,7 +341,7 @@ namespace Avalonia.Media.TextFormatting } // End of this run - yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties); + processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties)); runText = runText.Skip(j); @@ -357,10 +354,10 @@ namespace Avalonia.Media.TextFormatting if (currentRun is null || runText.IsEmpty) { - yield break; + return; } - yield return currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties); + processedRuns.AddRange(currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties)); } /// @@ -390,6 +387,10 @@ namespace Avalonia.Media.TextFormatting if (textRun == null) { + textRuns.Add(new TextEndOfParagraph()); + + textSourceLength += TextRun.DefaultTextSourceLength; + break; } @@ -465,7 +466,7 @@ namespace Avalonia.Media.TextFormatting return false; } - private static bool TryMeasureLength(IReadOnlyList textRuns, double paragraphWidth, out int measuredLength) + private static bool TryMeasureLength(IReadOnlyList textRuns, double paragraphWidth, out int measuredLength) { measuredLength = 0; var currentWidth = 0.0; @@ -474,9 +475,9 @@ namespace Avalonia.Media.TextFormatting { switch (currentRun) { - case ShapedTextCharacters shapedTextCharacters: + case ShapedTextRun shapedTextCharacters: { - if(shapedTextCharacters.ShapedBuffer.Length > 0) + if (shapedTextCharacters.ShapedBuffer.Length > 0) { var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster; var lastCluster = firstCluster; @@ -497,12 +498,12 @@ namespace Avalonia.Media.TextFormatting } measuredLength += currentRun.Length; - } + } break; } - case { } drawableTextRun: + case DrawableTextRun drawableTextRun: { if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth) { @@ -510,14 +511,20 @@ namespace Avalonia.Media.TextFormatting } measuredLength += currentRun.Length; - currentWidth += currentRun.Size.Width; + currentWidth += drawableTextRun.Size.Width; + + break; + } + default: + { + measuredLength += currentRun.Length; break; } } } - found: + found: return measuredLength != 0; } @@ -538,7 +545,7 @@ namespace Avalonia.Media.TextFormatting var shapedBuffer = new ShapedBuffer(characterBufferRange, glyphInfos, glyphTypeface, properties.FontRenderingEmSize, (sbyte)flowDirection); - var textRuns = new List { new ShapedTextCharacters(shapedBuffer, properties) }; + var textRuns = new List { new ShapedTextRun(shapedBuffer, properties) }; return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine(); } @@ -553,13 +560,13 @@ namespace Avalonia.Media.TextFormatting /// /// The current line break if the line was explicitly broken. /// The wrapped text line. - private static TextLineImpl PerformTextWrapping(List textRuns, int firstTextSourceIndex, + private static TextLineImpl PerformTextWrapping(IReadOnlyList textRuns, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection, TextLineBreak? currentLineBreak) { - if(textRuns.Count == 0) + if (textRuns.Count == 0) { - return CreateEmptyTextLine(firstTextSourceIndex,paragraphWidth, paragraphProperties); + return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties); } if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) @@ -575,46 +582,24 @@ namespace Avalonia.Media.TextFormatting for (var index = 0; index < textRuns.Count; index++) { - var currentRun = textRuns[index]; - - var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); - - var lineBreaker = new LineBreakEnumerator(runText); - var breakFound = false; - while (lineBreaker.MoveNext()) - { - if (lineBreaker.Current.Required && - currentLength + lineBreaker.Current.PositionMeasure <= measuredLength) - { - //Explicit break found - breakFound = true; - - currentPosition = currentLength + lineBreaker.Current.PositionWrap; - - break; - } + var currentRun = textRuns[index]; - if (currentLength + lineBreaker.Current.PositionMeasure > measuredLength) - { - if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow) + switch (currentRun) + { + case ShapedTextRun: { - if (lastWrapPosition > 0) - { - currentPosition = lastWrapPosition; + var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); - breakFound = true; + var lineBreaker = new LineBreakEnumerator(runText); - break; - } - - //Find next possible wrap position (overflow) - if (index < textRuns.Count - 1) + while (lineBreaker.MoveNext()) { - if (lineBreaker.Current.PositionWrap != currentRun.Length) + if (lineBreaker.Current.Required && + currentLength + lineBreaker.Current.PositionMeasure <= measuredLength) { - //We already found the next possible wrap position. + //Explicit break found breakFound = true; currentPosition = currentLength + lineBreaker.Current.PositionWrap; @@ -622,51 +607,81 @@ namespace Avalonia.Media.TextFormatting break; } - while (lineBreaker.MoveNext() && index < textRuns.Count) + if (currentLength + lineBreaker.Current.PositionMeasure > measuredLength) { - currentPosition += lineBreaker.Current.PositionWrap; - - if (lineBreaker.Current.PositionWrap != currentRun.Length) + if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow) { - break; - } + if (lastWrapPosition > 0) + { + currentPosition = lastWrapPosition; - index++; + breakFound = true; + + break; + } + + //Find next possible wrap position (overflow) + if (index < textRuns.Count - 1) + { + if (lineBreaker.Current.PositionWrap != currentRun.Length) + { + //We already found the next possible wrap position. + breakFound = true; + + currentPosition = currentLength + lineBreaker.Current.PositionWrap; + + break; + } + + while (lineBreaker.MoveNext() && index < textRuns.Count) + { + currentPosition += lineBreaker.Current.PositionWrap; + + if (lineBreaker.Current.PositionWrap != currentRun.Length) + { + break; + } + + index++; + + if (index >= textRuns.Count) + { + break; + } + + currentRun = textRuns[index]; + + runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); + + lineBreaker = new LineBreakEnumerator(runText); + } + } + else + { + currentPosition = currentLength + lineBreaker.Current.PositionWrap; + } + + breakFound = true; - if (index >= textRuns.Count) - { break; } - currentRun = textRuns[index]; + //We overflowed so we use the last available wrap position. + currentPosition = lastWrapPosition == 0 ? measuredLength : lastWrapPosition; - runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); + breakFound = true; - lineBreaker = new LineBreakEnumerator(runText); + break; } - } - else - { - currentPosition = currentLength + lineBreaker.Current.PositionWrap; - } - breakFound = true; + if (lineBreaker.Current.PositionMeasure != lineBreaker.Current.PositionWrap) + { + lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap; + } + } break; } - - //We overflowed so we use the last available wrap position. - currentPosition = lastWrapPosition == 0 ? measuredLength : lastWrapPosition; - - breakFound = true; - - break; - } - - if (lineBreaker.Current.PositionMeasure != lineBreaker.Current.PositionWrap) - { - lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap; - } } if (!breakFound) @@ -681,12 +696,12 @@ namespace Avalonia.Media.TextFormatting break; } - var splitResult = SplitDrawableRuns(textRuns, measuredLength); + var splitResult = SplitTextRuns(textRuns, measuredLength); var remainingCharacters = splitResult.Second; var lineBreak = remainingCharacters?.Count > 0 ? - new TextLineBreak(currentLineBreak?.TextEndOfLine, resolvedFlowDirection, remainingCharacters) : + new TextLineBreak(null, resolvedFlowDirection, remainingCharacters) : null; if (lineBreak is null && currentLineBreak?.TextEndOfLine != null) @@ -744,7 +759,7 @@ namespace Avalonia.Media.TextFormatting /// /// The shaped symbol. /// - internal static ShapedTextCharacters CreateSymbol(TextRun textRun, FlowDirection flowDirection) + internal static ShapedTextRun CreateSymbol(TextRun textRun, FlowDirection flowDirection) { var textShaper = TextShaper.Current; @@ -760,7 +775,7 @@ namespace Avalonia.Media.TextFormatting var shapedBuffer = textShaper.ShapeText(characterBuffer, textRun.Length, shaperOptions); - return new ShapedTextCharacters(shapedBuffer, textRun.Properties); + return new ShapedTextRun(shapedBuffer, textRun.Properties); } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index f803001481..468623b356 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -8,7 +8,7 @@ namespace Avalonia.Media.TextFormatting /// /// Represents a multi line text layout. /// - public class TextLayout + public class TextLayout : IDisposable { private readonly ITextSource _textSource; private readonly TextParagraphProperties _paragraphProperties; @@ -448,7 +448,7 @@ namespace Avalonia.Media.TextFormatting var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth, _paragraphProperties, previousLine?.TextLineBreak); - if (textLine == null || textLine.Length == 0 || textLine.TextRuns.Count == 0 && textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph) + if(textLine == null || textLine.Length == 0) { if (previousLine != null && previousLine.NewLineLength > 0) { @@ -501,6 +501,11 @@ namespace Avalonia.Media.TextFormatting break; } + + if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph) + { + break; + } } //Make sure the TextLayout always contains at least on empty line @@ -561,5 +566,13 @@ namespace Avalonia.Media.TextFormatting return _textTrimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties)); } + + public void Dispose() + { + foreach (var line in TextLines) + { + line.Dispose(); + } + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index 2752af8f0c..e30a0fe9f4 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -39,9 +39,11 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + public override List? Collapse(TextLine textLine) { - if (textLine.TextRuns is not List textRuns || textRuns.Count == 0) + var textRuns = textLine.TextRuns; + + if (textRuns == null || textRuns.Count == 0) { return null; } @@ -52,7 +54,7 @@ namespace Avalonia.Media.TextFormatting if (Width < shapedSymbol.GlyphRun.Size.Width) { - return new List(0); + return new List(0); } // Overview of ellipsis structure @@ -65,93 +67,102 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { - case ShapedTextCharacters shapedRun: - { - currentWidth += currentRun.Size.Width; - - if (currentWidth > availableWidth) + case ShapedTextRun shapedRun: { - shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength); - - var collapsedRuns = new List(textRuns.Count); + currentWidth += shapedRun.Size.Width; - if (measuredLength > 0) + if (currentWidth > availableWidth) { - List? preSplitRuns = null; - List? postSplitRuns; - - if (_prefixLength > 0) - { - var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, - Math.Min(_prefixLength, measuredLength)); + shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength); - collapsedRuns.AddRange(splitResult.First); + var collapsedRuns = new List(textRuns.Count); - preSplitRuns = splitResult.First; - postSplitRuns = splitResult.Second; - } - else + if (measuredLength > 0) { - postSplitRuns = textRuns; - } + IReadOnlyList? preSplitRuns = null; + IReadOnlyList? postSplitRuns; - collapsedRuns.Add(shapedSymbol); + if (_prefixLength > 0) + { + var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, + Math.Min(_prefixLength, measuredLength)); - if (measuredLength <= _prefixLength || postSplitRuns is null) - { - return collapsedRuns; - } + collapsedRuns.AddRange(splitResult.First); - var availableSuffixWidth = availableWidth; + preSplitRuns = splitResult.First; + postSplitRuns = splitResult.Second; + } + else + { + postSplitRuns = textRuns; + } - if (preSplitRuns is not null) - { - foreach (var run in preSplitRuns) + collapsedRuns.Add(shapedSymbol); + + if (measuredLength <= _prefixLength || postSplitRuns is null) { - availableSuffixWidth -= run.Size.Width; + return collapsedRuns; } - } - for (var i = postSplitRuns.Count - 1; i >= 0; i--) - { - var run = postSplitRuns[i]; + var availableSuffixWidth = availableWidth; - switch (run) + if (preSplitRuns is not null) { - case ShapedTextCharacters endShapedRun: + foreach (var run in preSplitRuns) { - if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth, - out var suffixCount, out var suffixWidth)) + if (run is DrawableTextRun drawableTextRun) { - availableSuffixWidth -= suffixWidth; + availableSuffixWidth -= drawableTextRun.Size.Width; + } + } + } - if (suffixCount > 0) + for (var i = postSplitRuns.Count - 1; i >= 0; i--) + { + var run = postSplitRuns[i]; + + switch (run) + { + case ShapedTextRun endShapedRun: { - var splitSuffix = - endShapedRun.Split(run.Length - suffixCount); + if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth, + out var suffixCount, out var suffixWidth)) + { + availableSuffixWidth -= suffixWidth; - collapsedRuns.Add(splitSuffix.Second!); - } - } + if (suffixCount > 0) + { + var splitSuffix = + endShapedRun.Split(run.Length - suffixCount); + + collapsedRuns.Add(splitSuffix.Second!); + } + } - break; + break; + } } } } - } - else - { - collapsedRuns.Add(shapedSymbol); + else + { + collapsedRuns.Add(shapedSymbol); + } + + return collapsedRuns; } - return collapsedRuns; - } + availableWidth -= shapedRun.Size.Width; - break; - } - } + break; + } + case DrawableTextRun drawableTextRun: + { + availableWidth -= drawableTextRun.Size.Width; - availableWidth -= currentRun.Size.Width; + break; + } + } runIndex++; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLine.cs b/src/Avalonia.Base/Media/TextFormatting/TextLine.cs index 61b24dc8c5..3cb26882dc 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLine.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLine.cs @@ -6,7 +6,7 @@ namespace Avalonia.Media.TextFormatting /// /// Represents a line of text that is used for text rendering. /// - public abstract class TextLine + public abstract class TextLine : IDisposable { /// /// Gets the text runs that are contained within a line. @@ -207,5 +207,7 @@ namespace Avalonia.Media.TextFormatting /// number of characters of the specified range /// an array of bounding rectangles. public abstract IReadOnlyList GetTextBounds(int firstTextSourceCharacterIndex, int textLength); + + public abstract void Dispose(); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs index ce35e47fbd..bf26ac5df4 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media.TextFormatting public class TextLineBreak { public TextLineBreak(TextEndOfLine? textEndOfLine = null, FlowDirection flowDirection = FlowDirection.LeftToRight, - IReadOnlyList? remainingRuns = null) + IReadOnlyList? remainingRuns = null) { TextEndOfLine = textEndOfLine; FlowDirection = flowDirection; @@ -25,6 +25,6 @@ namespace Avalonia.Media.TextFormatting /// /// Get the remaining runs that were split up by the during the formatting process. /// - public IReadOnlyList? RemainingRuns { get; } + public IReadOnlyList? RemainingRuns { get; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 3241dfd12b..ab9686a34a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1,18 +1,19 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { internal class TextLineImpl : TextLine { - private readonly List _textRuns; + private IReadOnlyList _textRuns; private readonly double _paragraphWidth; private readonly TextParagraphProperties _paragraphProperties; private TextLineMetrics _textLineMetrics; private readonly FlowDirection _resolvedFlowDirection; - public TextLineImpl(List textRuns, int firstTextSourceIndex, int length, double paragraphWidth, + public TextLineImpl(IReadOnlyList textRuns, int firstTextSourceIndex, int length, double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection = FlowDirection.LeftToRight, TextLineBreak? lineBreak = null, bool hasCollapsed = false) { @@ -86,11 +87,14 @@ namespace Avalonia.Media.TextFormatting foreach (var textRun in _textRuns) { - var offsetY = GetBaselineOffset(this, textRun); + if (textRun is DrawableTextRun drawable) + { + var offsetY = GetBaselineOffset(this, drawable); - textRun.Draw(drawingContext, new Point(currentX, currentY + offsetY)); + drawable.Draw(drawingContext, new Point(currentX, currentY + offsetY)); - currentX += textRun.Size.Width; + currentX += drawable.Size.Width; + } } } @@ -180,7 +184,14 @@ namespace Avalonia.Media.TextFormatting { var lastRun = _textRuns[_textRuns.Count - 1]; - return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, lastRun.Size.Width); + var size = 0.0; + + if (lastRun is DrawableTextRun drawableTextRun) + { + size = drawableTextRun.Size.Width; + } + + return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, size); } // process hit that happens within the line @@ -192,14 +203,14 @@ namespace Avalonia.Media.TextFormatting { var currentRun = _textRuns[i]; - if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) + if (currentRun is ShapedTextRun shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) { var rightToLeftIndex = i; currentPosition += currentRun.Length; while (rightToLeftIndex + 1 <= _textRuns.Count - 1) { - var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextCharacters; + var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextRun; if (nextShaped == null || nextShaped.ShapedBuffer.IsLeftToRight) { @@ -220,9 +231,16 @@ namespace Avalonia.Media.TextFormatting currentRun = _textRuns[j]; - if (currentDistance + currentRun.Size.Width <= distance) + if(currentRun is not ShapedTextRun) { - currentDistance += currentRun.Size.Width; + continue; + } + + shapedRun = (ShapedTextRun)currentRun; + + if (currentDistance + shapedRun.Size.Width <= distance) + { + currentDistance += shapedRun.Size.Width; currentPosition -= currentRun.Length; continue; @@ -234,12 +252,19 @@ namespace Avalonia.Media.TextFormatting characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance); - if (i < _textRuns.Count - 1 && currentDistance + currentRun.Size.Width < distance) + if (currentRun is DrawableTextRun drawableTextRun) { - currentDistance += currentRun.Size.Width; + if (i < _textRuns.Count - 1 && currentDistance + drawableTextRun.Size.Width < distance) + { + currentDistance += drawableTextRun.Size.Width; - currentPosition += currentRun.Length; + currentPosition += currentRun.Length; + continue; + } + } + else + { continue; } @@ -249,13 +274,13 @@ namespace Avalonia.Media.TextFormatting return characterHit; } - private static CharacterHit GetRunCharacterHit(DrawableTextRun run, int currentPosition, double distance) + private static CharacterHit GetRunCharacterHit(TextRun run, int currentPosition, double distance) { CharacterHit characterHit; switch (run) { - case ShapedTextCharacters shapedRun: + case ShapedTextRun shapedRun: { characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _); @@ -270,9 +295,9 @@ namespace Avalonia.Media.TextFormatting break; } - default: + case DrawableTextRun drawableTextRun: { - if (distance < run.Size.Width / 2) + if (distance < drawableTextRun.Size.Width / 2) { characterHit = new CharacterHit(currentPosition); } @@ -282,6 +307,10 @@ namespace Avalonia.Media.TextFormatting } break; } + default: + characterHit = new CharacterHit(currentPosition, run.Length); + + break; } return characterHit; @@ -303,21 +332,21 @@ namespace Avalonia.Media.TextFormatting { var currentRun = _textRuns[index]; - if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) + if (currentRun is ShapedTextRun shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) { var i = index; - var rightToLeftWidth = currentRun.Size.Width; + var rightToLeftWidth = shapedRun.Size.Width; while (i + 1 <= _textRuns.Count - 1) { var nextRun = _textRuns[i + 1]; - if (nextRun is ShapedTextCharacters nextShapedRun && !nextShapedRun.ShapedBuffer.IsLeftToRight) + if (nextRun is ShapedTextRun nextShapedRun && !nextShapedRun.ShapedBuffer.IsLeftToRight) { i++; - rightToLeftWidth += nextRun.Size.Width; + rightToLeftWidth += nextShapedRun.Size.Width; continue; } @@ -331,7 +360,10 @@ namespace Avalonia.Media.TextFormatting { currentRun = _textRuns[i]; - rightToLeftWidth -= currentRun.Size.Width; + if (currentRun is DrawableTextRun drawable) + { + rightToLeftWidth -= drawable.Size.Width; + } if (currentPosition + currentRun.Length >= characterIndex) { @@ -355,8 +387,13 @@ namespace Avalonia.Media.TextFormatting return Math.Max(0, currentDistance + distance); } + if (currentRun is DrawableTextRun drawableTextRun) + { + currentDistance += drawableTextRun.Size.Width; + } + //No hit hit found so we add the full width - currentDistance += currentRun.Size.Width; + currentPosition += currentRun.Length; remainingLength -= currentRun.Length; } @@ -380,8 +417,12 @@ namespace Avalonia.Media.TextFormatting return Math.Max(0, currentDistance - distance); } + if (currentRun is DrawableTextRun drawableTextRun) + { + currentDistance -= drawableTextRun.Size.Width; + } + //No hit hit found so we add the full width - currentDistance -= currentRun.Size.Width; currentPosition += currentRun.Length; remainingLength -= currentRun.Length; } @@ -391,7 +432,7 @@ namespace Avalonia.Media.TextFormatting } private static bool TryGetDistanceFromCharacterHit( - DrawableTextRun currentRun, + TextRun currentRun, CharacterHit characterHit, int currentPosition, int remainingLength, @@ -407,7 +448,7 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { - case ShapedTextCharacters shapedTextCharacters: + case ShapedTextRun shapedTextCharacters: { currentGlyphRun = shapedTextCharacters.GlyphRun; @@ -432,7 +473,7 @@ namespace Avalonia.Media.TextFormatting break; } - default: + case DrawableTextRun drawableTextRun: { if (characterIndex == currentPosition) { @@ -441,7 +482,7 @@ namespace Avalonia.Media.TextFormatting if (characterIndex == currentPosition + currentRun.Length) { - distance = currentRun.Size.Width; + distance = drawableTextRun.Size.Width; return true; @@ -449,6 +490,10 @@ namespace Avalonia.Media.TextFormatting break; } + default: + { + return false; + } } return false; @@ -476,7 +521,7 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { - case ShapedTextCharacters shapedRun: + case ShapedTextRun shapedRun: { nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit); break; @@ -532,7 +577,7 @@ namespace Avalonia.Media.TextFormatting var startX = Start; double currentWidth = 0; - var currentRect = Rect.Empty; + var currentRect = default(Rect); TextRunBounds lastRunBounds = default; @@ -550,7 +595,7 @@ namespace Avalonia.Media.TextFormatting double combinedWidth; - if (currentRun is ShapedTextCharacters currentShapedRun) + if (currentRun is ShapedTextRun currentShapedRun) { var firstCluster = currentShapedRun.GlyphRun.Metrics.FirstCluster; @@ -592,7 +637,7 @@ namespace Avalonia.Media.TextFormatting var rightToLeftIndex = index; var rightToLeftWidth = currentShapedRun.Size.Width; - while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun) + while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun) { if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) { @@ -624,12 +669,12 @@ namespace Avalonia.Media.TextFormatting for (int i = rightToLeftIndex - 1; i >= index; i--) { - if (TextRuns[i] is not ShapedTextCharacters) + if (TextRuns[i] is not ShapedTextRun) { continue; } - currentShapedRun = (ShapedTextCharacters)TextRuns[i]; + currentShapedRun = (ShapedTextRun)TextRuns[i]; currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); @@ -748,7 +793,7 @@ namespace Avalonia.Media.TextFormatting var startX = WidthIncludingTrailingWhitespace; double currentWidth = 0; - var currentRect = Rect.Empty; + var currentRect = default(Rect); for (var index = TextRuns.Count - 1; index >= 0; index--) { @@ -769,7 +814,7 @@ namespace Avalonia.Media.TextFormatting var characterLength = 0; var endX = startX; - if (currentRun is ShapedTextCharacters currentShapedRun) + if (currentRun is ShapedTextRun currentShapedRun) { var offset = Math.Max(0, firstTextSourceIndex - currentPosition); @@ -883,7 +928,7 @@ namespace Avalonia.Media.TextFormatting return result; } - private TextRunBounds GetRightToLeftTextRunBounds(ShapedTextCharacters currentRun, double endX, int firstTextSourceIndex, int characterIndex, int currentPosition, int remainingLength) + private TextRunBounds GetRightToLeftTextRunBounds(ShapedTextRun currentRun, double endX, int firstTextSourceIndex, int characterIndex, int currentPosition, int remainingLength) { var startX = endX; @@ -934,6 +979,17 @@ namespace Avalonia.Media.TextFormatting return GetTextBoundsRightToLeft(firstTextSourceIndex, textLength); } + public override void Dispose() + { + for (int i = 0; i < _textRuns.Count; i++) + { + if (_textRuns[i] is ShapedTextRun shapedTextRun) + { + shapedTextRun.Dispose(); + } + } + } + public TextLineImpl FinalizeLine() { _textLineMetrics = CreateLineMetrics(); @@ -943,9 +999,9 @@ namespace Avalonia.Media.TextFormatting return this; } - private static sbyte GetRunBidiLevel(DrawableTextRun run, FlowDirection flowDirection) + private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection) { - if (run is ShapedTextCharacters shapedTextCharacters) + if (run is ShapedTextRun shapedTextCharacters) { return shapedTextCharacters.BidiLevel; } @@ -1027,7 +1083,7 @@ namespace Avalonia.Media.TextFormatting { if (current.Level >= minLevelToReverse && current.Level % 2 != 0) { - if (current.Run is ShapedTextCharacters { IsReversed: false } shapedTextCharacters) + if (current.Run is ShapedTextRun { IsReversed: false } shapedTextCharacters) { shapedTextCharacters.Reverse(); } @@ -1039,16 +1095,18 @@ namespace Avalonia.Media.TextFormatting minLevelToReverse--; } - _textRuns.Clear(); + var textRuns = new List(_textRuns.Count); current = orderedRun; while (current != null) { - _textRuns.Add(current.Run); + textRuns.Add(current.Run); current = current.Next; } + + _textRuns = textRuns; } /// @@ -1145,7 +1203,7 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { - case ShapedTextCharacters shapedRun: + case ShapedTextRun shapedRun: { var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _); @@ -1230,7 +1288,7 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { - case ShapedTextCharacters shapedRun: + case ShapedTextRun shapedRun: { var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _); @@ -1286,7 +1344,7 @@ namespace Avalonia.Media.TextFormatting { var runIndex = 0; textPosition = FirstTextSourceIndex; - DrawableTextRun? previousRun = null; + TextRun? previousRun = null; while (runIndex < _textRuns.Count) { @@ -1294,7 +1352,7 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { - case ShapedTextCharacters shapedRun: + case ShapedTextRun shapedRun: { var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster; @@ -1303,7 +1361,7 @@ namespace Avalonia.Media.TextFormatting break; } - if (previousRun is ShapedTextCharacters previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight) + if (previousRun is ShapedTextRun previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight) { if (shapedRun.ShapedBuffer.IsLeftToRight) { @@ -1346,7 +1404,6 @@ namespace Avalonia.Media.TextFormatting break; } - default: { if (codepointIndex == textPosition) @@ -1363,6 +1420,7 @@ namespace Avalonia.Media.TextFormatting break; } + } runIndex++; @@ -1390,11 +1448,18 @@ namespace Avalonia.Media.TextFormatting var lineHeight = _paragraphProperties.LineHeight; + var lastRunIndex = _textRuns.Count - 1; + + if (lastRunIndex > 0 && _textRuns[lastRunIndex] is TextEndOfLine) + { + lastRunIndex--; + } + for (var index = 0; index < _textRuns.Count; index++) { switch (_textRuns[index]) { - case ShapedTextCharacters textRun: + case ShapedTextRun textRun: { var textMetrics = new TextMetrics(textRun.Properties.Typeface.GlyphTypeface, textRun.Properties.FontRenderingEmSize); @@ -1424,7 +1489,7 @@ namespace Avalonia.Media.TextFormatting } } - if (index == _textRuns.Count - 1) + if (index == lastRunIndex) { width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width; trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength; @@ -1436,7 +1501,7 @@ namespace Avalonia.Media.TextFormatting break; } - case { } drawableTextRun: + case DrawableTextRun drawableTextRun: { widthIncludingWhitespace += drawableTextRun.Size.Width; @@ -1444,7 +1509,7 @@ namespace Avalonia.Media.TextFormatting { case FlowDirection.LeftToRight: { - if (index == _textRuns.Count - 1) + if (index == lastRunIndex) { width = widthIncludingWhitespace; trailingWhitespaceLength = 0; @@ -1456,7 +1521,7 @@ namespace Avalonia.Media.TextFormatting case FlowDirection.RightToLeft: { - if (index == _textRuns.Count - 1) + if (index == lastRunIndex) { width = widthIncludingWhitespace; trailingWhitespaceLength = 0; @@ -1558,7 +1623,7 @@ namespace Avalonia.Media.TextFormatting private sealed class OrderedBidiRun { - public OrderedBidiRun(DrawableTextRun run, sbyte level) + public OrderedBidiRun(TextRun run, sbyte level) { Run = run; Level = level; @@ -1566,7 +1631,7 @@ namespace Avalonia.Media.TextFormatting public sbyte Level { get; } - public DrawableTextRun Run { get; } + public TextRun Run { get; } public OrderedBidiRun? Next { get; set; } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs index 5691dd8ad0..27515738a4 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs @@ -63,14 +63,11 @@ { get { return 0; } } - + /// /// Gets the default incremental tab width. /// - public virtual double DefaultIncrementalTab - { - get { return 4 * DefaultTextRunProperties.FontRenderingEmSize; } - } + public virtual double DefaultIncrementalTab => 0; /// /// Gets the letter spacing. diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRun.cs b/src/Avalonia.Base/Media/TextFormatting/TextRun.cs index 0306054767..56232ec9c8 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRun.cs @@ -40,11 +40,11 @@ namespace Avalonia.Media.TextFormatting { unsafe { - var characterBuffer = _textRun.CharacterBufferReference.CharacterBuffer; + var characterBuffer = new CharacterBufferRange(_textRun.CharacterBufferReference, _textRun.Length); fixed (char* charsPtr = characterBuffer.Span) { - return new string(charsPtr, 0, characterBuffer.Span.Length); + return new string(charsPtr, 0, _textRun.Length); } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs b/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs index 4aacec7c48..c161b08d20 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs @@ -29,10 +29,7 @@ namespace Avalonia.Media.TextFormatting return current; } - var textShaperImpl = AvaloniaLocator.Current.GetService(); - - if (textShaperImpl == null) - throw new InvalidOperationException("No text shaper implementation was registered."); + var textShaperImpl = AvaloniaLocator.Current.GetRequiredService(); current = new TextShaper(textShaperImpl); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs index 1de04ad061..deecbbe476 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs @@ -26,7 +26,7 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + public override List? Collapse(TextLine textLine) { return TextEllipsisHelper.Collapse(textLine, this, false); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs index 7c94715aa4..c291e1dfb9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs @@ -31,7 +31,7 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + public override List? Collapse(TextLine textLine) { return TextEllipsisHelper.Collapse(textLine, this, true); } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index 17ec9b1df2..100d381afe 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; +using Avalonia.Collections.Pooled; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode @@ -27,7 +28,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// as much as possible. /// /// - internal sealed class BidiAlgorithm + internal struct BidiAlgorithm : IDisposable { /// /// The original BiDiClass classes as provided by the caller @@ -66,7 +67,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The forward mapping maps the start index to the end index. /// The reverse mapping maps the end index to the start index. /// - private readonly BidiDictionary _isolatePairs = new BidiDictionary(); + private BidiDictionary? _isolatePairs; /// /// The working BiDi classes @@ -118,7 +119,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// A stack of pending isolate openings used by FindIsolatePairs() /// - private readonly Stack _pendingIsolateOpenings = new Stack(); + private Stack? _pendingIsolateOpenings; /// /// The level of the isolating run currently being processed @@ -184,7 +185,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// Initializes a new instance of the class. /// - internal BidiAlgorithm() + public BidiAlgorithm() { } @@ -227,7 +228,7 @@ namespace Avalonia.Media.TextFormatting.Unicode ArraySlice? outLevels) { // Reset state - _isolatePairs.Clear(); + _isolatePairs?.Clear(); _workingClassesBuffer.Clear(); _levelRuns.Clear(); _resolvedLevelsBuffer.Clear(); @@ -323,7 +324,7 @@ namespace Avalonia.Media.TextFormatting.Unicode // Skip isolate pairs // (Because we're working with a slice, we need to adjust the indices // we're using for the isolatePairs map) - if (_isolatePairs.TryGetValue(data.Start + i, out i)) + if (_isolatePairs?.TryGetValue(data.Start + i, out i) == true) { i -= data.Start; } @@ -358,7 +359,7 @@ namespace Avalonia.Media.TextFormatting.Unicode _hasIsolates = false; // BD9... - _pendingIsolateOpenings.Clear(); + _pendingIsolateOpenings?.Clear(); for (var i = 0; i < _originalClasses.Length; i++) { @@ -370,14 +371,16 @@ namespace Avalonia.Media.TextFormatting.Unicode case BidiClass.RightToLeftIsolate: case BidiClass.FirstStrongIsolate: { + _pendingIsolateOpenings ??= new Stack(); _pendingIsolateOpenings.Push(i); _hasIsolates = true; break; } case BidiClass.PopDirectionalIsolate: { - if (_pendingIsolateOpenings.Count > 0) + if (_pendingIsolateOpenings?.Count > 0) { + _isolatePairs ??= new BidiDictionary(); _isolatePairs.Add(_pendingIsolateOpenings.Pop(), i); } @@ -498,7 +501,7 @@ namespace Avalonia.Media.TextFormatting.Unicode if (resolvedIsolate == BidiClass.FirstStrongIsolate) { - if (!_isolatePairs.TryGetValue(i, out var endOfIsolate)) + if (_isolatePairs == null || !_isolatePairs.TryGetValue(i, out var endOfIsolate)) { endOfIsolate = _originalClasses.Length; } @@ -829,7 +832,7 @@ namespace Avalonia.Media.TextFormatting.Unicode var lastCharacterIndex = _isolatedRunMapping[_isolatedRunMapping.Length - 1]; var lastType = _originalClasses[lastCharacterIndex]; if ((lastType == BidiClass.LeftToRightIsolate || lastType == BidiClass.RightToLeftIsolate || lastType == BidiClass.FirstStrongIsolate) && - _isolatePairs.TryGetValue(lastCharacterIndex, out var nextRunIndex)) + _isolatePairs?.TryGetValue(lastCharacterIndex, out var nextRunIndex) == true) { // Find the continuing run index runIndex = FindRunForIndex(nextRunIndex); @@ -1714,5 +1717,13 @@ namespace Avalonia.Media.TextFormatting.Unicode public BidiClass Eos { get; } } + + public void Dispose() + { + _workingClassesBuffer.Dispose(); + _resolvedLevelsBuffer.Dispose(); + _x9Map.Dispose(); + _isolatedRunMapping.Dispose(); + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 0c51b0898d..644f7e9a8a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs @@ -11,7 +11,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// Represents a unicode string and all associated attributes /// for each character required for the bidirectional Unicode algorithm /// - internal class BidiData + internal struct BidiData : IDisposable { private ArrayBuilder _classes; private ArrayBuilder _pairedBracketTypes; @@ -181,5 +181,15 @@ namespace Avalonia.Media.TextFormatting.Unicode return _tempLevelBuffer.Add(length, false); } + + public void Dispose() + { + _classes.Dispose(); + _pairedBracketTypes.Dispose(); + _pairedBracketValues.Dispose(); + _savedClasses.Dispose(); + _savedPairedBracketTypes.Dispose(); + _tempLevelBuffer.Dispose(); + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs index e6408bcfa6..8b9d1c1d02 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Runtime.CompilerServices; namespace Avalonia.Media.TextFormatting.Unicode @@ -222,6 +223,71 @@ namespace Avalonia.Media.TextFormatting.Unicode return new Codepoint(code); } + + /// + /// Reads the at specified position. + /// + /// The buffer to read from. + /// The index to read at. + /// The count of character that were read. + /// + public static Codepoint ReadAt(CharacterBufferRange text, int index, out int count) + { + count = 1; + + if (index >= text.Length) + { + return ReplacementCodepoint; + } + + var code = text[index]; + + ushort hi, low; + + //# High surrogate + if (0xD800 <= code && code <= 0xDBFF) + { + hi = code; + + if (index + 1 == text.Length) + { + return ReplacementCodepoint; + } + + low = text[index + 1]; + + if (0xDC00 <= low && low <= 0xDFFF) + { + count = 2; + return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000)); + } + + return ReplacementCodepoint; + } + + //# Low surrogate + if (0xDC00 <= code && code <= 0xDFFF) + { + if (index == 0) + { + return ReplacementCodepoint; + } + + hi = text[index - 1]; + + low = code; + + if (0xD800 <= hi && hi <= 0xDBFF) + { + count = 2; + return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000)); + } + + return ReplacementCodepoint; + } + + return new Codepoint(code); + } /// /// Returns if is between diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs index 330ead476a..a2c36d9a13 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs @@ -5,7 +5,6 @@ namespace Avalonia.Media.TextFormatting.Unicode public ref struct CodepointEnumerator { private CharacterBufferRange _text; - private int _pos; public CodepointEnumerator(CharacterBufferRange text) { diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs index 69015fb17d..f75168083c 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs @@ -7,10 +7,11 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public readonly ref struct Grapheme { - public Grapheme(Codepoint firstCodepoint, ReadOnlySpan text) + public Grapheme(Codepoint firstCodepoint, int offset, int length) { FirstCodepoint = firstCodepoint; - Text = text; + Offset = offset; + Length = length; } /// @@ -19,8 +20,13 @@ namespace Avalonia.Media.TextFormatting.Unicode public Codepoint FirstCodepoint { get; } /// - /// The text that is representing the . + /// The Offset to the FirstCodepoint /// - public ReadOnlySpan Text { get; } + public int Offset { get; } + + /// + /// The length of the grapheme cluster + /// + public int Length { get; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs index 5ca120c856..dc21e06813 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs @@ -185,9 +185,7 @@ namespace Avalonia.Media.TextFormatting.Unicode Return: - var text = _text.Take(processor.CurrentCodeUnitOffset); - - Current = new Grapheme(firstCodepoint, text.Span); + Current = new Grapheme(firstCodepoint, _text.OffsetToFirstChar, processor.CurrentCodeUnitOffset); _text = _text.Skip(processor.CurrentCodeUnitOffset); diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/UnshapedTextRun.cs similarity index 61% rename from src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs rename to src/Avalonia.Base/Media/TextFormatting/UnshapedTextRun.cs index 0e8d6e3e4a..817086db88 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/UnshapedTextRun.cs @@ -5,9 +5,9 @@ namespace Avalonia.Media.TextFormatting /// /// A group of characters that can be shaped. /// - public sealed class ShapeableTextCharacters : TextRun + public sealed class UnshapedTextRun : TextRun { - public ShapeableTextCharacters(CharacterBufferReference characterBufferReference, int length, + public UnshapedTextRun(CharacterBufferReference characterBufferReference, int length, TextRunProperties properties, sbyte biDiLevel) { CharacterBufferReference = characterBufferReference; @@ -24,30 +24,30 @@ namespace Avalonia.Media.TextFormatting public sbyte BidiLevel { get; } - public bool CanShapeTogether(ShapeableTextCharacters shapeableTextCharacters) + public bool CanShapeTogether(UnshapedTextRun unshapedTextRun) { - if (!CharacterBufferReference.Equals(shapeableTextCharacters.CharacterBufferReference)) + if (!CharacterBufferReference.Equals(unshapedTextRun.CharacterBufferReference)) { return false; } - if (BidiLevel != shapeableTextCharacters.BidiLevel) + if (BidiLevel != unshapedTextRun.BidiLevel) { return false; } if (!MathUtilities.AreClose(Properties.FontRenderingEmSize, - shapeableTextCharacters.Properties.FontRenderingEmSize)) + unshapedTextRun.Properties.FontRenderingEmSize)) { return false; } - if (Properties.Typeface != shapeableTextCharacters.Properties.Typeface) + if (Properties.Typeface != unshapedTextRun.Properties.Typeface) { return false; } - if (Properties.BaselineAlignment != shapeableTextCharacters.Properties.BaselineAlignment) + if (Properties.BaselineAlignment != unshapedTextRun.Properties.BaselineAlignment) { return false; } diff --git a/src/Avalonia.Base/Media/TranslateTransform.cs b/src/Avalonia.Base/Media/TranslateTransform.cs index 0f910f3600..1d0169a62c 100644 --- a/src/Avalonia.Base/Media/TranslateTransform.cs +++ b/src/Avalonia.Base/Media/TranslateTransform.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Reactive; using Avalonia.VisualTree; namespace Avalonia.Media @@ -25,8 +26,6 @@ namespace Avalonia.Media /// public TranslateTransform() { - this.GetObservable(XProperty).Subscribe(_ => RaiseChanged()); - this.GetObservable(YProperty).Subscribe(_ => RaiseChanged()); } /// @@ -63,5 +62,15 @@ namespace Avalonia.Media /// Gets the transform's . /// public override Matrix Value => Matrix.CreateTranslation(X, Y); + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == XProperty || change.Property == YProperty) + { + RaiseChanged(); + } + } } } diff --git a/src/Avalonia.Base/Media/Typeface.cs b/src/Avalonia.Base/Media/Typeface.cs index e6047bf96c..1e744c30c8 100644 --- a/src/Avalonia.Base/Media/Typeface.cs +++ b/src/Avalonia.Base/Media/Typeface.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics; -using JetBrains.Annotations; namespace Avalonia.Media { @@ -17,7 +16,7 @@ namespace Avalonia.Media /// The font style. /// The font weight. /// The font stretch. - public Typeface([NotNull] FontFamily fontFamily, + public Typeface(FontFamily fontFamily, FontStyle style = FontStyle.Normal, FontWeight weight = FontWeight.Normal, FontStretch stretch = FontStretch.Normal) diff --git a/src/Avalonia.Base/Media/UnicodeRange.cs b/src/Avalonia.Base/Media/UnicodeRange.cs index f87f38b444..4cab7b4a14 100644 --- a/src/Avalonia.Base/Media/UnicodeRange.cs +++ b/src/Avalonia.Base/Media/UnicodeRange.cs @@ -104,7 +104,7 @@ namespace Avalonia.Media public readonly record struct UnicodeRangeSegment { - private static Regex s_regex = new Regex(@"^(?:[uU]\+)?(?:([0-9a-fA-F](?:[0-9a-fA-F?]{1,5})?))$"); + private static Regex s_regex = new Regex(@"^(?:[uU]\+)?(?:([0-9a-fA-F](?:[0-9a-fA-F?]{1,5})?))$", RegexOptions.Compiled); public UnicodeRangeSegment(int start, int end) { diff --git a/src/Avalonia.Base/Media/VisualBrush.cs b/src/Avalonia.Base/Media/VisualBrush.cs index 8dce79cf11..1261d233ac 100644 --- a/src/Avalonia.Base/Media/VisualBrush.cs +++ b/src/Avalonia.Base/Media/VisualBrush.cs @@ -6,7 +6,7 @@ namespace Avalonia.Media /// /// Paints an area with an . /// - public class VisualBrush : TileBrush, IVisualBrush + public class VisualBrush : TileBrush, IVisualBrush, IMutableBrush { /// /// Defines the property. @@ -45,7 +45,7 @@ namespace Avalonia.Media } /// - public override IBrush ToImmutable() + IImmutableBrush IMutableBrush.ToImmutable() { return new ImmutableVisualBrush(this); } diff --git a/src/Avalonia.Base/PixelRect.cs b/src/Avalonia.Base/PixelRect.cs index 855ba104ad..469f33e7fd 100644 --- a/src/Avalonia.Base/PixelRect.cs +++ b/src/Avalonia.Base/PixelRect.cs @@ -12,6 +12,7 @@ namespace Avalonia /// /// An empty rectangle. /// + [Obsolete("Use the default keyword instead.")] public static readonly PixelRect Empty = default; /// @@ -133,9 +134,13 @@ namespace Avalonia public PixelPoint Center => new PixelPoint(X + (Width / 2), Y + (Height / 2)); /// - /// Gets a value that indicates whether the rectangle is empty. + /// Gets a value indicating whether the instance has default values (the rectangle is empty). /// - public bool IsEmpty => Width == 0 && Height == 0; + public bool IsDefault => Width == 0 && Height == 0; + + /// + [Obsolete("Use IsDefault instead.")] + public bool IsEmpty => IsDefault; /// /// Checks for equality between two s. @@ -257,7 +262,7 @@ namespace Avalonia } else { - return Empty; + return default; } } @@ -290,11 +295,11 @@ namespace Avalonia /// The union. public PixelRect Union(PixelRect rect) { - if (IsEmpty) + if (IsDefault) { return rect; } - else if (rect.IsEmpty) + else if (rect.IsDefault) { return this; } diff --git a/src/Avalonia.Base/PixelVector.cs b/src/Avalonia.Base/PixelVector.cs index 79e7b94c21..0e156190a4 100644 --- a/src/Avalonia.Base/PixelVector.cs +++ b/src/Avalonia.Base/PixelVector.cs @@ -1,7 +1,6 @@ using System; using System.Globalization; using Avalonia.Animation.Animators; -using JetBrains.Annotations; namespace Avalonia { @@ -135,7 +134,6 @@ namespace Avalonia /// /// The other vector. /// True if vectors are nearly equal. - [Pure] public bool NearlyEquals(PixelVector other) { const float tolerance = float.Epsilon; diff --git a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs index dd3e1a4cb1..d0b27b057f 100644 --- a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs +++ b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs @@ -26,5 +26,7 @@ namespace Avalonia.Platform }; } public TimeSpan GetDoubleTapTime(PointerType type) => TimeSpan.FromMilliseconds(500); + + public TimeSpan HoldWaitDuration { get; set; } = TimeSpan.FromMilliseconds(300); } } diff --git a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs index 7801bdd50f..6a8ae4d954 100644 --- a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs +++ b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia.Metadata; namespace Avalonia.Platform @@ -7,5 +8,8 @@ namespace Avalonia.Platform /// Actual implementation of a glyph run that stores platform dependent resources. /// [Unstable] - public interface IGlyphRunImpl : IDisposable { } + public interface IGlyphRunImpl : IDisposable + { + IReadOnlyList GetIntersections(float lowerBound, float upperBound); + } } diff --git a/src/Avalonia.Base/Platform/IPlatformSettings.cs b/src/Avalonia.Base/Platform/IPlatformSettings.cs index e7921883fd..7c4c1420eb 100644 --- a/src/Avalonia.Base/Platform/IPlatformSettings.cs +++ b/src/Avalonia.Base/Platform/IPlatformSettings.cs @@ -27,5 +27,7 @@ namespace Avalonia.Platform /// tap gesture. /// TimeSpan GetDoubleTapTime(PointerType type); + + TimeSpan HoldWaitDuration { get; set; } } } diff --git a/src/Avalonia.Base/Platform/IRuntimePlatform.cs b/src/Avalonia.Base/Platform/IRuntimePlatform.cs index 91d2a1e0cf..64b504c479 100644 --- a/src/Avalonia.Base/Platform/IRuntimePlatform.cs +++ b/src/Avalonia.Base/Platform/IRuntimePlatform.cs @@ -23,29 +23,10 @@ namespace Avalonia.Platform [Unstable] public record struct RuntimePlatformInfo { - public OperatingSystemType OperatingSystem { get; set; } - public FormFactorType FormFactor => IsDesktop ? FormFactorType.Desktop : IsMobile ? FormFactorType.Mobile : FormFactorType.Unknown; public bool IsDesktop { get; set; } public bool IsMobile { get; set; } - public bool IsBrowser { get; set; } - public bool IsCoreClr { get; set; } - public bool IsMono { get; set; } - public bool IsDotNetFramework { get; set; } - public bool IsUnix { get; set; } - } - - [Unstable] - public enum OperatingSystemType - { - Unknown, - WinNT, - Linux, - OSX, - Android, - iOS, - Browser } [Unstable] diff --git a/src/Avalonia.Base/Platform/ITextShaperImpl.cs b/src/Avalonia.Base/Platform/ITextShaperImpl.cs index ff91097eda..c3eb89ab1a 100644 --- a/src/Avalonia.Base/Platform/ITextShaperImpl.cs +++ b/src/Avalonia.Base/Platform/ITextShaperImpl.cs @@ -13,6 +13,7 @@ namespace Avalonia.Platform /// Shapes the specified region within the text and returns a shaped buffer. /// /// The text buffer. + /// The length of text. /// Text shaper options to customize the shaping process. /// A shaped glyph run. ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options); diff --git a/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs b/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs index df2a26ddd3..467cd530fc 100644 --- a/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs +++ b/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs @@ -32,7 +32,7 @@ internal class AssemblyDescriptor : IAssemblyDescriptor Resources.Remove(Constants.AvaloniaResourceName); var indexLength = new BinaryReader(resources).ReadInt32(); - var index = AvaloniaResourcesIndexReaderWriter.Read(new SlicedStream(resources, 4, indexLength)); + 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)); diff --git a/src/Avalonia.Base/Platform/StandardRuntimePlatform.cs b/src/Avalonia.Base/Platform/StandardRuntimePlatform.cs index 4df9e8e917..8352d794d0 100644 --- a/src/Avalonia.Base/Platform/StandardRuntimePlatform.cs +++ b/src/Avalonia.Base/Platform/StandardRuntimePlatform.cs @@ -1,6 +1,6 @@ using System; -using System.Runtime.InteropServices; using System.Threading; +using Avalonia.Compatibility; using Avalonia.Platform.Internal; namespace Avalonia.Platform @@ -14,45 +14,13 @@ namespace Avalonia.Platform public IUnmanagedBlob AllocBlob(int size) => new UnmanagedBlob(size); - private static readonly Lazy Info = new(() => + private static readonly RuntimePlatformInfo s_info = new() { - OperatingSystemType os; + IsDesktop = OperatingSystemEx.IsWindows() || OperatingSystemEx.IsMacOS() || OperatingSystemEx.IsLinux(), + IsMobile = OperatingSystemEx.IsAndroid() || OperatingSystemEx.IsIOS() + }; - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - os = OperatingSystemType.OSX; - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - os = OperatingSystemType.Linux; - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - os = OperatingSystemType.WinNT; - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("Android"))) - os = OperatingSystemType.Android; - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("iOS"))) - os = OperatingSystemType.iOS; - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("Browser"))) - os = OperatingSystemType.Browser; - else - throw new Exception("Unknown OS platform " + RuntimeInformation.OSDescription); - // Source: https://github.com/dotnet/runtime/blob/main/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs - var isCoreClr = Environment.Version.Major >= 5 || RuntimeInformation.FrameworkDescription.StartsWith(".NET Core", StringComparison.OrdinalIgnoreCase); - var isMonoRuntime = Type.GetType("Mono.Runtime") != null; - var isFramework = !isCoreClr && RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework", StringComparison.OrdinalIgnoreCase); - - return new RuntimePlatformInfo - { - IsCoreClr = isCoreClr, - IsDotNetFramework = isFramework, - IsMono = isMonoRuntime, - - IsDesktop = os is OperatingSystemType.Linux or OperatingSystemType.OSX or OperatingSystemType.WinNT, - IsMobile = os is OperatingSystemType.Android or OperatingSystemType.iOS, - IsUnix = os is OperatingSystemType.Linux or OperatingSystemType.OSX or OperatingSystemType.Android, - IsBrowser = os == OperatingSystemType.Browser, - OperatingSystem = os, - }; - }); - - - public virtual RuntimePlatformInfo GetRuntimeInfo() => Info.Value; + public virtual RuntimePlatformInfo GetRuntimeInfo() => s_info; } } diff --git a/src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs b/src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs index 65d6733399..0a36b4c9dd 100644 --- a/src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs +++ b/src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs @@ -1,4 +1,5 @@ using System.Reflection; +using Avalonia.Compatibility; using Avalonia.Platform.Internal; using Avalonia.Platform.Interop; @@ -18,15 +19,9 @@ namespace Avalonia.Platform #if NET6_0_OR_GREATER new Net6Loader() #else - standardPlatform.GetRuntimeInfo().OperatingSystem switch - { - OperatingSystemType.WinNT => (IDynamicLibraryLoader)new Win32Loader(), - OperatingSystemType.OSX => new UnixLoader(), - OperatingSystemType.Linux => new UnixLoader(), - OperatingSystemType.Android => new UnixLoader(), - // iOS, WASM, ... - _ => new NotSupportedLoader() - } + OperatingSystemEx.IsWindows() ? (IDynamicLibraryLoader)new Win32Loader() + : OperatingSystemEx.IsMacOS() || OperatingSystemEx.IsLinux() || OperatingSystemEx.IsAndroid() ? new UnixLoader() + : new NotSupportedLoader() #endif ); } diff --git a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs index ef14211902..11dc80ef8f 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs @@ -1,7 +1,8 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Data; +using Avalonia.Threading; namespace Avalonia.PropertyStore { @@ -116,26 +117,42 @@ namespace Avalonia.PropertyStore private void SetValue(BindingValue value) { - if (Frame.Owner is null) - return; + static void Execute(BindingEntryBase instance, BindingValue value) + { + if (instance.Frame.Owner is null) + return; - LoggingUtils.LogIfNecessary(Frame.Owner.Owner, Property, value); + LoggingUtils.LogIfNecessary(instance.Frame.Owner.Owner, instance.Property, value); - if (value.HasValue) - { - if (!_hasValue || !EqualityComparer.Default.Equals(_value, value.Value)) + if (value.HasValue) + { + if (!instance._hasValue || !EqualityComparer.Default.Equals(instance._value, value.Value)) + { + instance._value = value.Value; + instance._hasValue = true; + if (instance._subscription is not null && instance._subscription != s_creatingQuiet) + instance.Frame.Owner?.OnBindingValueChanged(instance, instance.Frame.Priority); + } + } + else if (value.Type != BindingValueType.DoNothing) { - _value = value.Value; - _hasValue = true; - if (_subscription is not null && _subscription != s_creatingQuiet) - Frame.Owner?.OnBindingValueChanged(this, Frame.Priority); + instance.ClearValue(); + if (instance._subscription is not null && instance._subscription != s_creatingQuiet) + instance.Frame.Owner?.OnBindingValueCleared(instance.Property, instance.Frame.Priority); } } - else if (value.Type != BindingValueType.DoNothing) + + if (Dispatcher.UIThread.CheckAccess()) { - ClearValue(); - if (_subscription is not null && _subscription != s_creatingQuiet) - Frame.Owner?.OnBindingValueCleared(Property, Frame.Priority); + Execute(this, value); + } + else + { + // To avoid allocating closure in the outer scope we need to capture variables + // locally. This allows us to skip most of the allocations when on UI thread. + var instance = this; + var newValue = value; + Dispatcher.UIThread.Post(() => Execute(instance, newValue)); } } diff --git a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs index 4dca6c0100..8acb885604 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Data; +using Avalonia.Threading; namespace Avalonia.PropertyStore { @@ -40,20 +41,56 @@ namespace Avalonia.PropertyStore public void OnNext(T value) { - if (Property.ValidateValue?.Invoke(value) != false) - _owner.SetValue(Property, value, BindingPriority.LocalValue); + static void Execute(ValueStore owner, StyledPropertyBase property, T value) + { + if (property.ValidateValue?.Invoke(value) != false) + owner.SetValue(property, value, BindingPriority.LocalValue); + else + owner.ClearLocalValue(property); + } + + if (Dispatcher.UIThread.CheckAccess()) + { + Execute(_owner, Property, value); + } else - _owner.ClearLocalValue(Property); + { + // To avoid allocating closure in the outer scope we need to capture variables + // locally. This allows us to skip most of the allocations when on UI thread. + var instance = _owner; + var property = Property; + var newValue = value; + Dispatcher.UIThread.Post(() => Execute(instance, property, newValue)); + } } public void OnNext(BindingValue value) { - LoggingUtils.LogIfNecessary(_owner.Owner, Property, value); + static void Execute(LocalValueBindingObserver instance, BindingValue value) + { + var owner = instance._owner; + var property = instance.Property; + + LoggingUtils.LogIfNecessary(owner.Owner, property, value); - if (value.HasValue) - _owner.SetValue(Property, value.Value, BindingPriority.LocalValue); - else if (value.Type != BindingValueType.DataValidationError) - _owner.ClearLocalValue(Property); + if (value.HasValue) + owner.SetValue(property, value.Value, BindingPriority.LocalValue); + else if (value.Type != BindingValueType.DataValidationError) + owner.ClearLocalValue(property); + } + + if (Dispatcher.UIThread.CheckAccess()) + { + Execute(this, value); + } + else + { + // To avoid allocating closure in the outer scope we need to capture variables + // locally. This allows us to skip most of the allocations when on UI thread. + var instance = this; + var newValue = value; + Dispatcher.UIThread.Post(() => Execute(instance, newValue)); + } } } } diff --git a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs index 099e997d38..7c529591b6 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs @@ -1,5 +1,7 @@ using System; +using System.Security.Cryptography; using Avalonia.Data; +using Avalonia.Threading; namespace Avalonia.PropertyStore { @@ -34,28 +36,47 @@ namespace Avalonia.PropertyStore public void OnNext(object? value) { - if (value is BindingNotification n) + static void Execute(LocalValueUntypedBindingObserver instance, object? value) { - value = n.Value; - LoggingUtils.LogIfNecessary(_owner.Owner, Property, n); - } + var owner = instance._owner; + var property = instance.Property; - if (value == AvaloniaProperty.UnsetValue) - { - _owner.ClearLocalValue(Property); - } - else if (value == BindingOperations.DoNothing) - { - // Do nothing! + if (value is BindingNotification n) + { + value = n.Value; + LoggingUtils.LogIfNecessary(owner.Owner, property, n); + } + + if (value == AvaloniaProperty.UnsetValue) + { + owner.ClearLocalValue(property); + } + else if (value == BindingOperations.DoNothing) + { + // Do nothing! + } + else if (UntypedValueUtils.TryConvertAndValidate(property, value, out var typedValue)) + { + owner.SetValue(property, typedValue, BindingPriority.LocalValue); + } + else + { + owner.ClearLocalValue(property); + LoggingUtils.LogInvalidValue(owner.Owner, property, typeof(T), value); + } } - else if (UntypedValueUtils.TryConvertAndValidate(Property, value, out var typedValue)) + + if (Dispatcher.UIThread.CheckAccess()) { - _owner.SetValue(Property, typedValue, BindingPriority.LocalValue); + Execute(this, value); } - else + else if (value != BindingOperations.DoNothing) { - _owner.ClearLocalValue(Property); - LoggingUtils.LogInvalidValue(_owner.Owner, Property, typeof(T), value); + // To avoid allocating closure in the outer scope we need to capture variables + // locally. This allows us to skip most of the allocations when on UI thread. + var instance = this; + var newValue = value; + Dispatcher.UIThread.Post(() => Execute(instance, newValue)); } } } diff --git a/src/Avalonia.Base/Reactive/AnonymousObserver.cs b/src/Avalonia.Base/Reactive/AnonymousObserver.cs new file mode 100644 index 0000000000..c2e02ae879 --- /dev/null +++ b/src/Avalonia.Base/Reactive/AnonymousObserver.cs @@ -0,0 +1,62 @@ +using System; +using System.Threading.Tasks; + +namespace Avalonia.Reactive; + +internal class AnonymousObserver : IObserver +{ + private static readonly Action ThrowsOnError = ex => throw ex; + private static readonly Action NoOpCompleted = () => { }; + private readonly Action _onNext; + private readonly Action _onError; + private readonly Action _onCompleted; + + public AnonymousObserver(TaskCompletionSource tcs) + { + if (tcs is null) + { + throw new ArgumentNullException(nameof(tcs)); + } + + _onNext = tcs.SetResult; + _onError = tcs.SetException; + _onCompleted = NoOpCompleted; + } + + public AnonymousObserver(Action onNext, Action onError, Action onCompleted) + { + _onNext = onNext ?? throw new ArgumentNullException(nameof(onNext)); + _onError = onError ?? throw new ArgumentNullException(nameof(onError)); + _onCompleted = onCompleted ?? throw new ArgumentNullException(nameof(onCompleted)); + } + + public AnonymousObserver(Action onNext) + : this(onNext, ThrowsOnError, NoOpCompleted) + { + } + + public AnonymousObserver(Action onNext, Action onError) + : this(onNext, onError, NoOpCompleted) + { + } + + public AnonymousObserver(Action onNext, Action onCompleted) + : this(onNext, ThrowsOnError, onCompleted) + { + } + + public void OnCompleted() + { + _onCompleted.Invoke(); + } + + public void OnError(Exception error) + { + _onError.Invoke(error); + } + + public void OnNext(T value) + { + _onNext.Invoke(value); + } +} diff --git a/src/Avalonia.Base/Reactive/CombinedSubject.cs b/src/Avalonia.Base/Reactive/CombinedSubject.cs new file mode 100644 index 0000000000..dc6153133d --- /dev/null +++ b/src/Avalonia.Base/Reactive/CombinedSubject.cs @@ -0,0 +1,23 @@ +using System; + +namespace Avalonia.Reactive; + +internal class CombinedSubject : IAvaloniaSubject +{ + private readonly IObserver _observer; + private readonly IObservable _observable; + + public CombinedSubject(IObserver observer, IObservable observable) + { + _observer = observer; + _observable = observable; + } + + public void OnCompleted() => _observer.OnCompleted(); + + public void OnError(Exception error) => _observer.OnError(error); + + public void OnNext(T value) => _observer.OnNext(value); + + public IDisposable Subscribe(IObserver observer) => _observable.Subscribe(observer); +} diff --git a/src/Avalonia.Base/Reactive/CompositeDisposable.cs b/src/Avalonia.Base/Reactive/CompositeDisposable.cs new file mode 100644 index 0000000000..4952686ad8 --- /dev/null +++ b/src/Avalonia.Base/Reactive/CompositeDisposable.cs @@ -0,0 +1,427 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading; + +namespace Avalonia.Reactive; + +internal sealed class CompositeDisposable : ICollection, IDisposable +{ + private readonly object _gate = new object(); + private bool _disposed; + private List _disposables; + private int _count; + private const int ShrinkThreshold = 64; + + /// + /// Initializes a new instance of the class with the specified number of disposables. + /// + /// The number of disposables that the new CompositeDisposable can initially store. + /// is less than zero. + public CompositeDisposable(int capacity) + { + if (capacity < 0) + { + throw new ArgumentOutOfRangeException(nameof(capacity)); + } + + _disposables = new List(capacity); + } + + /// + /// Initializes a new instance of the class from a group of disposables. + /// + /// Disposables that will be disposed together. + /// is null. + /// Any of the disposables in the collection is null. + public CompositeDisposable(params IDisposable[] disposables) + { + if (disposables == null) + { + throw new ArgumentNullException(nameof(disposables)); + } + + _disposables = ToList(disposables); + + // _count can be read by other threads and thus should be properly visible + // also releases the _disposables contents so it becomes thread-safe + Volatile.Write(ref _count, _disposables.Count); + } + + /// + /// Initializes a new instance of the class from a group of disposables. + /// + /// Disposables that will be disposed together. + /// is null. + /// Any of the disposables in the collection is null. + public CompositeDisposable(IList disposables) + { + if (disposables == null) + { + throw new ArgumentNullException(nameof(disposables)); + } + + _disposables = ToList(disposables); + + // _count can be read by other threads and thus should be properly visible + // also releases the _disposables contents so it becomes thread-safe + Volatile.Write(ref _count, _disposables.Count); + } + + private static List ToList(IEnumerable disposables) + { + var capacity = disposables switch + { + IDisposable[] a => a.Length, + ICollection c => c.Count, + _ => 12 + }; + + var list = new List(capacity); + + // do the copy and null-check in one step to avoid a + // second loop for just checking for null items + foreach (var d in disposables) + { + if (d == null) + { + throw new ArgumentException("Disposables can't contain null", nameof(disposables)); + } + + list.Add(d); + } + + return list; + } + + /// + /// Gets the number of disposables contained in the . + /// + public int Count => Volatile.Read(ref _count); + + /// + /// Adds a disposable to the or disposes the disposable if the is disposed. + /// + /// Disposable to add. + /// is null. + public void Add(IDisposable item) + { + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + lock (_gate) + { + if (!_disposed) + { + _disposables.Add(item); + + // If read atomically outside the lock, it should be written atomically inside + // the plain read on _count is fine here because manipulation always happens + // from inside a lock. + Volatile.Write(ref _count, _count + 1); + return; + } + } + + item.Dispose(); + } + + /// + /// Removes and disposes the first occurrence of a disposable from the . + /// + /// Disposable to remove. + /// true if found; false otherwise. + /// is null. + public bool Remove(IDisposable item) + { + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + lock (_gate) + { + // this composite was already disposed and if the item was in there + // it has been already removed/disposed + if (_disposed) + { + return false; + } + + // + // List doesn't shrink the size of the underlying array but does collapse the array + // by copying the tail one position to the left of the removal index. We don't need + // index-based lookup but only ordering for sequential disposal. So, instead of spending + // cycles on the Array.Copy imposed by Remove, we use a null sentinel value. We also + // do manual Swiss cheese detection to shrink the list if there's a lot of holes in it. + // + + // read fields as infrequently as possible + var current = _disposables; + + var i = current.IndexOf(item); + if (i < 0) + { + // not found, just return + return false; + } + + current[i] = null; + + if (current.Capacity > ShrinkThreshold && _count < current.Capacity / 2) + { + var fresh = new List(current.Capacity / 2); + + foreach (var d in current) + { + if (d != null) + { + fresh.Add(d); + } + } + + _disposables = fresh; + } + + // make sure the Count property sees an atomic update + Volatile.Write(ref _count, _count - 1); + } + + // if we get here, the item was found and removed from the list + // just dispose it and report success + + item.Dispose(); + + return true; + } + + /// + /// Disposes all disposables in the group and removes them from the group. + /// + public void Dispose() + { + List? currentDisposables = null; + + lock (_gate) + { + if (!_disposed) + { + currentDisposables = _disposables; + + // nulling out the reference is faster no risk to + // future Add/Remove because _disposed will be true + // and thus _disposables won't be touched again. + _disposables = null!; // NB: All accesses are guarded by _disposed checks. + + Volatile.Write(ref _count, 0); + Volatile.Write(ref _disposed, true); + } + } + + if (currentDisposables != null) + { + foreach (var d in currentDisposables) + { + d?.Dispose(); + } + } + } + + /// + /// Removes and disposes all disposables from the , but does not dispose the . + /// + public void Clear() + { + IDisposable?[] previousDisposables; + + lock (_gate) + { + // disposed composites are always clear + if (_disposed) + { + return; + } + + var current = _disposables; + + previousDisposables = current.ToArray(); + current.Clear(); + + Volatile.Write(ref _count, 0); + } + + foreach (var d in previousDisposables) + { + d?.Dispose(); + } + } + + /// + /// Determines whether the contains a specific disposable. + /// + /// Disposable to search for. + /// true if the disposable was found; otherwise, false. + /// is null. + public bool Contains(IDisposable item) + { + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + lock (_gate) + { + if (_disposed) + { + return false; + } + + return _disposables.Contains(item); + } + } + + /// + /// Copies the disposables contained in the to an array, starting at a particular array index. + /// + /// Array to copy the contained disposables to. + /// Target index at which to copy the first disposable of the group. + /// is null. + /// is less than zero. -or - is larger than or equal to the array length. + public void CopyTo(IDisposable[] array, int arrayIndex) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + if (arrayIndex < 0 || arrayIndex >= array.Length) + { + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + } + + lock (_gate) + { + // disposed composites are always empty + if (_disposed) + { + return; + } + + if (arrayIndex + _count > array.Length) + { + // there is not enough space beyond arrayIndex + // to accommodate all _count disposables in this composite + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + } + + var i = arrayIndex; + + foreach (var d in _disposables) + { + if (d != null) + { + array[i++] = d; + } + } + } + } + + /// + /// Always returns false. + /// + public bool IsReadOnly => false; + + /// + /// Returns an enumerator that iterates through the . + /// + /// An enumerator to iterate over the disposables. + public IEnumerator GetEnumerator() + { + lock (_gate) + { + if (_disposed || _count == 0) + { + return EmptyEnumerator; + } + + // the copy is unavoidable but the creation + // of an outer IEnumerable is avoidable + return new CompositeEnumerator(_disposables.ToArray()); + } + } + + /// + /// Returns an enumerator that iterates through the . + /// + /// An enumerator to iterate over the disposables. + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Gets a value that indicates whether the object is disposed. + /// + public bool IsDisposed => Volatile.Read(ref _disposed); + + /// + /// An empty enumerator for the + /// method to avoid allocation on disposed or empty composites. + /// + private static readonly CompositeEnumerator EmptyEnumerator = + new CompositeEnumerator(Array.Empty()); + + /// + /// An enumerator for an array of disposables. + /// + private sealed class CompositeEnumerator : IEnumerator + { + private readonly IDisposable?[] _disposables; + private int _index; + + public CompositeEnumerator(IDisposable?[] disposables) + { + _disposables = disposables; + _index = -1; + } + + public IDisposable Current => _disposables[_index]!; // NB: _index is only advanced to non-null positions. + + object IEnumerator.Current => _disposables[_index]!; + + public void Dispose() + { + // Avoid retention of the referenced disposables + // beyond the lifecycle of the enumerator. + // Not sure if this happens by default to + // generic array enumerators though. + var disposables = _disposables; + Array.Clear(disposables, 0, disposables.Length); + } + + public bool MoveNext() + { + var disposables = _disposables; + + for (;;) + { + var idx = ++_index; + + if (idx >= disposables.Length) + { + return false; + } + + // inlined that filter for null elements + if (disposables[idx] != null) + { + return true; + } + } + } + + public void Reset() + { + _index = -1; + } + } +} diff --git a/src/Avalonia.Base/Reactive/Disposable.cs b/src/Avalonia.Base/Reactive/Disposable.cs new file mode 100644 index 0000000000..17b0d01db5 --- /dev/null +++ b/src/Avalonia.Base/Reactive/Disposable.cs @@ -0,0 +1,98 @@ +using System; +using System.Threading; + +namespace Avalonia.Reactive; + +/// +/// Provides a set of static methods for creating objects. +/// +internal static class Disposable +{ + /// + /// Represents a disposable that does nothing on disposal. + /// + private sealed class EmptyDisposable : IDisposable + { + public static readonly EmptyDisposable Instance = new(); + + private EmptyDisposable() + { + } + + public void Dispose() + { + // no op + } + } + + internal sealed class AnonymousDisposable : IDisposable + { + private volatile Action? _dispose; + public AnonymousDisposable(Action dispose) + { + _dispose = dispose; + } + public bool IsDisposed => _dispose == null; + public void Dispose() + { + Interlocked.Exchange(ref _dispose, null)?.Invoke(); + } + } + + internal sealed class AnonymousDisposable : IDisposable + { + private TState _state; + private volatile Action? _dispose; + + public AnonymousDisposable(TState state, Action dispose) + { + _state = state; + _dispose = dispose; + } + + public bool IsDisposed => _dispose == null; + public void Dispose() + { + Interlocked.Exchange(ref _dispose, null)?.Invoke(_state); + _state = default!; + } + } + + /// + /// Gets the disposable that does nothing when disposed. + /// + public static IDisposable Empty => EmptyDisposable.Instance; + + /// + /// Creates a disposable object that invokes the specified action when disposed. + /// + /// Action to run during the first call to . The action is guaranteed to be run at most once. + /// The disposable object that runs the given action upon disposal. + /// is null. + public static IDisposable Create(Action dispose) + { + if (dispose == null) + { + throw new ArgumentNullException(nameof(dispose)); + } + + return new AnonymousDisposable(dispose); + } + + /// + /// Creates a disposable object that invokes the specified action when disposed. + /// + /// The state to be passed to the action. + /// Action to run during the first call to . The action is guaranteed to be run at most once. + /// The disposable object that runs the given action upon disposal. + /// is null. + public static IDisposable Create(TState state, Action dispose) + { + if (dispose == null) + { + throw new ArgumentNullException(nameof(dispose)); + } + + return new AnonymousDisposable(state, dispose); + } +} diff --git a/src/Avalonia.Base/Reactive/DisposableMixin.cs b/src/Avalonia.Base/Reactive/DisposableMixin.cs new file mode 100644 index 0000000000..478312fa47 --- /dev/null +++ b/src/Avalonia.Base/Reactive/DisposableMixin.cs @@ -0,0 +1,37 @@ +using System; +using Avalonia.Reactive; + +namespace Avalonia.Reactive; + +/// +/// Extension methods associated with the IDisposable interface. +/// +internal static class DisposableMixin +{ + /// + /// Ensures the provided disposable is disposed with the specified . + /// + /// + /// The type of the disposable. + /// + /// + /// The disposable we are going to want to be disposed by the CompositeDisposable. + /// + /// + /// The to which will be added. + /// + /// + /// The disposable. + /// + public static T DisposeWith(this T item, CompositeDisposable compositeDisposable) + where T : IDisposable + { + if (compositeDisposable is null) + { + throw new ArgumentNullException(nameof(compositeDisposable)); + } + + compositeDisposable.Add(item); + return item; + } +} diff --git a/src/Avalonia.Base/Reactive/IAvaloniaSubject.cs b/src/Avalonia.Base/Reactive/IAvaloniaSubject.cs new file mode 100644 index 0000000000..272e2190b8 --- /dev/null +++ b/src/Avalonia.Base/Reactive/IAvaloniaSubject.cs @@ -0,0 +1,8 @@ +using System; + +namespace Avalonia.Reactive; + +internal interface IAvaloniaSubject : IObserver, IObservable /*, ISubject */ +{ + +} diff --git a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs index 9a3ab89b62..263109972f 100644 --- a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs +++ b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Reactive; -using System.Reactive.Disposables; using System.Threading; using Avalonia.Threading; @@ -12,7 +10,7 @@ namespace Avalonia.Reactive /// /// The observable type. /// - /// is rather heavyweight in terms of allocations and memory + /// ObservableBase{T} is rather heavyweight in terms of allocations and memory /// usage. This class provides a more lightweight base for some internal observable types /// in the Avalonia framework. /// @@ -21,11 +19,13 @@ namespace Avalonia.Reactive private Exception? _error; private List>? _observers = new List>(); + public bool HasObservers => _observers?.Count > 0; + public IDisposable Subscribe(IObserver observer) { _ = observer ?? throw new ArgumentNullException(nameof(observer)); - Dispatcher.UIThread.VerifyAccess(); + //Dispatcher.UIThread.VerifyAccess(); var first = false; diff --git a/src/Avalonia.Base/Reactive/LightweightSubject.cs b/src/Avalonia.Base/Reactive/LightweightSubject.cs new file mode 100644 index 0000000000..5663fd66d3 --- /dev/null +++ b/src/Avalonia.Base/Reactive/LightweightSubject.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using Avalonia.Threading; + +namespace Avalonia.Reactive; + +internal class LightweightSubject : LightweightObservableBase, IAvaloniaSubject +{ + public void OnCompleted() + { + PublishCompleted(); + } + + public void OnError(Exception error) + { + PublishError(error); + } + + public void OnNext(T value) + { + PublishNext(value); + } + + protected override void Initialize() { } + + protected override void Deinitialize() { } +} diff --git a/src/Avalonia.Base/Reactive/Observable.cs b/src/Avalonia.Base/Reactive/Observable.cs new file mode 100644 index 0000000000..1009829429 --- /dev/null +++ b/src/Avalonia.Base/Reactive/Observable.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Reactive.Operators; +using Avalonia.Threading; + +namespace Avalonia.Reactive; + +/// +/// Provides common observable methods as a replacement for the Rx framework. +/// +internal static class Observable +{ + public static IObservable Create(Func, IDisposable> subscribe) + { + return new CreateWithDisposableObservable(subscribe); + } + + public static IDisposable Subscribe(this IObservable source, Action action) + { + return source.Subscribe(new AnonymousObserver(action)); + } + + public static IObservable Select(this IObservable source, Func selector) + { + return Create(obs => + { + return source.Subscribe(new AnonymousObserver( + input => + { + TResult value; + try + { + value = selector(input); + } + catch (Exception ex) + { + obs.OnError(ex); + return; + } + + obs.OnNext(value); + }, obs.OnError, obs.OnCompleted)); + }); + } + + public static IObservable StartWith(this IObservable source, TSource value) + { + return Create(obs => + { + obs.OnNext(value); + return source.Subscribe(obs); + }); + } + + public static IObservable Where(this IObservable source, Func predicate) + { + return Create(obs => + { + return source.Subscribe(new AnonymousObserver( + input => + { + bool shouldRun; + try + { + shouldRun = predicate(input); + } + catch (Exception ex) + { + obs.OnError(ex); + return; + } + if (shouldRun) + { + obs.OnNext(input); + } + }, obs.OnError, obs.OnCompleted)); + }); + } + + public static IObservable Switch( + this IObservable> sources) + { + return new Switch(sources); + } + + public static IObservable CombineLatest( + this IObservable first, IObservable second, + Func resultSelector) + { + return new CombineLatest(first, second, resultSelector); + } + + public static IObservable CombineLatest( + this IEnumerable> inputs) + { + return new CombineLatest(inputs, items => items); + } + + public static IObservable Skip(this IObservable source, int skipCount) + { + if (skipCount <= 0) + { + throw new ArgumentException("Skip count must be bigger than zero", nameof(skipCount)); + } + + return Create(obs => + { + var remaining = skipCount; + return source.Subscribe(new AnonymousObserver( + input => + { + if (remaining <= 0) + { + obs.OnNext(input); + } + else + { + remaining--; + } + }, obs.OnError, obs.OnCompleted)); + }); + } + + public static IObservable Take(this IObservable source, int takeCount) + { + if (takeCount <= 0) + { + return Empty(); + } + + return Create(obs => + { + var remaining = takeCount; + IDisposable? sub = null; + sub = source.Subscribe(new AnonymousObserver( + input => + { + if (remaining > 0) + { + --remaining; + obs.OnNext(input); + + if (remaining == 0) + { + sub?.Dispose(); + obs.OnCompleted(); + } + } + }, obs.OnError, obs.OnCompleted)); + return sub; + }); + } + + public static IObservable FromEventPattern(Action addHandler, Action removeHandler) + { + return Create(observer => + { + var handler = new Action(observer.OnNext); + var converted = new EventHandler((_, args) => handler(args)); + addHandler(converted); + + return Disposable.Create(() => removeHandler(converted)); + }); + } + + public static IObservable Return(T value) + { + return new ReturnImpl(value); + } + + public static IObservable Empty() + { + return EmptyImpl.Instance; + } + + /// + /// Returns an observable that fires once with the specified value and never completes. + /// + /// The type of the value. + /// The value. + /// The observable. + public static IObservable SingleValue(T value) + { + return new SingleValueImpl(value); + } + + private sealed class SingleValueImpl : IObservable + { + private readonly T _value; + + public SingleValueImpl(T value) + { + _value = value; + } + public IDisposable Subscribe(IObserver observer) + { + observer.OnNext(_value); + return Disposable.Empty; + } + } + + private sealed class ReturnImpl : IObservable + { + private readonly T _value; + + public ReturnImpl(T value) + { + _value = value; + } + public IDisposable Subscribe(IObserver observer) + { + observer.OnNext(_value); + observer.OnCompleted(); + return Disposable.Empty; + } + } + + internal sealed class EmptyImpl : IObservable + { + internal static readonly IObservable Instance = new EmptyImpl(); + + private EmptyImpl() { } + + public IDisposable Subscribe(IObserver observer) + { + observer.OnCompleted(); + return Disposable.Empty; + } + } + + private sealed class CreateWithDisposableObservable : IObservable + { + private readonly Func, IDisposable> _subscribe; + + public CreateWithDisposableObservable(Func, IDisposable> subscribe) + { + _subscribe = subscribe; + } + + public IDisposable Subscribe(IObserver observer) + { + return _subscribe(observer); + } + } +} diff --git a/src/Avalonia.Base/Reactive/ObservableEx.cs b/src/Avalonia.Base/Reactive/ObservableEx.cs deleted file mode 100644 index 1cea568c88..0000000000 --- a/src/Avalonia.Base/Reactive/ObservableEx.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Reactive.Disposables; - -namespace Avalonia.Reactive -{ - /// - /// Provides common observable methods not found in standard Rx framework. - /// - public static class ObservableEx - { - /// - /// Returns an observable that fires once with the specified value and never completes. - /// - /// The type of the value. - /// The value. - /// The observable. - public static IObservable SingleValue(T value) - { - return new SingleValueImpl(value); - } - - private class SingleValueImpl : IObservable - { - private T _value; - - public SingleValueImpl(T value) - { - _value = value; - } - public IDisposable Subscribe(IObserver observer) - { - observer.OnNext(_value); - return Disposable.Empty; - } - } - } -} diff --git a/src/Avalonia.Base/Reactive/Operators/CombineLatest.cs b/src/Avalonia.Base/Reactive/Operators/CombineLatest.cs new file mode 100644 index 0000000000..aa95ceec35 --- /dev/null +++ b/src/Avalonia.Base/Reactive/Operators/CombineLatest.cs @@ -0,0 +1,374 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace Avalonia.Reactive.Operators; + +// Code based on https://github.com/dotnet/reactive/blob/main/Rx.NET/Source/src/System.Reactive/Linq/Observable/CombineLatest.cs + +internal sealed class CombineLatest : IObservable +{ + private readonly IObservable _first; + private readonly IObservable _second; + private readonly Func _resultSelector; + + public CombineLatest(IObservable first, IObservable second, + Func resultSelector) + { + _first = first; + _second = second; + _resultSelector = resultSelector; + } + + public IDisposable Subscribe(IObserver observer) + { + var sink = new _(_resultSelector, observer); + sink.Run(_first, _second); + return sink; + } + + internal sealed class _ : IdentitySink + { + private readonly Func _resultSelector; + private readonly object _gate = new object(); + + public _(Func resultSelector, IObserver observer) + : base(observer) + { + _resultSelector = resultSelector; + _firstDisposable = null!; + _secondDisposable = null!; + } + + private IDisposable _firstDisposable; + private IDisposable _secondDisposable; + + public void Run(IObservable first, IObservable second) + { + var fstO = new FirstObserver(this); + var sndO = new SecondObserver(this); + + fstO.SetOther(sndO); + sndO.SetOther(fstO); + + _firstDisposable = first.Subscribe(fstO); + _secondDisposable = second.Subscribe(sndO); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _firstDisposable.Dispose(); + _secondDisposable.Dispose(); + } + + base.Dispose(disposing); + } + + private sealed class FirstObserver : IObserver + { + private readonly _ _parent; + private SecondObserver _other; + + public FirstObserver(_ parent) + { + _parent = parent; + _other = default!; // NB: Will be set by SetOther. + } + + public void SetOther(SecondObserver other) { _other = other; } + + public bool HasValue { get; private set; } + public TFirst? Value { get; private set; } + public bool Done { get; private set; } + + public void OnNext(TFirst value) + { + lock (_parent._gate) + { + HasValue = true; + Value = value; + + if (_other.HasValue) + { + TResult res; + try + { + res = _parent._resultSelector(value, _other.Value!); + } + catch (Exception ex) + { + _parent.ForwardOnError(ex); + return; + } + + _parent.ForwardOnNext(res); + } + else if (_other.Done) + { + _parent.ForwardOnCompleted(); + } + } + } + + public void OnError(Exception error) + { + lock (_parent._gate) + { + _parent.ForwardOnError(error); + } + } + + public void OnCompleted() + { + lock (_parent._gate) + { + Done = true; + + if (_other.Done) + { + _parent.ForwardOnCompleted(); + } + else + { + _parent._firstDisposable.Dispose(); + } + } + } + } + + private sealed class SecondObserver : IObserver + { + private readonly _ _parent; + private FirstObserver _other; + + public SecondObserver(_ parent) + { + _parent = parent; + _other = default!; // NB: Will be set by SetOther. + } + + public void SetOther(FirstObserver other) { _other = other; } + + public bool HasValue { get; private set; } + public TSecond? Value { get; private set; } + public bool Done { get; private set; } + + public void OnNext(TSecond value) + { + lock (_parent._gate) + { + HasValue = true; + Value = value; + + if (_other.HasValue) + { + TResult res; + try + { + res = _parent._resultSelector(_other.Value!, value); + } + catch (Exception ex) + { + _parent.ForwardOnError(ex); + return; + } + + _parent.ForwardOnNext(res); + } + else if (_other.Done) + { + _parent.ForwardOnCompleted(); + } + } + } + + public void OnError(Exception error) + { + lock (_parent._gate) + { + _parent.ForwardOnError(error); + } + } + + public void OnCompleted() + { + lock (_parent._gate) + { + Done = true; + + if (_other.Done) + { + _parent.ForwardOnCompleted(); + } + else + { + _parent._secondDisposable.Dispose(); + } + } + } + } + } +} + +internal sealed class CombineLatest : IObservable +{ + private readonly IEnumerable> _sources; + private readonly Func _resultSelector; + + public CombineLatest(IEnumerable> sources, Func resultSelector) + { + _sources = sources; + _resultSelector = resultSelector; + } + + public IDisposable Subscribe(IObserver observer) + { + var sink = new _(_resultSelector, observer); + sink.Run(_sources); + return sink; + } + + internal sealed class _ : IdentitySink + { + private readonly object _gate = new object(); + private readonly Func _resultSelector; + + public _(Func resultSelector, IObserver observer) + : base(observer) + { + _resultSelector = resultSelector; + + // NB: These will be set in Run before getting used. + _hasValue = null!; + _values = null!; + _isDone = null!; + _subscriptions = null!; + } + + private bool[] _hasValue; + private bool _hasValueAll; + private TSource[] _values; + private bool[] _isDone; + private IDisposable[] _subscriptions; + + public void Run(IEnumerable> sources) + { + var srcs = sources.ToArray(); + + var N = srcs.Length; + + _hasValue = new bool[N]; + _hasValueAll = false; + + _values = new TSource[N]; + + _isDone = new bool[N]; + + _subscriptions = new IDisposable[N]; + + for (var i = 0; i < N; i++) + { + var j = i; + + var o = new SourceObserver(this, j); + _subscriptions[j] = o; + + o.Disposable = srcs[j].Subscribe(o); + } + + SetUpstream(new CompositeDisposable(_subscriptions)); + } + + private void OnNext(int index, TSource value) + { + lock (_gate) + { + _values[index] = value; + + _hasValue[index] = true; + + if (_hasValueAll || (_hasValueAll = _hasValue.All(v => v))) + { + TResult res; + try + { + res = _resultSelector(_values); + } + catch (Exception ex) + { + ForwardOnError(ex); + return; + } + + ForwardOnNext(res); + } + else if (_isDone.Where((_, i) => i != index).All(d => d)) + { + ForwardOnCompleted(); + } + } + } + + private new void OnError(Exception error) + { + lock (_gate) + { + ForwardOnError(error); + } + } + + private void OnCompleted(int index) + { + lock (_gate) + { + _isDone[index] = true; + + if (_isDone.All(d => d)) + { + ForwardOnCompleted(); + } + else + { + _subscriptions[index].Dispose(); + } + } + } + + private sealed class SourceObserver : IObserver, IDisposable + { + private readonly _ _parent; + private readonly int _index; + + public SourceObserver(_ parent, int index) + { + _parent = parent; + _index = index; + } + + public IDisposable? Disposable { get; set; } + + public void OnNext(TSource value) + { + _parent.OnNext(_index, value); + } + + public void OnError(Exception error) + { + _parent.OnError(error); + } + + public void OnCompleted() + { + _parent.OnCompleted(_index); + } + + public void Dispose() + { + Disposable?.Dispose(); + } + } + } +} diff --git a/src/Avalonia.Base/Reactive/Operators/Sink.cs b/src/Avalonia.Base/Reactive/Operators/Sink.cs new file mode 100644 index 0000000000..0fef350acc --- /dev/null +++ b/src/Avalonia.Base/Reactive/Operators/Sink.cs @@ -0,0 +1,111 @@ +using System; +using System.Threading; + +namespace Avalonia.Reactive.Operators; + +// Code based on https://github.com/dotnet/reactive/blob/main/Rx.NET/Source/src/System.Reactive/Internal/Sink.cs + +internal abstract class Sink : IDisposable +{ + private IDisposable? _upstream; + private volatile IObserver _observer; + + protected Sink(IObserver observer) + { + _observer = observer; + } + + public void Dispose() + { + Dispose(true); + } + + /// + /// Override this method to dispose additional resources. + /// The method is guaranteed to be called at most once. + /// + /// If true, the method was called from . + protected virtual void Dispose(bool disposing) + { + //Calling base.Dispose(true) is not a proper disposal, so we can omit the assignment here. + //Sink is internal so this can pretty much be enforced. + //_observer = NopObserver.Instance; + + _upstream?.Dispose(); + } + + public void ForwardOnNext(TTarget value) + { + _observer.OnNext(value); + } + + public void ForwardOnCompleted() + { + _observer.OnCompleted(); + Dispose(); + } + + public void ForwardOnError(Exception error) + { + _observer.OnError(error); + Dispose(); + } + + protected void SetUpstream(IDisposable upstream) + { + _upstream = upstream; + } + + protected void DisposeUpstream() + { + _upstream?.Dispose(); + } +} + +internal abstract class Sink : Sink, IObserver +{ + protected Sink(IObserver observer) : base(observer) + { + } + + public virtual void Run(IObservable source) + { + SetUpstream(source.Subscribe(this)); + } + + public abstract void OnNext(TSource value); + + public virtual void OnError(Exception error) => ForwardOnError(error); + + public virtual void OnCompleted() => ForwardOnCompleted(); + + public IObserver GetForwarder() => new _(this); + + private sealed class _ : IObserver + { + private readonly Sink _forward; + + public _(Sink forward) + { + _forward = forward; + } + + public void OnNext(TTarget value) => _forward.ForwardOnNext(value); + + public void OnError(Exception error) => _forward.ForwardOnError(error); + + public void OnCompleted() => _forward.ForwardOnCompleted(); + } +} + +internal abstract class IdentitySink : Sink +{ + protected IdentitySink(IObserver observer) : base(observer) + { + } + + public override void OnNext(T value) + { + ForwardOnNext(value); + } +} diff --git a/src/Avalonia.Base/Reactive/Operators/Switch.cs b/src/Avalonia.Base/Reactive/Operators/Switch.cs new file mode 100644 index 0000000000..bc849c499c --- /dev/null +++ b/src/Avalonia.Base/Reactive/Operators/Switch.cs @@ -0,0 +1,144 @@ +using System; + +namespace Avalonia.Reactive.Operators; + +// Code based on https://github.com/dotnet/reactive/blob/main/Rx.NET/Source/src/System.Reactive/Linq/Observable/Switch.cs + +internal sealed class Switch : IObservable +{ + private readonly IObservable> _sources; + + public Switch(IObservable> sources) + { + _sources = sources; + } + + public IDisposable Subscribe(IObserver observer) + { + return _sources.Subscribe(new _(observer)); + } + + internal sealed class _ : Sink, TSource> + { + private readonly object _gate = new object(); + + public _(IObserver observer) + : base(observer) + { + } + + private IDisposable? _innerSerialDisposable; + private bool _isStopped; + private ulong _latest; + private bool _hasLatest; + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _innerSerialDisposable?.Dispose(); + } + + base.Dispose(disposing); + } + + public override void OnNext(IObservable value) + { + ulong id; + + lock (_gate) + { + id = unchecked(++_latest); + _hasLatest = true; + } + + var innerObserver = new InnerObserver(this, id); + + _innerSerialDisposable = innerObserver; + innerObserver.Disposable = value.Subscribe(innerObserver); + } + + public override void OnError(Exception error) + { + lock (_gate) + { + ForwardOnError(error); + } + } + + public override void OnCompleted() + { + lock (_gate) + { + DisposeUpstream(); + + _isStopped = true; + if (!_hasLatest) + { + ForwardOnCompleted(); + } + } + } + + private sealed class InnerObserver : IObserver, IDisposable + { + private readonly _ _parent; + private readonly ulong _id; + + public InnerObserver(_ parent, ulong id) + { + _parent = parent; + _id = id; + } + + public IDisposable? Disposable { get; set; } + + public void OnNext(TSource value) + { + lock (_parent._gate) + { + if (_parent._latest == _id) + { + _parent.ForwardOnNext(value); + } + } + } + + public void OnError(Exception error) + { + lock (_parent._gate) + { + Dispose(); + + if (_parent._latest == _id) + { + _parent.ForwardOnError(error); + } + } + } + + public void OnCompleted() + { + lock (_parent._gate) + { + Dispose(); + + if (_parent._latest == _id) + { + _parent._hasLatest = false; + + if (_parent._isStopped) + { + _parent.ForwardOnCompleted(); + } + } + } + } + + public void Dispose() + { + Disposable?.Dispose(); + } + } + } +} diff --git a/src/Avalonia.Base/Reactive/SerialDisposableValue.cs b/src/Avalonia.Base/Reactive/SerialDisposableValue.cs new file mode 100644 index 0000000000..9eaf6343bf --- /dev/null +++ b/src/Avalonia.Base/Reactive/SerialDisposableValue.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading; + +namespace Avalonia.Reactive; + +/// +/// Represents a disposable resource whose underlying disposable resource can be replaced by another disposable resource, causing automatic disposal of the previous underlying disposable resource. +/// +internal sealed class SerialDisposableValue : IDisposable +{ + private IDisposable? _current; + private bool _disposed; + + public IDisposable? Disposable + { + get => _current; + set + { + _current?.Dispose(); + _current = value; + + if (_disposed) + { + _current?.Dispose(); + _current = null; + } + } + } + + public void Dispose() + { + _disposed = true; + _current?.Dispose(); + } +} diff --git a/src/Avalonia.Base/Rect.cs b/src/Avalonia.Base/Rect.cs index a91b089a33..831ab28adc 100644 --- a/src/Avalonia.Base/Rect.cs +++ b/src/Avalonia.Base/Rect.cs @@ -18,7 +18,8 @@ namespace Avalonia /// /// An empty rectangle. /// - public static readonly Rect Empty = default(Rect); + [Obsolete("Use the default keyword instead.")] + public static readonly Rect Empty = default; /// /// The X position. @@ -169,12 +170,16 @@ namespace Avalonia public Point Center => new Point(_x + (_width / 2), _y + (_height / 2)); /// - /// Gets a value that indicates whether the rectangle is empty. + /// Gets a value indicating whether the instance has default values (the rectangle is empty). /// // ReSharper disable CompareOfFloatsByEqualityOperator - public bool IsEmpty => _width == 0 && _height == 0; + public bool IsDefault => _width == 0 && _height == 0; // ReSharper restore CompareOfFloatsByEqualityOperator + /// + [Obsolete("Use IsDefault instead.")] + public bool IsEmpty => IsDefault; + /// /// Checks for equality between two s. /// @@ -390,7 +395,7 @@ namespace Avalonia } else { - return Empty; + return default; } } @@ -457,13 +462,13 @@ namespace Avalonia /// public Rect Normalize() { - Rect rect = this; + Rect rect = this; if(double.IsNaN(rect.Right) || double.IsNaN(rect.Bottom) || double.IsNaN(rect.X) || double.IsNaN(rect.Y) || double.IsNaN(Height) || double.IsNaN(Width)) { - return Rect.Empty; + return default; } if (rect.Width < 0) @@ -493,11 +498,11 @@ namespace Avalonia /// The union. public Rect Union(Rect rect) { - if (IsEmpty) + if (IsDefault) { return rect; } - else if (rect.IsEmpty) + else if (rect.IsDefault) { return this; } diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs index 889c1d34c8..80b6bb9d2a 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs @@ -80,4 +80,6 @@ internal abstract class AnimationInstanceBase : IAnimationInstance _invalidated = true; TargetObject.NotifyAnimatedValueChanged(Property); } + + public void OnTick() => Invalidate(); } diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs index 8ec4ec19bc..b14d2c0b19 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs @@ -6,7 +6,7 @@ using Avalonia.Rendering.Composition.Server; namespace Avalonia.Rendering.Composition.Animations { - internal interface IAnimationInstance + internal interface IAnimationInstance : IServerClockItem { ServerObject TargetObject { get; } ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue); diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisual.cs new file mode 100644 index 0000000000..f816231781 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisual.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition; + +public class CompositionCustomVisual : CompositionContainerVisual +{ + private List? _messages; + + internal CompositionCustomVisual(Compositor compositor, CompositionCustomVisualHandler handler) + : base(compositor, new ServerCompositionCustomVisual(compositor.Server, handler)) + { + + } + + public void SendHandlerMessage(object message) + { + (_messages ??= new()).Add(message); + RegisterForSerialization(); + } + + private protected override void SerializeChangesCore(BatchStreamWriter writer) + { + base.SerializeChangesCore(writer); + if (_messages == null || _messages.Count == 0) + writer.Write(0); + else + { + writer.Write(_messages.Count); + foreach (var m in _messages) + writer.WriteObject(m); + _messages.Clear(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs b/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs new file mode 100644 index 0000000000..b7ed6fe612 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs @@ -0,0 +1,65 @@ +using System; +using System.Numerics; +using Avalonia.Media; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition; + +public abstract class CompositionCustomVisualHandler +{ + private ServerCompositionCustomVisual? _host; + + public virtual void OnMessage(object message) + { + + } + + public virtual void OnAnimationFrameUpdate() + { + + } + + public abstract void OnRender(ImmediateDrawingContext drawingContext); + + void VerifyAccess() + { + if (_host == null) + throw new InvalidOperationException("Object is not yet attached to the compositor"); + _host.Compositor.VerifyAccess(); + } + + protected Vector2 EffectiveSize + { + get + { + VerifyAccess(); + return _host!.Size; + } + } + + protected TimeSpan CompositionNow + { + get + { + VerifyAccess(); + return _host!.Compositor.ServerNow; + } + } + + public virtual Rect GetRenderBounds() => + new(0, 0, EffectiveSize.X, EffectiveSize.Y); + + internal void Attach(ServerCompositionCustomVisual visual) => _host = visual; + + protected void Invalidate() + { + VerifyAccess(); + _host!.HandlerInvalidate(); + } + + protected void RegisterForNextAnimationFrameUpdate() + { + VerifyAccess(); + _host!.HandlerRegisterForNextAnimationFrameUpdate(); + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs index 4ce87b67a5..2b1b3f461f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs @@ -33,4 +33,6 @@ public partial class Compositor public CompositionSolidColorVisual CreateSolidColorVisual() => new(this, new ServerCompositionSolidColorVisual(Server)); + + public CompositionCustomVisual CreateCustomVisual(CompositionCustomVisualHandler handler) => new(this, handler); } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index b4817bfe9a..7fc5487171 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -111,7 +111,7 @@ namespace Avalonia.Rendering.Composition } } - batch.CommitedAt = Server.Clock.Elapsed; + batch.CommittedAt = Server.Clock.Elapsed; _server.EnqueueBatch(batch); lock (_pendingBatchLock) diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs index 6e53a138cd..21f14283b5 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs @@ -654,7 +654,7 @@ namespace Avalonia.Rendering.Composition.Expressions } } - res = default(T); + res = default; return false; } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 03859d241f..e6bbba6ec0 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -163,7 +163,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont public CompositionDrawList? VisualBrushDrawList { get; set; } public Size GetRenderTargetSize(IVisualBrush brush) { - return VisualBrushDrawList?.Size ?? Size.Empty; + return VisualBrushDrawList?.Size ?? default; } public void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/IServerClockItem.cs b/src/Avalonia.Base/Rendering/Composition/Server/IServerClockItem.cs new file mode 100644 index 0000000000..d13f1158c8 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/IServerClockItem.cs @@ -0,0 +1,6 @@ +namespace Avalonia.Rendering.Composition.Server; + +internal interface IServerClockItem +{ + void OnTick(); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index 6cbd2797f9..aebe3a5cdd 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -37,7 +37,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua { if (_contentBounds == null) { - var rect = Rect.Empty; + var rect = default(Rect); if(_renderCommands!=null) foreach (var cmd in _renderCommands) rect = rect.Union(cmd.Item.Bounds); @@ -48,7 +48,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua } } - protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) + protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt) { if (reader.Read() == 1) { @@ -56,7 +56,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua _renderCommands = reader.ReadObject(); _contentBounds = null; } - base.DeserializeChangesCore(reader, commitedAt); + base.DeserializeChangesCore(reader, committedAt); } protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 8ecc43dd6e..b172430fbb 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -87,10 +87,8 @@ namespace Avalonia.Rendering.Composition.Server } _renderTarget ??= _compositor.CreateRenderTarget(_surfaces()); - - Compositor.UpdateServerTime(); - if(_dirtyRect.IsEmpty && !_redrawRequested) + if(_dirtyRect.IsDefault && !_redrawRequested) return; Revision++; @@ -119,7 +117,7 @@ namespace Avalonia.Rendering.Composition.Server _dirtyRect = new Rect(0, 0, layerSize.Width, layerSize.Height); } - if (!_dirtyRect.IsEmpty) + if (!_dirtyRect.IsDefault) { var visualBrushHelper = new CompositorDrawingContextProxy.VisualBrushRenderer(); using (var context = _layer.CreateDrawingContext(visualBrushHelper)) @@ -162,7 +160,7 @@ namespace Avalonia.Rendering.Composition.Server } RenderedVisuals = 0; - _dirtyRect = Rect.Empty; + _dirtyRect = default; } } @@ -181,7 +179,7 @@ namespace Avalonia.Rendering.Composition.Server public void AddDirtyRect(Rect rect) { - if(rect.IsEmpty) + if(rect.IsDefault) return; var snapped = SnapToDevicePixels(rect, Scaling); DebugEvents?.RectInvalidated(rect); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index d724e14298..e33dc999dc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -37,16 +37,23 @@ namespace Avalonia.Rendering.Composition.Server return; currentTransformedClip = currentTransformedClip.Intersect(_combinedTransformedClipBounds); - if(currentTransformedClip.IsEmpty) + if(currentTransformedClip.IsDefault) return; Root!.RenderedVisuals++; + if (Opacity != 1) + canvas.PushOpacity(Opacity); + if (AdornedVisual != null) + { + canvas.PostTransform = Matrix.Identity; + canvas.Transform = Matrix.Identity; + canvas.PushClip(AdornedVisual._combinedTransformedClipBounds); + } var transform = GlobalTransformMatrix; canvas.PostTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; - if (Opacity != 1) - canvas.PushOpacity(Opacity); + var boundsRect = new Rect(new Size(Size.X, Size.Y)); if (ClipToBounds && !HandlesClipToBounds) canvas.PushClip(Root!.SnapToDevicePixels(boundsRect)); @@ -67,6 +74,8 @@ namespace Avalonia.Rendering.Composition.Server canvas.PopGeometryClip(); if (ClipToBounds && !HandlesClipToBounds) canvas.PopClip(); + if (AdornedVisual != null) + canvas.PopClip(); if(Opacity != 1) canvas.PopOpacity(); } @@ -139,7 +148,7 @@ namespace Avalonia.Rendering.Composition.Server if (ownBounds != _oldOwnContentBounds || positionChanged) { _oldOwnContentBounds = ownBounds; - if (ownBounds.IsEmpty) + if (ownBounds.IsDefault) TransformedOwnContentBounds = default; else TransformedOwnContentBounds = @@ -155,15 +164,25 @@ namespace Avalonia.Rendering.Composition.Server _clipSizeDirty = false; } + + _combinedTransformedClipBounds = + AdornedVisual?._combinedTransformedClipBounds + ?? Parent?._combinedTransformedClipBounds + ?? new Rect(Root!.Size); - _combinedTransformedClipBounds = Parent?._combinedTransformedClipBounds ?? new Rect(Root!.Size); if (_transformedClipBounds != null) _combinedTransformedClipBounds = _combinedTransformedClipBounds.Intersect(_transformedClipBounds.Value); EffectiveOpacity = Opacity * (Parent?.EffectiveOpacity ?? 1); - IsVisibleInFrame = _parent?.IsVisibleInFrame != false && Visible && EffectiveOpacity > 0.04 && !_isBackface && - !_combinedTransformedClipBounds.IsEmpty; + IsHitTestVisibleInFrame = _parent?.IsHitTestVisibleInFrame != false + && Visible + && !_isBackface + && !_combinedTransformedClipBounds.IsDefault; + + IsVisibleInFrame = IsHitTestVisibleInFrame + && _parent?.IsVisibleInFrame != false + && EffectiveOpacity > 0.04; if (wasVisible != IsVisibleInFrame || positionChanged) { @@ -187,12 +206,12 @@ namespace Avalonia.Rendering.Composition.Server readback.Revision = root.Revision; readback.Matrix = GlobalTransformMatrix; readback.TargetId = Root.Id; - readback.Visible = IsVisibleInFrame; + readback.Visible = IsHitTestVisibleInFrame; } void AddDirtyRect(Rect rc) { - if(rc == Rect.Empty) + if(rc == default) return; Root?.AddDirtyRect(rc); } @@ -216,13 +235,29 @@ namespace Avalonia.Rendering.Composition.Server partial void OnRootChanging() { if (Root != null) + { Root.RemoveVisual(this); + OnDetachedFromRoot(Root); + } + } + + protected virtual void OnDetachedFromRoot(ServerCompositionTarget target) + { + } partial void OnRootChanged() { if (Root != null) + { Root.AddVisual(this); + OnAttachedToRoot(Root); + } + } + + protected virtual void OnAttachedToRoot(ServerCompositionTarget target) + { + } protected override void ValuesInvalidated() @@ -232,6 +267,7 @@ namespace Avalonia.Rendering.Composition.Server } public bool IsVisibleInFrame { get; set; } + public bool IsHitTestVisibleInFrame { get; set; } public double EffectiveOpacity { get; set; } public Rect TransformedOwnContentBounds { get; set; } public virtual Rect OwnContentBounds => new Rect(0, 0, Size.X, Size.Y); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index 041a3dd6af..e7405995f5 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Threading; using Avalonia.Logging; using Avalonia.Platform; using Avalonia.Rendering.Composition.Animations; @@ -26,11 +27,12 @@ namespace Avalonia.Rendering.Composition.Server public Stopwatch Clock { get; } = Stopwatch.StartNew(); public TimeSpan ServerNow { get; private set; } private List _activeTargets = new(); - private HashSet _activeAnimations = new(); - private List _animationsToUpdate = new(); + private HashSet _clockItems = new(); + private List _clockItemsToUpdate = new(); internal BatchStreamObjectPool BatchObjectPool; internal BatchStreamMemoryPool BatchMemoryPool; private object _lock = new object(); + private Thread? _safeThread; public PlatformRenderInterfaceContextManager RenderInterface { get; } internal static readonly object RenderThreadJobsStartMarker = new(); internal static readonly object RenderThreadJobsEndMarker = new(); @@ -129,22 +131,31 @@ namespace Avalonia.Rendering.Composition.Server { lock (_lock) { - RenderCore(); + try + { + _safeThread = Thread.CurrentThread; + RenderCore(); + } + finally + { + _safeThread = null; + } } } private void RenderCore() { + UpdateServerTime(); ApplyPendingBatches(); CompletePendingBatches(); - foreach(var animation in _activeAnimations) - _animationsToUpdate.Add(animation); - - foreach(var animation in _animationsToUpdate) - animation.Invalidate(); + foreach(var animation in _clockItems) + _clockItemsToUpdate.Add(animation); + + foreach (var animation in _clockItemsToUpdate) + animation.OnTick(); - _animationsToUpdate.Clear(); + _clockItemsToUpdate.Clear(); try { @@ -168,16 +179,23 @@ namespace Avalonia.Rendering.Composition.Server _activeTargets.Remove(target); } - public void AddToClock(IAnimationInstance animationInstance) => - _activeAnimations.Add(animationInstance); + public void AddToClock(IServerClockItem item) => + _clockItems.Add(item); - public void RemoveFromClock(IAnimationInstance animationInstance) => - _activeAnimations.Remove(animationInstance); + public void RemoveFromClock(IServerClockItem item) => + _clockItems.Remove(item); public IRenderTarget CreateRenderTarget(IEnumerable surfaces) { using (RenderInterface.EnsureCurrent()) return RenderInterface.CreateRenderTarget(surfaces); } + + public bool CheckAccess() => _safeThread == Thread.CurrentThread; + public void VerifyAccess() + { + if (!CheckAccess()) + throw new InvalidOperationException("This object can be only accessed under compositor lock"); + } } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs new file mode 100644 index 0000000000..74889c9bfe --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs @@ -0,0 +1,82 @@ +using System; +using System.Numerics; +using Avalonia.Logging; +using Avalonia.Media; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server; + +internal class ServerCompositionCustomVisual : ServerCompositionContainerVisual, IServerClockItem +{ + private readonly CompositionCustomVisualHandler _handler; + private bool _wantsNextAnimationFrameAfterTick; + internal ServerCompositionCustomVisual(ServerCompositor compositor, CompositionCustomVisualHandler handler) : base(compositor) + { + _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + _handler.Attach(this); + } + + protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt) + { + base.DeserializeChangesCore(reader, committedAt); + var count = reader.Read(); + for (var c = 0; c < count; c++) + { + try + { + _handler.OnMessage(reader.ReadObject()!); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, LogArea.Visual) + ?.Log(_handler, $"Exception in {_handler.GetType().Name}.{nameof(CompositionCustomVisualHandler.OnMessage)} {{0}}", e); + } + } + } + + public void OnTick() + { + _wantsNextAnimationFrameAfterTick = false; + _handler.OnAnimationFrameUpdate(); + if (!_wantsNextAnimationFrameAfterTick) + Compositor.RemoveFromClock(this); + } + + public override Rect OwnContentBounds => _handler.GetRenderBounds(); + + protected override void OnAttachedToRoot(ServerCompositionTarget target) + { + if (_wantsNextAnimationFrameAfterTick) + Compositor.AddToClock(this); + base.OnAttachedToRoot(target); + } + + protected override void OnDetachedFromRoot(ServerCompositionTarget target) + { + Compositor.RemoveFromClock(this); + base.OnDetachedFromRoot(target); + } + + internal void HandlerInvalidate() => ValuesInvalidated(); + + internal void HandlerRegisterForNextAnimationFrameUpdate() + { + _wantsNextAnimationFrameAfterTick = true; + if (Root != null) + Compositor.AddToClock(this); + } + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) + { + using var context = new ImmediateDrawingContext(canvas, false); + try + { + _handler.OnRender(context); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, LogArea.Visual) + ?.Log(_handler, $"Exception in {_handler.GetType().Name}.{nameof(CompositionCustomVisualHandler.OnRender)} {{0}}", e); + } + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs index 08a3fdce22..2500358866 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs @@ -14,7 +14,7 @@ namespace Avalonia.Rendering.Composition.Server { public List List { get; } = new List(); - protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) + protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt) { if (reader.Read() == 1) { @@ -23,7 +23,7 @@ namespace Avalonia.Rendering.Composition.Server for (var c = 0; c < count; c++) List.Add(reader.ReadObject()); } - base.DeserializeChangesCore(reader, commitedAt); + base.DeserializeChangesCore(reader, committedAt); } public override long LastChangedBy diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index 257d3b29a2..31cb16ec20 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -104,13 +104,13 @@ namespace Avalonia.Rendering.Composition.Server } protected void SetAnimatedValue(CompositionProperty prop, ref T field, - TimeSpan commitedAt, IAnimationInstance animation) where T : struct + TimeSpan committedAt, IAnimationInstance animation) where T : struct { if (IsActive && _animations.TryGetValue(prop, out var oldAnimation)) oldAnimation.Deactivate(); _animations[prop] = animation; - animation.Initialize(commitedAt, ExpressionVariant.Create(field), prop); + animation.Initialize(committedAt, ExpressionVariant.Create(field), prop); if(IsActive) animation.Activate(); @@ -165,7 +165,7 @@ namespace Avalonia.Rendering.Composition.Server public virtual CompositionProperty? GetCompositionProperty(string fieldName) => null; - protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) + protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt) { if (this is IDisposable disp && reader.Read() == 1) @@ -174,7 +174,7 @@ namespace Avalonia.Rendering.Composition.Server public void DeserializeChanges(BatchStreamReader reader, Batch batch) { - DeserializeChangesCore(reader, batch.CommitedAt); + DeserializeChangesCore(reader, batch.CommittedAt); ValuesInvalidated(); ItselfLastChangedBy = batch.SequenceId; } diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs index d3e3664f84..803ea809c0 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs @@ -29,7 +29,7 @@ namespace Avalonia.Rendering.Composition.Transport public BatchStreamData Changes { get; private set; } - public TimeSpan CommitedAt { get; set; } + public TimeSpan CommittedAt { get; set; } public void Complete() { diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs index a3cad3cebd..5a88f0e91d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Server; @@ -27,6 +28,37 @@ public record struct BatchStreamSegment public int ElementCount { get; set; } } + +// Unsafe.ReadUnaligned/Unsafe.WriteUnaligned are broken on arm, +// see https://github.com/dotnet/runtime/issues/80068 +static unsafe class UnalignedMemoryHelper +{ + public static T ReadUnaligned(byte* src) where T : unmanaged + { +#if NET6_0_OR_GREATER + Unsafe.SkipInit(out var rv); +#else + T rv; +#endif + UnalignedMemcpy((byte*)&rv, src, Unsafe.SizeOf()); + return rv; + } + + public static void WriteUnaligned(byte* dst, T value) where T : unmanaged + { + UnalignedMemcpy(dst, (byte*)&value, Unsafe.SizeOf()); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static unsafe void UnalignedMemcpy(byte* dst, byte* src, int count) + { + for (var c = 0; c < count; c++) + { + dst[c] = src[c]; + } + } +} + internal class BatchStreamWriter : IDisposable { private readonly BatchStreamData _output; @@ -74,7 +106,15 @@ internal class BatchStreamWriter : IDisposable var size = Unsafe.SizeOf(); if (_currentDataSegment.Data == IntPtr.Zero || _currentDataSegment.ElementCount + size > _memoryPool.BufferSize) NextDataSegment(); - Unsafe.WriteUnaligned((byte*)_currentDataSegment.Data + _currentDataSegment.ElementCount, item); + var ptr = (byte*)_currentDataSegment.Data + _currentDataSegment.ElementCount; + + // Unsafe.ReadUnaligned/Unsafe.WriteUnaligned are broken on arm32, + // see https://github.com/dotnet/runtime/issues/80068 + if (RuntimeInformation.ProcessArchitecture == Architecture.Arm) + UnalignedMemoryHelper.WriteUnaligned(ptr, item); + else + Unsafe.WriteUnaligned(ptr, item); + _currentDataSegment.ElementCount += size; } @@ -125,7 +165,16 @@ internal class BatchStreamReader : IDisposable if (_memoryOffset + size > _currentDataSegment.ElementCount) throw new InvalidOperationException("Attempted to read more memory then left in the current segment"); - var rv = Unsafe.ReadUnaligned((byte*)_currentDataSegment.Data + _memoryOffset); + var ptr = (byte*)_currentDataSegment.Data + _memoryOffset; + T rv; + + // Unsafe.ReadUnaligned/Unsafe.WriteUnaligned are broken on arm32, + // see https://github.com/dotnet/runtime/issues/80068 + if (RuntimeInformation.ProcessArchitecture == Architecture.Arm) + rv = UnalignedMemoryHelper.ReadUnaligned(ptr); + else + rv = Unsafe.ReadUnaligned(ptr); + _memoryOffset += size; if (_memoryOffset == _currentDataSegment.ElementCount) { diff --git a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs index 6981dc39e3..93e9f4d740 100644 --- a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs +++ b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs @@ -69,7 +69,7 @@ namespace Avalonia.Rendering.Composition { if (item.Parent != null) throw new InvalidOperationException("Visual already has a parent"); - item.Parent = item; + item.Parent = _owner; } } } diff --git a/src/Avalonia.Base/Rendering/DeferredRenderer.cs b/src/Avalonia.Base/Rendering/DeferredRenderer.cs index 05aa1d1ea4..f0b993a2b0 100644 --- a/src/Avalonia.Base/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Base/Rendering/DeferredRenderer.cs @@ -49,6 +49,8 @@ namespace Avalonia.Rendering /// /// The control to render. /// The render loop. + /// The target render factory. + /// The Platform Render Context. /// The scene builder to use. Optional. /// The dispatcher to use. Optional. /// Lock object used before trying to access render target @@ -309,7 +311,7 @@ namespace Avalonia.Rendering /// Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) { - return TryGetChildScene(_currentDraw)?.Size ?? Size.Empty; + return TryGetChildScene(_currentDraw)?.Size ?? default; } /// @@ -461,7 +463,7 @@ namespace Avalonia.Rendering { clipBounds = node.ClipBounds.Intersect(clipBounds); - if (!clipBounds.IsEmpty && node.Opacity > 0) + if (!clipBounds.IsDefault && node.Opacity > 0) { var isLayerRoot = node.Visual == layer; diff --git a/src/Avalonia.Base/Rendering/DirtyRects.cs b/src/Avalonia.Base/Rendering/DirtyRects.cs index f4b6a6b6ce..723fe400b3 100644 --- a/src/Avalonia.Base/Rendering/DirtyRects.cs +++ b/src/Avalonia.Base/Rendering/DirtyRects.cs @@ -10,6 +10,9 @@ namespace Avalonia.Rendering { private List _rects = new List(); + /// + /// Gets a value indicating whether the collection of dirty rectangles is empty. + /// public bool IsEmpty => _rects.Count == 0; /// @@ -27,7 +30,7 @@ namespace Avalonia.Rendering /// public void Add(Rect rect) { - if (!rect.IsEmpty) + if (!rect.IsDefault) { for (var i = 0; i < _rects.Count; ++i) { diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index 1c797a5348..60b144e806 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -30,6 +30,8 @@ namespace Avalonia.Rendering /// Initializes a new instance of the class. /// /// The control to render. + /// The target render factory. + /// The render contex. public ImmediateRenderer(Visual root, Func renderTargetFactory, PlatformRenderInterfaceContextManager? renderContext = null) { @@ -136,7 +138,7 @@ namespace Avalonia.Rendering /// public void AddDirty(Visual visual) { - if (visual.Bounds != Rect.Empty) + if (!visual.Bounds.IsDefault) { var m = visual.TransformToVisual(_root); @@ -201,7 +203,7 @@ namespace Avalonia.Rendering Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) { (brush.Visual as IVisualBrushInitialize)?.EnsureInitialized(); - return brush.Visual?.Bounds.Size ?? Size.Empty; + return brush.Visual?.Bounds.Size ?? default; } /// @@ -328,11 +330,11 @@ namespace Avalonia.Rendering ? visual is IVisualWithRoundRectClip roundClipVisual ? context.PushClip(new RoundedRect(bounds, roundClipVisual.ClipToBoundsRadius)) : context.PushClip(bounds) - : default(DrawingContext.PushedState)) + : default) #pragma warning restore CS0618 // Type or member is obsolete - using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default(DrawingContext.PushedState)) - using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default(DrawingContext.PushedState)) + using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default) + using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default) using (context.PushTransformContainer()) { visual.Render(context); diff --git a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs index 198b36564a..261a39fa09 100644 --- a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs +++ b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; using Avalonia.Metadata; using Avalonia.Platform; +using Avalonia.Reactive; namespace Avalonia.Rendering; diff --git a/src/Avalonia.Base/Rendering/RenderLoop.cs b/src/Avalonia.Base/Rendering/RenderLoop.cs index 5a08bfc6a1..1f58ca3827 100644 --- a/src/Avalonia.Base/Rendering/RenderLoop.cs +++ b/src/Avalonia.Base/Rendering/RenderLoop.cs @@ -50,8 +50,7 @@ namespace Avalonia.Rendering { get { - return _timer ??= AvaloniaLocator.Current.GetService() ?? - throw new InvalidOperationException("Cannot locate IRenderTimer."); + return _timer ??= AvaloniaLocator.Current.GetRequiredService(); } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs index 98e89f6549..b1190a159b 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs @@ -27,7 +27,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public Rect Bounds => Rect.Empty; + public Rect Bounds => default; /// /// Gets the BitmapBlend to be pushed or null if the operation represents a pop. diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs index 90430bed02..e1bfaa4aa3 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs @@ -40,7 +40,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public Rect Bounds => Rect.Empty; + public Rect Bounds => default; /// /// Gets the clip to be pushed or null if the operation represents a pop. diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs index 667b66420b..842edf2bcb 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs @@ -28,7 +28,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public Rect Bounds => Rect.Empty; + public Rect Bounds => default; /// /// Gets the clip to be pushed or null if the operation represents a pop. diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs index 5fd200ddff..3ecc07fa54 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// The bounds of the mask. /// Auxiliary data required to draw the brush. public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null) - : base(Rect.Empty, Matrix.Identity, aux) + : base(default, Matrix.Identity, aux) { Mask = mask.ToImmutable(); MaskBounds = bounds; @@ -30,7 +30,7 @@ namespace Avalonia.Rendering.SceneGraph /// opacity mask pop. /// public OpacityMaskNode() - : base(Rect.Empty, Matrix.Identity, null) + : base(default, Matrix.Identity, null) { } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs index 8fc630588f..e41e639067 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs @@ -26,7 +26,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public Rect Bounds => Rect.Empty; + public Rect Bounds => default; /// /// Gets the opacity to be pushed or null if the operation represents a pop. diff --git a/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs index d54bd3fad8..55ff772772 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs @@ -331,8 +331,8 @@ namespace Avalonia.Rendering.SceneGraph scene.Size = newSize; - Rect horizontalDirtyRect = Rect.Empty; - Rect verticalDirtyRect = Rect.Empty; + Rect horizontalDirtyRect = default; + Rect verticalDirtyRect = default; if (newSize.Width > oldSize.Width) { @@ -429,7 +429,7 @@ namespace Avalonia.Rendering.SceneGraph else { layer.OpacityMask = null; - layer.OpacityMaskRect = Rect.Empty; + layer.OpacityMaskRect = default; } layer.GeometryClip = node.HasAncestorGeometryClip ? diff --git a/src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs index a991f2f657..b9491e6cbd 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Utilities; @@ -303,7 +303,7 @@ namespace Avalonia.Rendering.SceneGraph if (ClipToBounds) { context.Transform = Matrix.Identity; - if (ClipToBoundsRadius.IsEmpty) + if (ClipToBoundsRadius.IsDefault) context.PushClip(ClipBounds); else context.PushClip(new RoundedRect(ClipBounds, ClipToBoundsRadius)); diff --git a/src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs b/src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs index dd6cf7ad15..1bbf804b5f 100644 --- a/src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs @@ -1,6 +1,6 @@ using System; using System.Diagnostics; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Threading; namespace Avalonia.Rendering diff --git a/src/Avalonia.Base/Rendering/Utilities/TileBrushCalculator.cs b/src/Avalonia.Base/Rendering/Utilities/TileBrushCalculator.cs index af2c7f71dc..6b34b44337 100644 --- a/src/Avalonia.Base/Rendering/Utilities/TileBrushCalculator.cs +++ b/src/Avalonia.Base/Rendering/Utilities/TileBrushCalculator.cs @@ -109,7 +109,7 @@ namespace Avalonia.Rendering.Utilities { if (IntermediateTransform != Matrix.Identity) return true; - if (SourceRect.Position != default(Point)) + if (SourceRect.Position != default) return true; if (SourceRect.Size.AspectRatio == _imageSize.AspectRatio) return false; diff --git a/src/Avalonia.Base/Size.cs b/src/Avalonia.Base/Size.cs index 5f20206200..aec237afae 100644 --- a/src/Avalonia.Base/Size.cs +++ b/src/Avalonia.Base/Size.cs @@ -28,8 +28,9 @@ namespace Avalonia public static readonly Size Infinity = new Size(double.PositiveInfinity, double.PositiveInfinity); /// - /// A size representing zero + /// A size representing zero. /// + [Obsolete("Use the default keyword instead.")] public static readonly Size Empty = new Size(0, 0); /// @@ -309,9 +310,6 @@ namespace Avalonia /// /// Gets a value indicating whether the Width and Height values are zero. /// - public bool IsDefault - { - get { return (_width == 0) && (_height == 0); } - } + public bool IsDefault => (_width == 0) && (_height == 0); } } diff --git a/src/Avalonia.Base/Styling/StyleInstance.cs b/src/Avalonia.Base/Styling/StyleInstance.cs index ca602167c0..61cb31c6d0 100644 --- a/src/Avalonia.Base/Styling/StyleInstance.cs +++ b/src/Avalonia.Base/Styling/StyleInstance.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using System.Reactive.Subjects; using Avalonia.Animation; using Avalonia.Data; using Avalonia.PropertyStore; +using Avalonia.Reactive; using Avalonia.Styling.Activators; namespace Avalonia.Styling @@ -24,7 +24,7 @@ namespace Avalonia.Styling private bool _isActive; private List? _setters; private List? _animations; - private Subject? _animationTrigger; + private LightweightSubject? _animationTrigger; public StyleInstance( IStyle style, @@ -67,7 +67,7 @@ namespace Avalonia.Styling { if (_animations is not null && control is Animatable animatable) { - _animationTrigger ??= new Subject(); + _animationTrigger ??= new LightweightSubject(); foreach (var animation in _animations) animation.Apply(animatable, null, _animationTrigger); diff --git a/src/Avalonia.Base/Thickness.cs b/src/Avalonia.Base/Thickness.cs index c8e6d7dfd2..f9e4355edd 100644 --- a/src/Avalonia.Base/Thickness.cs +++ b/src/Avalonia.Base/Thickness.cs @@ -97,10 +97,9 @@ namespace Avalonia /// public double Bottom => _bottom; - /// - /// Gets a value indicating whether all sides are set to 0. - /// - public bool IsEmpty => Left.Equals(0) && IsUniform; + /// + [Obsolete("Use IsDefault instead.")] + public bool IsEmpty => IsDefault; /// /// Gets a value indicating whether all sides are equal. @@ -292,15 +291,13 @@ namespace Avalonia left = this._left; top = this._top; right = this._right; - bottom = this._bottom; + bottom = this._bottom; } /// - /// Gets a value indicating whether the left, top, right and bottom thickness values are zero. + /// Gets a value indicating whether the instance has default values + /// (the left, top, right and bottom values are zero). /// - public bool IsDefault - { - get { return (_left == 0) && (_top == 0) && (_right == 0) && (_bottom == 0); } - } + public bool IsDefault => (_left == 0) && (_top == 0) && (_right == 0) && (_bottom == 0); } } diff --git a/src/Avalonia.Base/Threading/DispatcherTimer.cs b/src/Avalonia.Base/Threading/DispatcherTimer.cs index 0c25d89722..f81229eb48 100644 --- a/src/Avalonia.Base/Threading/DispatcherTimer.cs +++ b/src/Avalonia.Base/Threading/DispatcherTimer.cs @@ -1,5 +1,5 @@ using System; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Platform; namespace Avalonia.Threading @@ -176,13 +176,7 @@ namespace Avalonia.Threading { if (!IsEnabled) { - var threading = AvaloniaLocator.Current.GetService(); - - if (threading == null) - { - throw new Exception("Could not start timer: IPlatformThreadingInterface is not registered."); - } - + var threading = AvaloniaLocator.Current.GetRequiredService(); _timer = threading.StartTimer(_priority, Interval, InternalTick); } } diff --git a/src/Avalonia.Base/Utilities/ArrayBuilder.cs b/src/Avalonia.Base/Utilities/ArrayBuilder.cs index 60bf2c7586..e6b67bd383 100644 --- a/src/Avalonia.Base/Utilities/ArrayBuilder.cs +++ b/src/Avalonia.Base/Utilities/ArrayBuilder.cs @@ -3,6 +3,7 @@ // Ported from: https://github.com/SixLabors/Fonts/ using System; +using System.Buffers; using System.Runtime.CompilerServices; namespace Avalonia.Utilities @@ -11,7 +12,7 @@ namespace Avalonia.Utilities /// A helper type for avoiding allocations while building arrays. /// /// The type of item contained in the array. - internal struct ArrayBuilder + internal struct ArrayBuilder : IDisposable where T : struct { private const int DefaultCapacity = 4; @@ -135,7 +136,7 @@ namespace Avalonia.Utilities } // Same expansion algorithm as List. - var newCapacity = length == 0 ? DefaultCapacity : (uint)length * 2u; + var newCapacity = length == 0 ? DefaultCapacity : length * 2; if (newCapacity > MaxCoreClrArrayLength) { @@ -144,14 +145,15 @@ namespace Avalonia.Utilities if (newCapacity < min) { - newCapacity = (uint)min; + newCapacity = min; } - - var array = new T[newCapacity]; + + var array = ArrayPool.Shared.Rent(newCapacity); if (_size > 0) { Array.Copy(_data!, array, _size); + ArrayPool.Shared.Return(_data!); } _data = array; @@ -180,5 +182,13 @@ namespace Avalonia.Utilities /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public ArraySlice AsSlice(int start, int length) => new ArraySlice(_data!, start, length); + + public void Dispose() + { + if (_data != null) + { + ArrayPool.Shared.Return(_data); + } + } } } diff --git a/src/Avalonia.Base/Utilities/ArraySlice.cs b/src/Avalonia.Base/Utilities/ArraySlice.cs index 39c0cd5556..b70088a907 100644 --- a/src/Avalonia.Base/Utilities/ArraySlice.cs +++ b/src/Avalonia.Base/Utilities/ArraySlice.cs @@ -3,6 +3,7 @@ // Ported from: https://github.com/SixLabors/Fonts/ using System; +using System.Buffers; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -185,5 +186,13 @@ namespace Avalonia.Utilities /// int IReadOnlyCollection.Count => Length; + + public void ReturnRent() + { + if (_data != null) + { + ArrayPool.Shared.Return(_data); + } + } } } diff --git a/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs b/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs index 5235e23e08..3c7e82f080 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs @@ -1,105 +1,116 @@ using System; using System.Collections.Generic; using System.IO; -using System.Runtime.Serialization; -using System.Xml.Linq; -using System.Linq; -using System.Diagnostics.CodeAnalysis; +using System.Text; namespace Avalonia.Utilities { - #if !BUILDTASK +#if !BUILDTASK public - #endif +#endif static class AvaloniaResourcesIndexReaderWriter { - private const int LastKnownVersion = 1; - public static List Read(Stream stream) + private const int XmlLegacyVersion = 1; + private const int BinaryCurrentVersion = 2; + + public static List ReadIndex(Stream stream) { - var ver = new BinaryReader(stream).ReadInt32(); - if (ver > LastKnownVersion) - throw new Exception("Resources index format version is not known"); - - var assetDoc = XDocument.Load(stream); - XNamespace assetNs = assetDoc.Root!.Attribute("xmlns")!.Value; - List entries = - (from entry in assetDoc.Root.Element(assetNs + "Entries")!.Elements(assetNs + "AvaloniaResourcesIndexEntry") - select new AvaloniaResourcesIndexEntry - { - Path = entry.Element(assetNs + "Path")!.Value, - Offset = int.Parse(entry.Element(assetNs + "Offset")!.Value), - Size = int.Parse(entry.Element(assetNs + "Size")!.Value) - }).ToList(); + using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true); - return entries; + var version = reader.ReadInt32(); + return version switch + { + XmlLegacyVersion => ReadXmlIndex(), + BinaryCurrentVersion => ReadBinaryIndex(reader), + _ => throw new Exception($"Unknown resources index format version {version}") + }; } - [RequiresUnreferencedCode("AvaloniaResources uses Data Contract Serialization, which might require unreferenced code")] - public static void Write(Stream stream, List entries) + private static List ReadXmlIndex() + => throw new NotSupportedException("Found legacy resources index format: please recompile your XAML files"); + + private static List ReadBinaryIndex(BinaryReader reader) { - new BinaryWriter(stream).Write(LastKnownVersion); - new DataContractSerializer(typeof(AvaloniaResourcesIndex)).WriteObject(stream, - new AvaloniaResourcesIndex() - { - Entries = entries + var entryCount = reader.ReadInt32(); + var entries = new List(entryCount); + + for (var i = 0; i < entryCount; ++i) + { + entries.Add(new AvaloniaResourcesIndexEntry { + Path = reader.ReadString(), + Offset = reader.ReadInt32(), + Size = reader.ReadInt32() }); + } + + return entries; } - [RequiresUnreferencedCode("AvaloniaResources uses Data Contract Serialization, which might require unreferenced code")] - public static byte[] Create(Dictionary data) + public static void WriteIndex(Stream output, List entries) { - var sources = data.ToList(); - var offsets = new Dictionary(); - var coffset = 0; - foreach (var s in sources) + using var writer = new BinaryWriter(output, Encoding.UTF8, leaveOpen: true); + + WriteIndex(writer, entries); + } + + private static void WriteIndex(BinaryWriter writer, List entries) + { + writer.Write(BinaryCurrentVersion); + writer.Write(entries.Count); + + foreach (var entry in entries) { - offsets[s.Key] = coffset; - coffset += s.Value.Length; + writer.Write(entry.Path ?? string.Empty); + writer.Write(entry.Offset); + writer.Write(entry.Size); } - var index = sources.Select(s => new AvaloniaResourcesIndexEntry - { - Path = s.Key, - Size = s.Value.Length, - Offset = offsets[s.Key] - }).ToList(); - var output = new MemoryStream(); - var ms = new MemoryStream(); - AvaloniaResourcesIndexReaderWriter.Write(ms, index); - new BinaryWriter(output).Write((int)ms.Length); - ms.Position = 0; - ms.CopyTo(output); - foreach (var s in sources) + } + + public static void WriteResources(Stream output, List<(string Path, int Size, Func Open)> resources) + { + var entries = new List(resources.Count); + var offset = 0; + + foreach (var resource in resources) { - output.Write(s.Value,0,s.Value.Length); + entries.Add(new AvaloniaResourcesIndexEntry + { + Path = resource.Path, + Offset = offset, + Size = resource.Size + }); + offset += resource.Size; } - return output.ToArray(); - } - } + using var writer = new BinaryWriter(output, Encoding.UTF8, leaveOpen: true); + writer.Write(0); // index size placeholder, overwritten below - [DataContract] -#if !BUILDTASK - public -#endif - class AvaloniaResourcesIndex - { - [DataMember] - public List Entries { get; set; } = new List(); + var posBeforeEntries = output.Position; + WriteIndex(writer, entries); + + var posAfterEntries = output.Position; + var indexSize = (int) (posAfterEntries - posBeforeEntries); + output.Position = 0L; + writer.Write(indexSize); + output.Position = posAfterEntries; + + foreach (var resource in resources) + { + using var resourceStream = resource.Open(); + resourceStream.CopyTo(output); + } + } } - [DataContract] #if !BUILDTASK public #endif class AvaloniaResourcesIndexEntry { - [DataMember] public string? Path { get; set; } - - [DataMember] + public int Offset { get; set; } - - [DataMember] + public int Size { get; set; } } } diff --git a/src/Avalonia.Base/Utilities/IWeakSubscriber.cs b/src/Avalonia.Base/Utilities/IWeakSubscriber.cs deleted file mode 100644 index 2a5b8d39c5..0000000000 --- a/src/Avalonia.Base/Utilities/IWeakSubscriber.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace Avalonia.Utilities -{ - /// - /// Defines a listener to a event subscribed vis the . - /// - /// The type of the event arguments. - public interface IWeakSubscriber where T : EventArgs - { - /// - /// Invoked when the subscribed event is raised. - /// - /// The event sender. - /// The event arguments. - void OnEvent(object? sender, T e); - } -} diff --git a/src/Avalonia.Base/Utilities/SingleOrDictionary.cs b/src/Avalonia.Base/Utilities/SingleOrDictionary.cs index 00cc5864f5..0eb7ae2e31 100644 --- a/src/Avalonia.Base/Utilities/SingleOrDictionary.cs +++ b/src/Avalonia.Base/Utilities/SingleOrDictionary.cs @@ -42,7 +42,7 @@ namespace Avalonia.Utilities { if (!_singleValue.HasValue || !EqualityComparer.Default.Equals(_singleValue.Value.Key, key)) { - value = default(TValue); + value = default; return false; } else diff --git a/src/Avalonia.Base/Utilities/StringTokenizer.cs b/src/Avalonia.Base/Utilities/StringTokenizer.cs index 726c9735ef..aad742e02b 100644 --- a/src/Avalonia.Base/Utilities/StringTokenizer.cs +++ b/src/Avalonia.Base/Utilities/StringTokenizer.cs @@ -63,7 +63,7 @@ namespace Avalonia.Utilities } else { - result = default(Int32); + result = default; return false; } } @@ -87,7 +87,7 @@ namespace Avalonia.Utilities } else { - result = default(double); + result = default; return false; } } diff --git a/src/Avalonia.Base/Utilities/WeakObservable.cs b/src/Avalonia.Base/Utilities/WeakObservable.cs deleted file mode 100644 index e1c350d539..0000000000 --- a/src/Avalonia.Base/Utilities/WeakObservable.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Reactive; -using System.Reactive.Linq; - -namespace Avalonia.Utilities -{ - /// - /// Provides extension methods for working with weak event handlers. - /// - public static class WeakObservable - { - - private class Handler - : IWeakSubscriber, - IWeakEventSubscriber where TEventArgs : EventArgs - { - private IObserver> _observer; - - public Handler(IObserver> observer) - { - _observer = observer; - } - - public void OnEvent(object? sender, TEventArgs e) - { - _observer.OnNext(new EventPattern(sender, e)); - } - - public void OnEvent(object? sender, WeakEvent ev, TEventArgs e) - { - _observer.OnNext(new EventPattern(sender, e)); - } - } - - /// - /// Converts a WeakEvent conforming to the standard .NET event pattern into an observable - /// sequence, subscribing weakly. - /// - /// The type of target. - /// The type of the event args. - /// Object instance that exposes the event to convert. - /// The weak event to convert. - /// - public static IObservable> FromEventPattern( - TTarget target, WeakEvent ev) - where TEventArgs : EventArgs where TTarget : class - { - _ = target ?? throw new ArgumentNullException(nameof(target)); - _ = ev ?? throw new ArgumentNullException(nameof(ev)); - - return Observable.Create>(observer => - { - var handler = new Handler(observer); - ev.Subscribe(target, handler); - return () => ev.Unsubscribe(target, handler); - }).Publish().RefCount(); - } - - } -} diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index e3dc4fbb75..7fcb53bcea 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -11,6 +11,7 @@ using Avalonia.Logging; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; +using Avalonia.Reactive; using Avalonia.Rendering; using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition.Server; @@ -367,7 +368,10 @@ namespace Avalonia /// The drawing context. public virtual void Render(DrawingContext context) { - Contract.Requires(context != null); + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } } /// @@ -384,52 +388,55 @@ namespace Avalonia protected static void AffectsRender(params AvaloniaProperty[] properties) where T : Visual { - static void Invalidate(AvaloniaPropertyChangedEventArgs e) - { - if (e.Sender is T sender) + var invalidateObserver = new AnonymousObserver( + static e => { - sender.InvalidateVisual(); - } - } - - static void InvalidateAndSubscribe(AvaloniaPropertyChangedEventArgs e) - { - if (e.Sender is T sender) + if (e.Sender is T sender) + { + sender.InvalidateVisual(); + } + }); + + + var invalidateAndSubscribeObserver = new AnonymousObserver( + static e => { - if (e.OldValue is IAffectsRender oldValue) + if (e.Sender is T sender) { - if (sender._affectsRenderWeakSubscriber != null) + if (e.OldValue is IAffectsRender oldValue) { - InvalidatedWeakEvent.Unsubscribe(oldValue, sender._affectsRenderWeakSubscriber); + if (sender._affectsRenderWeakSubscriber != null) + { + InvalidatedWeakEvent.Unsubscribe(oldValue, sender._affectsRenderWeakSubscriber); + } } - } - if (e.NewValue is IAffectsRender newValue) - { - if (sender._affectsRenderWeakSubscriber == null) + if (e.NewValue is IAffectsRender newValue) { - sender._affectsRenderWeakSubscriber = new TargetWeakEventSubscriber( - sender, static (target, _, _, _) => - { - target.InvalidateVisual(); - }); + if (sender._affectsRenderWeakSubscriber == null) + { + sender._affectsRenderWeakSubscriber = new TargetWeakEventSubscriber( + sender, static (target, _, _, _) => + { + target.InvalidateVisual(); + }); + } + InvalidatedWeakEvent.Subscribe(newValue, sender._affectsRenderWeakSubscriber); } - InvalidatedWeakEvent.Subscribe(newValue, sender._affectsRenderWeakSubscriber); - } - sender.InvalidateVisual(); - } - } + sender.InvalidateVisual(); + } + }); foreach (var property in properties) { if (property.CanValueAffectRender()) { - property.Changed.Subscribe(e => InvalidateAndSubscribe(e)); + property.Changed.Subscribe(invalidateAndSubscribeObserver); } else { - property.Changed.Subscribe(e => Invalidate(e)); + property.Changed.Subscribe(invalidateObserver); } } } @@ -477,6 +484,7 @@ namespace Avalonia { AttachToCompositor(compositingRenderer.Compositor); } + InvalidateMirrorTransform(); OnAttachedToVisualTree(e); AttachedToVisualTree?.Invoke(this, e); InvalidateVisual(); @@ -540,6 +548,9 @@ namespace Avalonia OnDetachedFromVisualTree(e); if (CompositionVisual != null) { + if (ChildCompositionVisual != null) + CompositionVisual.Children.Remove(ChildCompositionVisual); + CompositionVisual.DrawList = null; CompositionVisual = null; } @@ -613,23 +624,22 @@ namespace Avalonia /// Called when a visual's changes. /// /// The event args. - private static void RenderTransformChanged(AvaloniaPropertyChangedEventArgs e) + private static void RenderTransformChanged(AvaloniaPropertyChangedEventArgs e) { var sender = e.Sender as Visual; if (sender?.VisualRoot != null) { - var oldValue = e.OldValue as Transform; - var newValue = e.NewValue as Transform; + var (oldValue, newValue) = e.GetOldAndNewValue(); - if (oldValue != null) + if (oldValue is Transform oldTransform) { - oldValue.Changed -= sender.RenderTransformChanged; + oldTransform.Changed -= sender.RenderTransformChanged; } - if (newValue != null) + if (newValue is Transform newTransform) { - newValue.Changed += sender.RenderTransformChanged; + newTransform.Changed += sender.RenderTransformChanged; } sender.InvalidateVisual(); diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index 35b22f4f23..e44b7290af 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -18,6 +18,9 @@ Shared/AvaloniaResourcesIndex.cs + + Shared/Constants.cs + Shared/AvaloniaResourceXamlInfo.cs diff --git a/src/Avalonia.Build.Tasks/ComInteropHelper.cs b/src/Avalonia.Build.Tasks/ComInteropHelper.cs index 007231417d..c990741e1e 100644 --- a/src/Avalonia.Build.Tasks/ComInteropHelper.cs +++ b/src/Avalonia.Build.Tasks/ComInteropHelper.cs @@ -65,10 +65,8 @@ namespace Avalonia.Build.Tasks { Instruction instruction = instructions[i]; - if (instruction.OpCode == OpCodes.Call && instruction.Operand is MethodReference) + if (instruction.OpCode == OpCodes.Call && instruction.Operand is MethodReference methodDescription) { - var methodDescription = (MethodReference)instruction.Operand; - if (methodDescription.Name.StartsWith("Calli") && methodDescription.DeclaringType.Name == "LocalInterop") { var callSite = new CallSite(methodDescription.ReturnType) { CallingConvention = MethodCallingConvention.StdCall }; diff --git a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs index fd07e0a143..3bd1ce5ce7 100644 --- a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs +++ b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.Build.Framework; @@ -10,9 +11,10 @@ namespace Avalonia.Build.Tasks public bool Execute() { Enum.TryParse(ReportImportance, true, out MessageImportance outputImportance); + var writtenFilePaths = new List(); - OutputPath = OutputPath ?? AssemblyFile; - RefOutputPath = RefOutputPath ?? RefAssemblyFile; + OutputPath ??= AssemblyFile; + RefOutputPath ??= RefAssemblyFile; var outputPdb = GetPdbPath(OutputPath); var input = AssemblyFile; var refInput = RefOutputPath; @@ -21,8 +23,9 @@ namespace Avalonia.Build.Tasks if (OriginalCopyPath != null) { var originalCopyPathRef = Path.ChangeExtension(OriginalCopyPath, ".ref.dll"); - + File.Copy(AssemblyFile, OriginalCopyPath, true); + writtenFilePaths.Add(OriginalCopyPath); input = OriginalCopyPath; File.Delete(AssemblyFile); @@ -30,6 +33,7 @@ namespace Avalonia.Build.Tasks { var copyPdb = GetPdbPath(OriginalCopyPath); File.Copy(inputPdb, copyPdb, true); + writtenFilePaths.Add(copyPdb); File.Delete(inputPdb); inputPdb = copyPdb; } @@ -39,6 +43,7 @@ namespace Avalonia.Build.Tasks // We also copy ref assembly just for case if needed later for testing. // But do not remove the original one, as MSBuild actually complains about it with multi-thread compiling. File.Copy(RefAssemblyFile, originalCopyPathRef, true); + writtenFilePaths.Add(originalCopyPathRef); refInput = originalCopyPathRef; } } @@ -53,13 +58,25 @@ namespace Avalonia.Build.Tasks ProjectDirectory, VerifyIl, DefaultCompileBindings, outputImportance, (SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null, SkipXamlCompilation, DebuggerLaunch); if (!res.Success) + { + WrittenFilePaths = writtenFilePaths.ToArray(); return false; + } + if (!res.WrittenFile) { File.Copy(input, OutputPath, true); - if(File.Exists(inputPdb)) + if (File.Exists(inputPdb)) File.Copy(inputPdb, outputPdb, true); } + else if (!string.IsNullOrWhiteSpace(RefOutputPath) && File.Exists(RefOutputPath)) + writtenFilePaths.Add(RefOutputPath); + + writtenFilePaths.Add(OutputPath); + if (File.Exists(outputPdb)) + writtenFilePaths.Add(outputPdb); + + WrittenFilePaths = writtenFilePaths.ToArray(); return true; } @@ -103,5 +120,8 @@ namespace Avalonia.Build.Tasks public ITaskHost HostObject { get; set; } public bool DebuggerLaunch { get; set; } + + [Output] + public string[] WrittenFilePaths { get; private set; } = Array.Empty(); } } diff --git a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs index fad6ad397b..264a86b014 100644 --- a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs +++ b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs @@ -7,7 +7,7 @@ using System.Text; using Avalonia.Markup.Xaml.PortableXaml; using Avalonia.Utilities; using Microsoft.Build.Framework; -using SPath=System.IO.Path; +using SPath = System.IO.Path; namespace Avalonia.Build.Tasks { public class GenerateAvaloniaResourcesTask : ITask @@ -30,12 +30,17 @@ namespace Avalonia.Build.Tasks private byte[] _data; private string _sourcePath; - public Source(string relativePath, string root) + public Source(ITaskItem avaloniaResourceItem, string root) { root = SPath.GetFullPath(root); - Path = "/" + relativePath.Replace('\\', '/'); + var relativePath = avaloniaResourceItem.ItemSpec; _sourcePath = SPath.Combine(root, relativePath); Size = (int)new FileInfo(_sourcePath).Length; + var link = avaloniaResourceItem.GetMetadata("Link"); + var path = !string.IsNullOrEmpty(link) + ? link + : relativePath; + Path = "/" + path.Replace('\\', '/'); } public string SystemPath => _sourcePath ?? Path; @@ -65,43 +70,22 @@ namespace Avalonia.Build.Tasks List BuildResourceSources() => Resources.Select(r => { - - var src = new Source(r.ItemSpec, Root); + var src = new Source(r, Root); BuildEngine.LogMessage(FormattableString.Invariant($"avares -> name:{src.Path}, path: {src.SystemPath}, size:{src.Size}, ItemSpec:{r.ItemSpec}"), _reportImportance); return src; }).ToList(); private void Pack(Stream output, List sources) { - var offsets = new Dictionary(); - var coffset = 0; - foreach (var s in sources) - { - offsets[s] = coffset; - coffset += s.Size; - } - var index = sources.Select(s => new AvaloniaResourcesIndexEntry - { - Path = s.Path, - Size = s.Size, - Offset = offsets[s] - }).ToList(); - var ms = new MemoryStream(); - AvaloniaResourcesIndexReaderWriter.Write(ms, index); - new BinaryWriter(output).Write((int)ms.Length); - ms.Position = 0; - ms.CopyTo(output); - foreach (var s in sources) - { - using(var input = s.Open()) - input.CopyTo(output); - } + AvaloniaResourcesIndexReaderWriter.WriteResources( + output, + sources.Select(source => (source.Path, source.Size, (Func) source.Open)).ToList()); } private bool PreProcessXamlFiles(List sources) { - var typeToXamlIndex = new Dictionary(); - + var typeToXamlIndex = new Dictionary(); + foreach (var s in sources.ToArray()) { if (s.Path.ToLowerInvariant().EndsWith(".xaml") || s.Path.ToLowerInvariant().EndsWith(".paml") || s.Path.ToLowerInvariant().EndsWith(".axaml")) @@ -111,7 +95,7 @@ namespace Avalonia.Build.Tasks { info = XamlFileInfo.Parse(s.ReadAsString()); } - catch(Exception e) + catch (Exception e) { BuildEngine.LogError(BuildEngineErrorCode.InvalidXAML, s.SystemPath, "File doesn't contain valid XAML: " + e); return false; @@ -121,7 +105,7 @@ namespace Avalonia.Build.Tasks { if (typeToXamlIndex.ContainsKey(info.XClass)) { - + BuildEngine.LogError(BuildEngineErrorCode.DuplicateXClass, s.SystemPath, $"Duplicate x:Class directive, {info.XClass} is already used in {typeToXamlIndex[info.XClass]}"); return false; diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs index f83e07bd74..079ea91db1 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs @@ -1,6 +1,8 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Avalonia.Platform.Internal; using Avalonia.Utilities; using Mono.Cecil; using Mono.Cecil.Cil; @@ -34,13 +36,13 @@ namespace Avalonia.Build.Tasks { _asm = asm; _embedded = ((EmbeddedResource)asm.MainModule.Resources.FirstOrDefault(r => - r.ResourceType == ResourceType.Embedded && r.Name == "!AvaloniaResources")); + r.ResourceType == ResourceType.Embedded && r.Name == Constants.AvaloniaResourceName)); if (_embedded == null) return; using (var stream = _embedded.GetResourceStream()) { var br = new BinaryReader(stream); - var index = AvaloniaResourcesIndexReaderWriter.Read(new MemoryStream(br.ReadBytes(br.ReadInt32()))); + var index = AvaloniaResourcesIndexReaderWriter.ReadIndex(new MemoryStream(br.ReadBytes(br.ReadInt32()))); var baseOffset = stream.Position; foreach (var e in index) { @@ -61,9 +63,18 @@ namespace Avalonia.Build.Tasks if (_resources.Count == 0) return; - _embedded = new EmbeddedResource("!AvaloniaResources", ManifestResourceAttributes.Public, - AvaloniaResourcesIndexReaderWriter.Create(_resources.ToDictionary(x => x.Key, - x => x.Value.FileContents))); + var output = new MemoryStream(); + + AvaloniaResourcesIndexReaderWriter.WriteResources( + output, + _resources.Select(x => ( + Path: x.Key, + Size: x.Value.FileContents.Length, + Open: (Func) (() => new MemoryStream(x.Value.FileContents)) + )).ToList()); + + output.Position = 0L; + _embedded = new EmbeddedResource(Constants.AvaloniaResourceName, ManifestResourceAttributes.Public, output); _asm.MainModule.Resources.Add(_embedded); } diff --git a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj index f3f8e4c82c..5a31053bdc 100644 --- a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj +++ b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj @@ -15,9 +15,7 @@ - - diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs index 2cc5b99b2e..aaf272c6d2 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs @@ -289,8 +289,8 @@ namespace Avalonia.Controls }; // See: https://htmlcolorcodes.com/assets/downloads/flat-design-colors/flat-design-color-chart.png - protected static Color[,]? _colorChart = null; - protected static object _colorChartMutex = new object(); + private static Color[,]? _colorChart = null; + private static readonly object _colorChartMutex = new(); /// /// Initializes all color chart colors. diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatHalfColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatHalfColorPalette.cs index 2758124fae..4a30fb6cb4 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatHalfColorPalette.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatHalfColorPalette.cs @@ -10,8 +10,8 @@ namespace Avalonia.Controls /// public class FlatHalfColorPalette : IColorPalette { - protected static Color[,]? _colorChart = null; - protected static object _colorChartMutex = new object(); + private static Color[,]? _colorChart = null; + private static readonly object _colorChartMutex = new(); /// /// Initializes all color chart colors. diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs index d009926bc5..d4b904163c 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs @@ -344,8 +344,8 @@ namespace Avalonia.Controls // See: https://material.io/design/color/the-color-system.html#tools-for-picking-colors // This is a reduced palette for uniformity - protected static Color[,]? _colorChart = null; - protected static object _colorChartMutex = new object(); + private static Color[,]? _colorChart = null; + private static readonly object _colorChartMutex = new(); /// /// Initializes all color chart colors. diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialHalfColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialHalfColorPalette.cs index 01d44aa65d..4432642675 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialHalfColorPalette.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialHalfColorPalette.cs @@ -10,8 +10,8 @@ namespace Avalonia.Controls /// public class MaterialHalfColorPalette : IColorPalette { - protected static Color[,]? _colorChart = null; - protected static object _colorChartMutex = new object(); + private static Color[,]? _colorChart = null; + private static readonly object _colorChartMutex = new(); /// /// Initializes all color chart colors. diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 2df46889a9..6f4c0003a8 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -15,6 +15,7 @@ using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Threading; using Avalonia.Utilities; +using Avalonia.Reactive; namespace Avalonia.Controls.Primitives { diff --git a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj index 6369961f0f..6556ce721e 100644 --- a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj +++ b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj @@ -12,9 +12,7 @@ - - diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 454678c64b..f35124ee0a 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -27,6 +27,7 @@ using Avalonia.Layout; using Avalonia.Controls.Metadata; using Avalonia.Input.GestureRecognizers; using Avalonia.Styling; +using Avalonia.Reactive; namespace Avalonia.Controls { @@ -48,19 +49,13 @@ namespace Avalonia.Controls private const string DATAGRID_elementColumnHeadersPresenterName = "PART_ColumnHeadersPresenter"; private const string DATAGRID_elementFrozenColumnScrollBarSpacerName = "PART_FrozenColumnScrollBarSpacer"; private const string DATAGRID_elementHorizontalScrollbarName = "PART_HorizontalScrollbar"; - private const string DATAGRID_elementRowHeadersPresenterName = "PART_RowHeadersPresenter"; private const string DATAGRID_elementTopLeftCornerHeaderName = "PART_TopLeftCornerHeader"; private const string DATAGRID_elementTopRightCornerHeaderName = "PART_TopRightCornerHeader"; private const string DATAGRID_elementBottomRightCornerHeaderName = "PART_BottomRightCorner"; - private const string DATAGRID_elementValidationSummary = "PART_ValidationSummary"; private const string DATAGRID_elementVerticalScrollbarName = "PART_VerticalScrollbar"; - - private const bool DATAGRID_defaultAutoGenerateColumns = true; internal const bool DATAGRID_defaultCanUserReorderColumns = true; internal const bool DATAGRID_defaultCanUserResizeColumns = true; internal const bool DATAGRID_defaultCanUserSortColumns = true; - private const DataGridRowDetailsVisibilityMode DATAGRID_defaultRowDetailsVisibility = DataGridRowDetailsVisibilityMode.VisibleWhenSelected; - private const DataGridSelectionMode DATAGRID_defaultSelectionMode = DataGridSelectionMode.Extended; /// /// The default order to use for columns when there is no @@ -1124,7 +1119,7 @@ namespace Avalonia.Controls EnsureColumnHeadersVisibility(); if (!newValueCols) { - _columnHeadersPresenter.Measure(Size.Empty); + _columnHeadersPresenter.Measure(default); } else { @@ -1165,7 +1160,7 @@ namespace Avalonia.Controls _topLeftCornerHeader.IsVisible = newValueRows && newValueCols; if (_topLeftCornerHeader.IsVisible) { - _topLeftCornerHeader.Measure(Size.Empty); + _topLeftCornerHeader.Measure(default); } } @@ -3300,7 +3295,7 @@ namespace Avalonia.Controls newCell.IsVisible = column.IsVisible; if (row.OwningGrid.CellTheme is {} cellTheme) { - newCell.SetValue(ThemeProperty, cellTheme, BindingPriority.TemplatedParent); + newCell.SetValue(ThemeProperty, cellTheme, BindingPriority.Template); } } row.Cells.Insert(column.Index, newCell); @@ -4158,6 +4153,7 @@ namespace Avalonia.Controls if (exitEditingMode) { + CurrentColumn.EndCellEditInternal(); _editingColumnIndex = -1; editingCell.UpdatePseudoClasses(); diff --git a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs index ce74604f70..e859a6e725 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs @@ -4,12 +4,7 @@ // All other rights reserved. using Avalonia.Data; -using Avalonia.Utilities; using System; -using System.Reactive.Disposables; -using System.Reactive.Subjects; -using Avalonia.Reactive; -using System.Diagnostics; using Avalonia.Controls.Utils; using Avalonia.Markup.Xaml.MarkupExtensions; diff --git a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs index 6b1796e50b..39c1e4c118 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs @@ -31,7 +31,7 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static StyledProperty IsThreeStateProperty = + public static readonly StyledProperty IsThreeStateProperty = CheckBox.IsThreeStateProperty.AddOwner(); /// @@ -192,14 +192,14 @@ namespace Avalonia.Controls void OnLayoutUpdated(object sender, EventArgs e) { - if(!editingCheckBox.Bounds.IsEmpty) + if(!editingCheckBox.Bounds.IsDefault) { editingCheckBox.LayoutUpdated -= OnLayoutUpdated; ProcessPointerArgs(); } } - if(editingCheckBox.Bounds.IsEmpty) + if(editingCheckBox.Bounds.IsDefault) { editingCheckBox.LayoutUpdated += OnLayoutUpdated; } diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index d417c87746..d1e1efdd85 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -50,10 +50,13 @@ namespace Avalonia.Controls InheritsWidth = true; } - internal DataGrid OwningGrid + /// + /// Gets the control that contains this column. + /// + protected internal DataGrid OwningGrid { get; - set; + internal set; } internal int Index @@ -182,7 +185,7 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static StyledProperty IsVisibleProperty = + public static readonly StyledProperty IsVisibleProperty = Control.IsVisibleProperty.AddOwner(); /// @@ -638,7 +641,7 @@ namespace Avalonia.Controls public Control GetCellContent(DataGridRow dataGridRow) { - Contract.Requires(dataGridRow != null); + dataGridRow = dataGridRow ?? throw new ArgumentNullException(nameof(dataGridRow)); if (OwningGrid == null) { throw DataGridError.DataGrid.NoOwningGrid(GetType()); @@ -656,7 +659,7 @@ namespace Avalonia.Controls public Control GetCellContent(object dataItem) { - Contract.Requires(dataItem != null); + dataItem = dataItem ?? throw new ArgumentNullException(nameof(dataItem)); if (OwningGrid == null) { throw DataGridError.DataGrid.NoOwningGrid(GetType()); @@ -797,11 +800,22 @@ namespace Avalonia.Controls protected internal virtual void RefreshCellContent(Control element, string propertyName) { } + /// + /// When overridden in a derived class, called when a cell in the column exits editing mode. + /// + protected virtual void EndCellEdit() + { } + internal void CancelCellEditInternal(Control editingElement, object uneditedValue) { CancelCellEdit(editingElement, uneditedValue); } + internal void EndCellEditInternal() + { + EndCellEdit(); + } + /// /// Coerces a DataGridLength to a valid value. If any value components are double.NaN, this method /// coerces them to a proper initial value. For star columns, the desired width is calculated based @@ -894,7 +908,7 @@ namespace Avalonia.Controls result[!ContentControl.ContentTemplateProperty] = this[!HeaderTemplateProperty]; if (OwningGrid.ColumnHeaderTheme is {} columnTheme) { - result.SetValue(StyledElement.ThemeProperty, columnTheme, BindingPriority.TemplatedParent); + result.SetValue(StyledElement.ThemeProperty, columnTheme, BindingPriority.Template); } return result; diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 6473c9c051..b3e106a7bf 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -34,7 +34,6 @@ namespace Avalonia.Controls } private const int DATAGRIDCOLUMNHEADER_resizeRegionWidth = 5; - private const double DATAGRIDCOLUMNHEADER_separatorThickness = 1; private const int DATAGRIDCOLUMNHEADER_columnsDragTreshold = 5; private bool _areHandlersSuspended; @@ -674,7 +673,7 @@ namespace Avalonia.Controls }; if (OwningGrid.ColumnHeaderTheme is {} columnHeaderTheme) { - dragIndicator.SetValue(ThemeProperty, columnHeaderTheme, BindingPriority.TemplatedParent); + dragIndicator.SetValue(ThemeProperty, columnHeaderTheme, BindingPriority.Template); } dragIndicator.PseudoClasses.Add(":dragIndicator"); diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index dfa6ea2e46..ea9b2fe972 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -15,6 +15,7 @@ using Avalonia.Utilities; using Avalonia.VisualTree; using System; using System.Diagnostics; +using Avalonia.Reactive; namespace Avalonia.Controls { @@ -253,7 +254,7 @@ namespace Avalonia.Controls }; if (OwningGrid.CellTheme is {} cellTheme) { - _fillerCell.SetValue(ThemeProperty, cellTheme, BindingPriority.TemplatedParent); + _fillerCell.SetValue(ThemeProperty, cellTheme, BindingPriority.Template); } if (_cellsElement != null) { @@ -1021,11 +1022,11 @@ namespace Avalonia.Controls { layoutableContent.LayoutUpdated += DetailsContent_LayoutUpdated; - _detailsContentSizeSubscription = - System.Reactive.Disposables.StableCompositeDisposable.Create( - System.Reactive.Disposables.Disposable.Create(() => layoutableContent.LayoutUpdated -= DetailsContent_LayoutUpdated), - _detailsContent.GetObservable(MarginProperty) - .Subscribe(DetailsContent_MarginChanged)); + _detailsContentSizeSubscription = new CompositeDisposable(2) + { + Disposable.Create(() => layoutableContent.LayoutUpdated -= DetailsContent_LayoutUpdated), + _detailsContent.GetObservable(MarginProperty).Subscribe(DetailsContent_MarginChanged) + }; } diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs index c746b19cc7..10efded58a 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -10,7 +10,7 @@ using Avalonia.Input; using Avalonia.Media; using System; using System.Diagnostics; -using System.Reactive.Linq; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs index fe3ba0abf6..c42d21d126 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs @@ -18,8 +18,6 @@ namespace Avalonia.Controls.Primitives public class DataGridRowHeader : ContentControl { private const string DATAGRIDROWHEADER_elementRootName = "PART_Root"; - private const double DATAGRIDROWHEADER_separatorThickness = 1; - private Control _rootElement; public static readonly StyledProperty SeparatorBrushProperty = diff --git a/src/Avalonia.Controls.DataGrid/DataGridRows.cs b/src/Avalonia.Controls.DataGrid/DataGridRows.cs index 6c997f62c1..00e035270c 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRows.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRows.cs @@ -16,7 +16,6 @@ using System.Diagnostics; using System.Linq; using Avalonia.Data; using Avalonia.Styling; -using JetBrains.Annotations; namespace Avalonia.Controls { @@ -1030,7 +1029,7 @@ namespace Avalonia.Controls dataGridRow.DataContext = dataContext; if (RowTheme is {} rowTheme) { - dataGridRow.SetValue(ThemeProperty, rowTheme, BindingPriority.TemplatedParent); + dataGridRow.SetValue(ThemeProperty, rowTheme, BindingPriority.Template); } CompleteCellsCollection(dataGridRow); @@ -2744,7 +2743,7 @@ namespace Avalonia.Controls groupHeader.Level = rowGroupInfo.Level; if (RowGroupTheme is {} rowGroupTheme) { - groupHeader.SetValue(ThemeProperty, rowGroupTheme, BindingPriority.TemplatedParent); + groupHeader.SetValue(ThemeProperty, rowGroupTheme, BindingPriority.Template); } // Set the RowGroupHeader's PropertyName. Unfortunately, CollectionViewGroup doesn't have this diff --git a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs index 24ae358dcc..516e9cf6c2 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs @@ -55,17 +55,25 @@ namespace Avalonia.Controls get => _cellEditingCellTemplate; set => SetAndRaise(CellEditingTemplateProperty, ref _cellEditingCellTemplate, value); } - - private static void OnCellTemplateChanged(AvaloniaPropertyChangedEventArgs e) + + private bool _forceGenerateCellFromTemplate; + + protected override void EndCellEdit() { - var oldValue = (IDataTemplate)e.OldValue; - var value = (IDataTemplate)e.NewValue; + //the next call to generate element should not resuse the current content as we need to exit edit mode + _forceGenerateCellFromTemplate = true; + base.EndCellEdit(); } protected override Control GenerateElement(DataGridCell cell, object dataItem) { if (CellTemplate != null) { + if (_forceGenerateCellFromTemplate) + { + _forceGenerateCellFromTemplate = false; + return CellTemplate.Build(dataItem); + } return (CellTemplate is IRecyclingDataTemplate recyclingDataTemplate) ? recyclingDataTemplate.Build(dataItem, cell.Content as Control) : CellTemplate.Build(dataItem); diff --git a/src/Avalonia.Controls.DataGrid/DataGridValueConverter.cs b/src/Avalonia.Controls.DataGrid/DataGridValueConverter.cs index 6144679b60..c823ee1b9c 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridValueConverter.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridValueConverter.cs @@ -22,14 +22,18 @@ namespace Avalonia.Controls return DefaultValueConverter.Instance.Convert(value, targetType, parameter, culture); } - // This suppresses a warning saying that we should use String.IsNullOrEmpty instead of a string - // comparison, but in this case we want to explicitly check for Empty and not Null. + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { if (targetType != null && targetType.IsNullableType()) { var strValue = value as string; - if (string.IsNullOrEmpty(strValue)) + + // This suppresses a warning saying that we should use String.IsNullOrEmpty instead of a string + // comparison, but in this case we want to explicitly check for Empty and not Null. +#pragma warning disable CA1820 + if (strValue == string.Empty) +#pragma warning restore CA1820 { return null; } diff --git a/src/Avalonia.Controls.DataGrid/IndexToValueTable.cs b/src/Avalonia.Controls.DataGrid/IndexToValueTable.cs index 65c3af344c..68a2fc562f 100644 --- a/src/Avalonia.Controls.DataGrid/IndexToValueTable.cs +++ b/src/Avalonia.Controls.DataGrid/IndexToValueTable.cs @@ -422,7 +422,7 @@ namespace Avalonia.Controls else { found = false; - return default(T); + return default; } } diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs index 38d559a031..06a77f0894 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs @@ -161,7 +161,7 @@ namespace Avalonia.Controls.Primitives { // Clip RectangleGeometry rg = new RectangleGeometry(); - rg.Rect = Rect.Empty; + rg.Rect = default; cell.Clip = rg; } } diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs index b34f52f47d..f9b84793c6 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs @@ -305,7 +305,7 @@ namespace Avalonia.Controls.Primitives } if (!OwningGrid.AreColumnHeadersVisible) { - return Size.Empty; + return default; } double height = OwningGrid.ColumnHeaderHeight; bool autoSizeHeight; diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridDetailsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridDetailsPresenter.cs index 543485b311..07e7708003 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridDetailsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridDetailsPresenter.cs @@ -112,7 +112,7 @@ namespace Avalonia.Controls.Primitives { if (OwningGrid == null || Children.Count == 0) { - return Size.Empty; + return default; } double desiredWidth = OwningGrid.AreRowDetailsFrozen ? diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs index 9feca71cda..4fd0ca3d26 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs @@ -26,7 +26,6 @@ namespace Avalonia.Controls.Primitives /// true if the grid is frozen; otherwise, false. The default is true. public static bool GetIsFrozen(Control element) { - Contract.Requires(element != null); return element.GetValue(IsFrozenProperty); } @@ -38,7 +37,6 @@ namespace Avalonia.Controls.Primitives /// is null. public static void SetIsFrozen(Control element, bool value) { - Contract.Requires(element != null); element.SetValue(IsFrozenProperty, value); } } diff --git a/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs b/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs index 1d1a595ccf..d6c46bb1e0 100644 --- a/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs +++ b/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs @@ -2,7 +2,7 @@ using Avalonia.Reactive; using System; using System.Collections.Generic; -using System.Reactive.Subjects; +using Avalonia.Reactive; namespace Avalonia.Controls.Utils { @@ -16,16 +16,16 @@ namespace Avalonia.Controls.Utils internal class CellEditBinding : ICellEditBinding { - private readonly Subject _changedSubject = new Subject(); + private readonly LightweightSubject _changedSubject = new(); private readonly List _validationErrors = new List(); private readonly SubjectWrapper _inner; public bool IsValid => _validationErrors.Count <= 0; public IEnumerable ValidationErrors => _validationErrors; public IObservable ValidationChanged => _changedSubject; - public ISubject InternalSubject => _inner; + public IAvaloniaSubject InternalSubject => _inner; - public CellEditBinding(ISubject bindingSourceSubject) + public CellEditBinding(IAvaloniaSubject bindingSourceSubject) { _inner = new SubjectWrapper(bindingSourceSubject, this); } @@ -48,16 +48,16 @@ namespace Avalonia.Controls.Utils return IsValid; } - class SubjectWrapper : LightweightObservableBase, ISubject, IDisposable + class SubjectWrapper : LightweightObservableBase, IAvaloniaSubject, IDisposable { - private readonly ISubject _sourceSubject; + private readonly IAvaloniaSubject _sourceSubject; private readonly CellEditBinding _editBinding; private IDisposable _subscription; private object _controlValue; private bool _isControlValueSet = false; private bool _settingSourceValue = false; - public SubjectWrapper(ISubject bindingSourceSubject, CellEditBinding editBinding) + public SubjectWrapper(IAvaloniaSubject bindingSourceSubject, CellEditBinding editBinding) { _sourceSubject = bindingSourceSubject; _editBinding = editBinding; diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilder.cs index 5bcd87162e..cf79fcd1a8 100644 --- a/src/Avalonia.Controls/AppBuilder.cs +++ b/src/Avalonia.Controls/AppBuilder.cs @@ -1,4 +1,8 @@ -using Avalonia.Controls; +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Linq; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform; namespace Avalonia @@ -6,15 +10,244 @@ namespace Avalonia /// /// Initializes platform-specific services for an . /// - public sealed class AppBuilder : AppBuilderBase + public class AppBuilder { + private static bool s_setupWasAlreadyCalled; + private Action? _optionsInitializers; + private Func? _appFactory; + private IApplicationLifetime? _lifetime; + + /// + /// Gets or sets the instance. + /// + public IRuntimePlatform RuntimePlatform { get; set; } + + /// + /// Gets or sets a method to call the initialize the runtime platform services (e. g. AssetLoader) + /// + public Action RuntimePlatformServicesInitializer { get; private set; } + + /// + /// Gets the instance being initialized. + /// + public Application? Instance { get; private set; } + + /// + /// Gets the type of the Instance (even if it's not created yet) + /// + public Type? ApplicationType { get; private set; } + + /// + /// Gets or sets a method to call the initialize the windowing subsystem. + /// + public Action? WindowingSubsystemInitializer { get; private set; } + + /// + /// Gets the name of the currently selected windowing subsystem. + /// + public string? WindowingSubsystemName { get; private set; } + + /// + /// Gets or sets a method to call the initialize the windowing subsystem. + /// + public Action? RenderingSubsystemInitializer { get; private set; } + + /// + /// Gets the name of the currently selected rendering subsystem. + /// + public string? RenderingSubsystemName { get; private set; } + + /// + /// Gets or sets a method to call after the is setup. + /// + public Action AfterSetupCallback { get; private set; } = builder => { }; + + + public Action AfterPlatformServicesSetupCallback { get; private set; } = builder => { }; + /// /// Initializes a new instance of the class. /// public AppBuilder() - : base(new StandardRuntimePlatform(), + : this(new StandardRuntimePlatform(), builder => StandardRuntimePlatformServices.Register(builder.ApplicationType?.Assembly)) { } + + /// + /// Initializes a new instance of the class. + /// + protected AppBuilder(IRuntimePlatform platform, Action platformServices) + { + RuntimePlatform = platform; + RuntimePlatformServicesInitializer = () => platformServices(this); + } + + /// + /// Begin configuring an . + /// + /// The subclass of to configure. + /// An instance. + public static AppBuilder Configure() + where TApp : Application, new() + { + return new AppBuilder() + { + ApplicationType = typeof(TApp), + // Needed for CoreRT compatibility + _appFactory = () => new TApp() + }; + } + + /// + /// Begin configuring an . + /// + /// Factory function for . + /// The subclass of to configure. + /// is useful for passing of dependencies to . + /// An instance. + public static AppBuilder Configure(Func appFactory) + where TApp : Application + { + return new AppBuilder() + { + ApplicationType = typeof(TApp), + _appFactory = appFactory + }; + } + + protected AppBuilder Self => this; + + public AppBuilder AfterSetup(Action callback) + { + AfterSetupCallback = (Action)Delegate.Combine(AfterSetupCallback, callback); + return Self; + } + + + public AppBuilder AfterPlatformServicesSetup(Action callback) + { + AfterPlatformServicesSetupCallback = (Action)Delegate.Combine(AfterPlatformServicesSetupCallback, callback); + return Self; + } + + public delegate void AppMainDelegate(Application app, string[] args); + + public void Start(AppMainDelegate main, string[] args) + { + Setup(); + main(Instance!, args); + } + + /// + /// Sets up the platform-specific services for the application, but does not run it. + /// + /// + public AppBuilder SetupWithoutStarting() + { + Setup(); + return Self; + } + + /// + /// Sets up the platform-specific services for the application and initialized it with a particular lifetime, but does not run it. + /// + /// + /// + public AppBuilder SetupWithLifetime(IApplicationLifetime lifetime) + { + _lifetime = lifetime; + Setup(); + return Self; + } + + /// + /// Specifies a windowing subsystem to use. + /// + /// The method to call to initialize the windowing subsystem. + /// The name of the windowing subsystem. + /// An instance. + public AppBuilder UseWindowingSubsystem(Action initializer, string name = "") + { + WindowingSubsystemInitializer = initializer; + WindowingSubsystemName = name; + return Self; + } + + /// + /// Specifies a rendering subsystem to use. + /// + /// The method to call to initialize the rendering subsystem. + /// The name of the rendering subsystem. + /// An instance. + public AppBuilder UseRenderingSubsystem(Action initializer, string name = "") + { + RenderingSubsystemInitializer = initializer; + RenderingSubsystemName = name; + return Self; + } + + /// + /// Configures platform-specific options + /// + public AppBuilder With(T options) + { + _optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind().ToConstant(options); }; + return Self; + } + + /// + /// Configures platform-specific options + /// + public AppBuilder With(Func options) + { + _optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind().ToFunc(options); }; + return Self; + } + + /// + /// Sets up the platform-specific services for the . + /// + private void Setup() + { + if (RuntimePlatformServicesInitializer == null) + { + throw new InvalidOperationException("No runtime platform services configured."); + } + + if (WindowingSubsystemInitializer == null) + { + throw new InvalidOperationException("No windowing system configured."); + } + + if (RenderingSubsystemInitializer == null) + { + throw new InvalidOperationException("No rendering system configured."); + } + + if (_appFactory == null) + { + throw new InvalidOperationException("No Application factory configured."); + } + + if (s_setupWasAlreadyCalled) + { + throw new InvalidOperationException("Setup was already called on one of AppBuilder instances"); + } + + s_setupWasAlreadyCalled = true; + _optionsInitializers?.Invoke(); + RuntimePlatformServicesInitializer(); + RenderingSubsystemInitializer(); + WindowingSubsystemInitializer(); + AfterPlatformServicesSetupCallback(Self); + Instance = _appFactory(); + Instance.ApplicationLifetime = _lifetime; + AvaloniaLocator.CurrentMutable.BindToSelf(Instance); + Instance.RegisterServices(); + Instance.Initialize(); + AfterSetupCallback(Self); + Instance.OnFrameworkInitializationCompleted(); + } } } diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs deleted file mode 100644 index a100d38d78..0000000000 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ /dev/null @@ -1,244 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Linq; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Platform; - -namespace Avalonia.Controls -{ - /// - /// Base class for initializing platform-specific services for an . - /// - /// The type of the AppBuilder class itself. - public abstract class AppBuilderBase where TAppBuilder : AppBuilderBase, new() - { - private static bool s_setupWasAlreadyCalled; - private Action? _optionsInitializers; - private Func? _appFactory; - private IApplicationLifetime? _lifetime; - - /// - /// Gets or sets the instance. - /// - public IRuntimePlatform RuntimePlatform { get; set; } - - /// - /// Gets or sets a method to call the initialize the runtime platform services (e. g. AssetLoader) - /// - public Action RuntimePlatformServicesInitializer { get; private set; } - - /// - /// Gets the instance being initialized. - /// - public Application? Instance { get; private set; } - - /// - /// Gets the type of the Instance (even if it's not created yet) - /// - public Type? ApplicationType { get; private set; } - - /// - /// Gets or sets a method to call the initialize the windowing subsystem. - /// - public Action? WindowingSubsystemInitializer { get; private set; } - - /// - /// Gets the name of the currently selected windowing subsystem. - /// - public string? WindowingSubsystemName { get; private set; } - - /// - /// Gets or sets a method to call the initialize the windowing subsystem. - /// - public Action? RenderingSubsystemInitializer { get; private set; } - - /// - /// Gets the name of the currently selected rendering subsystem. - /// - public string? RenderingSubsystemName { get; private set; } - - /// - /// Gets or sets a method to call after the is setup. - /// - public Action AfterSetupCallback { get; private set; } = builder => { }; - - - public Action AfterPlatformServicesSetupCallback { get; private set; } = builder => { }; - - protected AppBuilderBase(IRuntimePlatform platform, Action platformServices) - { - RuntimePlatform = platform; - RuntimePlatformServicesInitializer = () => platformServices((TAppBuilder)this); - } - - /// - /// Begin configuring an . - /// - /// The subclass of to configure. - /// An instance. - public static TAppBuilder Configure() - where TApp : Application, new() - { - return new TAppBuilder() - { - ApplicationType = typeof(TApp), - // Needed for CoreRT compatibility - _appFactory = () => new TApp() - }; - } - - /// - /// Begin configuring an . - /// - /// Factory function for . - /// The subclass of to configure. - /// is useful for passing of dependencies to . - /// An instance. - public static TAppBuilder Configure(Func appFactory) - where TApp : Application - { - return new TAppBuilder() - { - ApplicationType = typeof(TApp), - _appFactory = appFactory - }; - } - - protected TAppBuilder Self => (TAppBuilder)this; - - public TAppBuilder AfterSetup(Action callback) - { - AfterSetupCallback = (Action)Delegate.Combine(AfterSetupCallback, callback); - return Self; - } - - - public TAppBuilder AfterPlatformServicesSetup(Action callback) - { - AfterPlatformServicesSetupCallback = (Action)Delegate.Combine(AfterPlatformServicesSetupCallback, callback); - return Self; - } - - public delegate void AppMainDelegate(Application app, string[] args); - - public void Start(AppMainDelegate main, string[] args) - { - Setup(); - main(Instance!, args); - } - - /// - /// Sets up the platform-specific services for the application, but does not run it. - /// - /// - public TAppBuilder SetupWithoutStarting() - { - Setup(); - return Self; - } - - /// - /// Sets up the platform-specific services for the application and initialized it with a particular lifetime, but does not run it. - /// - /// - /// - public TAppBuilder SetupWithLifetime(IApplicationLifetime lifetime) - { - _lifetime = lifetime; - Setup(); - return Self; - } - - /// - /// Specifies a windowing subsystem to use. - /// - /// The method to call to initialize the windowing subsystem. - /// The name of the windowing subsystem. - /// An instance. - public TAppBuilder UseWindowingSubsystem(Action initializer, string name = "") - { - WindowingSubsystemInitializer = initializer; - WindowingSubsystemName = name; - return Self; - } - - /// - /// Specifies a rendering subsystem to use. - /// - /// The method to call to initialize the rendering subsystem. - /// The name of the rendering subsystem. - /// An instance. - public TAppBuilder UseRenderingSubsystem(Action initializer, string name = "") - { - RenderingSubsystemInitializer = initializer; - RenderingSubsystemName = name; - return Self; - } - - protected virtual bool CheckSetup => true; - - /// - /// Configures platform-specific options - /// - public TAppBuilder With(T options) - { - _optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind().ToConstant(options); }; - return Self; - } - - /// - /// Configures platform-specific options - /// - public TAppBuilder With(Func options) - { - _optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind().ToFunc(options); }; - return Self; - } - - /// - /// Sets up the platform-specific services for the . - /// - private void Setup() - { - if (RuntimePlatformServicesInitializer == null) - { - throw new InvalidOperationException("No runtime platform services configured."); - } - - if (WindowingSubsystemInitializer == null) - { - throw new InvalidOperationException("No windowing system configured."); - } - - if (RenderingSubsystemInitializer == null) - { - throw new InvalidOperationException("No rendering system configured."); - } - - if (_appFactory == null) - { - throw new InvalidOperationException("No Application factory configured."); - } - - if (s_setupWasAlreadyCalled && CheckSetup) - { - throw new InvalidOperationException("Setup was already called on one of AppBuilder instances"); - } - - s_setupWasAlreadyCalled = true; - _optionsInitializers?.Invoke(); - RuntimePlatformServicesInitializer(); - RenderingSubsystemInitializer(); - WindowingSubsystemInitializer(); - AfterPlatformServicesSetupCallback(Self); - Instance = _appFactory(); - Instance.ApplicationLifetime = _lifetime; - AvaloniaLocator.CurrentMutable.BindToSelf(Instance); - Instance.RegisterServices(); - Instance.Initialize(); - AfterSetupCallback(Self); - Instance.OnFrameworkInitializationCompleted(); - } - } -} diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index dc9a0207ad..5b652cce19 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Reactive.Concurrency; -using System.Threading; using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; @@ -231,7 +229,6 @@ namespace Avalonia .Bind().ToConstant(FocusManager) .Bind().ToConstant(InputManager) .Bind().ToTransient() - .Bind().ToConstant(AvaloniaScheduler.Instance) .Bind().ToConstant(DragDropDevice.Instance); // TODO: Fix this, for now we keep this behavior since someone might be relying on it in 0.9.x diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index c59458311c..fde401fb01 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -83,12 +83,12 @@ namespace Avalonia.Controls.ApplicationLifetimes public void Shutdown(int exitCode = 0) { - DoShutdown(new ShutdownRequestedEventArgs(), true, exitCode); + DoShutdown(new ShutdownRequestedEventArgs(), true, true, exitCode); } public bool TryShutdown(int exitCode = 0) { - return DoShutdown(new ShutdownRequestedEventArgs(), false, exitCode); + return DoShutdown(new ShutdownRequestedEventArgs(), true, false, exitCode); } public int Start(string[] args) @@ -134,7 +134,11 @@ namespace Avalonia.Controls.ApplicationLifetimes _activeLifetime = null; } - private bool DoShutdown(ShutdownRequestedEventArgs e, bool force = false, int exitCode = 0) + private bool DoShutdown( + ShutdownRequestedEventArgs e, + bool isProgrammatic, + bool force = false, + int exitCode = 0) { if (!force) { @@ -159,7 +163,7 @@ namespace Avalonia.Controls.ApplicationLifetimes { if (w.Owner is null) { - w.Close(); + w.CloseCore(WindowCloseReason.ApplicationShutdown, isProgrammatic); } } @@ -183,7 +187,7 @@ namespace Avalonia.Controls.ApplicationLifetimes return true; } - private void OnShutdownRequested(object? sender, ShutdownRequestedEventArgs e) => DoShutdown(e); + private void OnShutdownRequested(object? sender, ShutdownRequestedEventArgs e) => DoShutdown(e, false); } public class ClassicDesktopStyleApplicationLifetimeOptions @@ -196,9 +200,8 @@ namespace Avalonia { public static class ClassicDesktopStyleApplicationLifetimeExtensions { - public static int StartWithClassicDesktopLifetime( - this T builder, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) - where T : AppBuilderBase, new() + public static int StartWithClassicDesktopLifetime( + this AppBuilder builder, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) { var lifetime = new ClassicDesktopStyleApplicationLifetime() { diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs index 4b88f6b537..22b5f8236d 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using Avalonia.Metadata; namespace Avalonia.Controls.ApplicationLifetimes @@ -19,7 +18,7 @@ namespace Avalonia.Controls.ApplicationLifetimes /// /// Gets the arguments passed to the - /// + /// /// method. /// string[]? Args { get; } diff --git a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs index e0d986f2b4..98885e11ca 100644 --- a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs @@ -10,7 +10,7 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using System.Threading; using System.Threading.Tasks; using Avalonia.Collections; @@ -474,6 +474,7 @@ namespace Avalonia.Controls FilterModeProperty.Changed.AddClassHandler((x,e) => x.OnFilterModePropertyChanged(e)); ItemFilterProperty.Changed.AddClassHandler((x,e) => x.OnItemFilterPropertyChanged(e)); ItemsProperty.Changed.AddClassHandler((x,e) => x.OnItemsPropertyChanged(e)); + ItemTemplateProperty.Changed.AddClassHandler((x,e) => x.OnItemTemplatePropertyChanged(e)); IsEnabledProperty.Changed.AddClassHandler((x,e) => x.OnControlIsEnabledChanged(e)); } diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 2710ac5cc2..42c577041a 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -6,8 +6,6 @@ - - diff --git a/src/Avalonia.Controls/BorderVisual.cs b/src/Avalonia.Controls/BorderVisual.cs index 7afbf9edcf..b0e5c30e2f 100644 --- a/src/Avalonia.Controls/BorderVisual.cs +++ b/src/Avalonia.Controls/BorderVisual.cs @@ -50,7 +50,7 @@ class CompositionBorderVisual : CompositionDrawListVisual if (ClipToBounds) { var clipRect = Root!.SnapToDevicePixels(new Rect(new Size(Size.X, Size.Y))); - if (_cornerRadius.IsEmpty) + if (_cornerRadius.IsDefault) canvas.PushClip(clipRect); else canvas.PushClip(new RoundedRect(clipRect, _cornerRadius)); @@ -63,9 +63,9 @@ class CompositionBorderVisual : CompositionDrawListVisual } - protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) + protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt) { - base.DeserializeChangesCore(reader, commitedAt); + base.DeserializeChangesCore(reader, committedAt); if (reader.Read()) _cornerRadius = reader.Read(); } @@ -73,4 +73,4 @@ class CompositionBorderVisual : CompositionDrawListVisual protected override bool HandlesClipToBounds => true; } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 8e5d4e1e06..1ec6f8dabc 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -5,6 +5,7 @@ using System.Windows.Input; using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; diff --git a/src/Avalonia.Controls/ButtonSpinner.cs b/src/Avalonia.Controls/ButtonSpinner.cs index e455c6c6f3..4b2997f431 100644 --- a/src/Avalonia.Controls/ButtonSpinner.cs +++ b/src/Avalonia.Controls/ButtonSpinner.cs @@ -1,7 +1,6 @@ -using System; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; -using Avalonia.Data; +using Avalonia.Reactive; using Avalonia.Input; using Avalonia.Interactivity; diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs index ec1273ca98..b17648f5bb 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs @@ -7,7 +7,7 @@ using System; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Data; diff --git a/src/Avalonia.Controls/Canvas.cs b/src/Avalonia.Controls/Canvas.cs index adee7d4d90..cd81c6e59b 100644 --- a/src/Avalonia.Controls/Canvas.cs +++ b/src/Avalonia.Controls/Canvas.cs @@ -1,7 +1,4 @@ -using System; -using System.Reactive.Concurrency; using Avalonia.Input; -using Avalonia.Layout; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs index 6d7e542bb2..f5fcbed9fb 100644 --- a/src/Avalonia.Controls/Chrome/CaptionButtons.cs +++ b/src/Avalonia.Controls/Chrome/CaptionButtons.cs @@ -1,5 +1,5 @@ using System; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; @@ -15,7 +15,7 @@ namespace Avalonia.Controls.Chrome [PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")] public class CaptionButtons : TemplatedControl { - private CompositeDisposable? _disposables; + private IDisposable? _disposables; /// /// Currently attached window. @@ -28,17 +28,14 @@ namespace Avalonia.Controls.Chrome { HostWindow = hostWindow; - _disposables = new CompositeDisposable - { - HostWindow.GetObservable(Window.WindowStateProperty) + _disposables = HostWindow.GetObservable(Window.WindowStateProperty) .Subscribe(x => { PseudoClasses.Set(":minimized", x == WindowState.Minimized); PseudoClasses.Set(":normal", x == WindowState.Normal); PseudoClasses.Set(":maximized", x == WindowState.Maximized); PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen); - }) - }; + }); } } diff --git a/src/Avalonia.Controls/Chrome/TitleBar.cs b/src/Avalonia.Controls/Chrome/TitleBar.cs index 1bf13111a9..47b0bb6e2d 100644 --- a/src/Avalonia.Controls/Chrome/TitleBar.cs +++ b/src/Avalonia.Controls/Chrome/TitleBar.cs @@ -1,5 +1,5 @@ using System; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; @@ -61,7 +61,7 @@ namespace Avalonia.Controls.Chrome if (VisualRoot is Window window) { - _disposables = new CompositeDisposable + _disposables = new CompositeDisposable(6) { window.GetObservable(Window.WindowDecorationMarginProperty) .Subscribe(x => UpdateSize(window)), diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index f02df2e9c1..6018779e34 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using Avalonia.Automation.Peers; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Controls.Generators; using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; @@ -95,19 +95,26 @@ namespace Avalonia.Controls { ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); FocusableProperty.OverrideDefaultValue(true); - SelectedItemProperty.Changed.AddClassHandler((x, e) => x.SelectedItemChanged(e)); - KeyDownEvent.AddClassHandler((x, e) => x.OnKeyDown(e), Interactivity.RoutingStrategies.Tunnel); IsTextSearchEnabledProperty.OverrideDefaultValue(true); - IsDropDownOpenProperty.Changed.AddClassHandler((x, e) => x.DropdownChanged(e)); } + /// + /// Occurs after the drop-down (popup) list of the closes. + /// + public event EventHandler? DropDownClosed; + + /// + /// Occurs after the drop-down (popup) list of the opens. + /// + public event EventHandler? DropDownOpened; + /// /// Gets or sets a value indicating whether the dropdown is currently open. /// public bool IsDropDownOpen { - get { return _isDropDownOpen; } - set { SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); } + get => _isDropDownOpen; + set => SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); } /// @@ -115,8 +122,8 @@ namespace Avalonia.Controls /// public double MaxDropDownHeight { - get { return GetValue(MaxDropDownHeightProperty); } - set { SetValue(MaxDropDownHeightProperty, value); } + get => GetValue(MaxDropDownHeightProperty); + set => SetValue(MaxDropDownHeightProperty, value); } /// @@ -124,8 +131,8 @@ namespace Avalonia.Controls /// protected object? SelectionBoxItem { - get { return _selectionBoxItem; } - set { SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value); } + get => _selectionBoxItem; + set => SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value); } /// @@ -133,8 +140,8 @@ namespace Avalonia.Controls /// public string? PlaceholderText { - get { return GetValue(PlaceholderTextProperty); } - set { SetValue(PlaceholderTextProperty, value); } + get => GetValue(PlaceholderTextProperty); + set => SetValue(PlaceholderTextProperty, value); } /// @@ -142,8 +149,8 @@ namespace Avalonia.Controls /// public IBrush? PlaceholderForeground { - get { return GetValue(PlaceholderForegroundProperty); } - set { SetValue(PlaceholderForegroundProperty, value); } + get => GetValue(PlaceholderForegroundProperty); + set => SetValue(PlaceholderForegroundProperty, value); } /// @@ -151,8 +158,8 @@ namespace Avalonia.Controls /// public ItemVirtualizationMode VirtualizationMode { - get { return GetValue(VirtualizationModeProperty); } - set { SetValue(VirtualizationModeProperty, value); } + get => GetValue(VirtualizationModeProperty); + set => SetValue(VirtualizationModeProperty, value); } /// @@ -160,8 +167,8 @@ namespace Avalonia.Controls /// public HorizontalAlignment HorizontalContentAlignment { - get { return GetValue(HorizontalContentAlignmentProperty); } - set { SetValue(HorizontalContentAlignmentProperty, value); } + get => GetValue(HorizontalContentAlignmentProperty); + set => SetValue(HorizontalContentAlignmentProperty, value); } /// @@ -169,8 +176,8 @@ namespace Avalonia.Controls /// public VerticalAlignment VerticalContentAlignment { - get { return GetValue(VerticalContentAlignmentProperty); } - set { SetValue(VerticalContentAlignmentProperty, value); } + get => GetValue(VerticalContentAlignmentProperty); + set => SetValue(VerticalContentAlignmentProperty, value); } /// @@ -182,6 +189,7 @@ namespace Avalonia.Controls ComboBoxItem.ContentTemplateProperty); } + /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); @@ -213,7 +221,12 @@ namespace Avalonia.Controls IsDropDownOpen = false; e.Handled = true; } - else if (IsDropDownOpen && e.Key == Key.Enter) + else if (!IsDropDownOpen && (e.Key == Key.Enter || e.Key == Key.Space)) + { + IsDropDownOpen = true; + e.Handled = true; + } + else if (IsDropDownOpen && (e.Key == Key.Enter || e.Key == Key.Space)) { SelectFocusedItem(); IsDropDownOpen = false; @@ -234,7 +247,7 @@ namespace Avalonia.Controls } // This part of code is needed just to acquire initial focus, subsequent focus navigation will be done by ItemsControl. else if (IsDropDownOpen && SelectedIndex < 0 && ItemCount > 0 && - (e.Key == Key.Up || e.Key == Key.Down) && IsFocused == true) + (e.Key == Key.Up || e.Key == Key.Down) && IsFocused == true) { var firstChild = Presenter?.Panel?.Children.FirstOrDefault(c => CanFocus(c)); if (firstChild != null) @@ -304,12 +317,11 @@ namespace Avalonia.Controls e.Handled = true; } } + PseudoClasses.Set(pcPressed, false); base.OnPointerReleased(e); - } - /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { @@ -324,6 +336,22 @@ namespace Avalonia.Controls _popup.Closed += PopupClosed; } + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == SelectedItemProperty) + { + UpdateSelectionBoxItem(change.NewValue); + TryFocusSelectedItem(); + } + else if (change.Property == IsDropDownOpenProperty) + { + PseudoClasses.Set(pcDropdownOpen, change.GetNewValue()); + } + + base.OnPropertyChanged(change); + } + protected override AutomationPeer OnCreateAutomationPeer() { return new ComboBoxAutomationPeer(this); @@ -345,6 +373,8 @@ namespace Avalonia.Controls { Focus(); } + + DropDownClosed?.Invoke(this, EventArgs.Empty); } private void PopupOpened(object? sender, EventArgs e) @@ -353,7 +383,7 @@ namespace Avalonia.Controls _subscriptionsOnOpen.Clear(); - var toplevel = this.GetVisualRoot() as TopLevel; + var toplevel = TopLevel.GetTopLevel(this); if (toplevel != null) { toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) => @@ -374,6 +404,8 @@ namespace Avalonia.Controls } UpdateFlowDirection(); + + DropDownOpened?.Invoke(this, EventArgs.Empty); } private void IsVisibleChanged(bool isVisible) @@ -384,12 +416,6 @@ namespace Avalonia.Controls } } - private void SelectedItemChanged(AvaloniaPropertyChangedEventArgs e) - { - UpdateSelectionBoxItem(e.NewValue); - TryFocusSelectedItem(); - } - private void TryFocusSelectedItem() { var selectedIndex = SelectedIndex; @@ -489,11 +515,5 @@ namespace Avalonia.Controls MoveSelection(NavigationDirection.Previous, WrapSelection); } } - - private void DropdownChanged(AvaloniaPropertyChangedEventArgs e) - { - bool newValue = e.GetNewValue(); - PseudoClasses.Set(pcDropdownOpen, newValue); - } } } diff --git a/src/Avalonia.Controls/ComboBoxItem.cs b/src/Avalonia.Controls/ComboBoxItem.cs index 83057d139f..7db713d692 100644 --- a/src/Avalonia.Controls/ComboBoxItem.cs +++ b/src/Avalonia.Controls/ComboBoxItem.cs @@ -1,5 +1,4 @@ -using System; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Automation; using Avalonia.Automation.Peers; @@ -12,8 +11,14 @@ namespace Avalonia.Controls { public ComboBoxItem() { - this.GetObservable(ComboBoxItem.IsFocusedProperty).Where(focused => focused) - .Subscribe(_ => (Parent as ComboBox)?.ItemFocused(this)); + this.GetObservable(ComboBoxItem.IsFocusedProperty) + .Subscribe(focused => + { + if (focused) + { + (Parent as ComboBox)?.ItemFocused(this); + } + }); } static ComboBoxItem() diff --git a/src/Avalonia.Controls/ContentControl.cs b/src/Avalonia.Controls/ContentControl.cs index b8a45e102f..d47a7a7809 100644 --- a/src/Avalonia.Controls/ContentControl.cs +++ b/src/Avalonia.Controls/ContentControl.cs @@ -116,14 +116,19 @@ namespace Avalonia.Controls return false; } - private void ContentChanged(AvaloniaPropertyChangedEventArgs e) + protected virtual void ContentChanged(AvaloniaPropertyChangedEventArgs e) { - if (e.OldValue is ILogical oldChild) + UpdateLogicalTree(e.OldValue, e.NewValue); + } + + protected void UpdateLogicalTree(object? toRemove, object? toAdd) + { + if (toRemove is ILogical oldChild) { LogicalChildren.Remove(oldChild); } - if (e.NewValue is ILogical newChild) + if (toAdd is ILogical newChild) { LogicalChildren.Add(newChild); } diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 020a6de539..b7ff75339d 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -15,6 +15,7 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Styling; using Avalonia.Automation; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 88c9823952..ed24c3c7c2 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -366,25 +366,30 @@ namespace Avalonia.Controls { base.OnAttachedToVisualTreeCore(e); + AddHandler(Gestures.HoldingEvent, OnHoldEvent); + InitializeIfNeeded(); ScheduleOnLoadedCore(); } - /// - protected sealed override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) + private void OnHoldEvent(object? sender, HoldingRoutedEventArgs e) { - base.OnDetachedFromVisualTreeCore(e); - - OnUnloadedCore(); + if(e.HoldingState == HoldingState.Started) + { + // Trigger ContentRequest when hold has started + RaiseEvent(new ContextRequestedEventArgs()); + } } /// - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + protected sealed override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) { - base.OnAttachedToVisualTree(e); + base.OnDetachedFromVisualTreeCore(e); + + RemoveHandler(Gestures.HoldingEvent, OnHoldEvent); - InvalidateMirrorTransform(); + OnUnloadedCore(); } /// diff --git a/src/Avalonia.Controls/ControlExtensions.cs b/src/Avalonia.Controls/ControlExtensions.cs index bceeb58adb..889a4cc79f 100644 --- a/src/Avalonia.Controls/ControlExtensions.cs +++ b/src/Avalonia.Controls/ControlExtensions.cs @@ -1,8 +1,5 @@ using System; -using System.Linq; -using Avalonia.Data; -using Avalonia.LogicalTree; -using Avalonia.Styling; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/DataValidationErrors.cs b/src/Avalonia.Controls/DataValidationErrors.cs index 00a06101c5..b082909807 100644 --- a/src/Avalonia.Controls/DataValidationErrors.cs +++ b/src/Avalonia.Controls/DataValidationErrors.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Controls.Metadata; using Avalonia.Controls.Templates; using Avalonia.Data; diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 64a02ccb46..5c35a09f1c 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -7,6 +7,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; +using Avalonia.Reactive; using Avalonia.Utilities; namespace Avalonia.Controls @@ -805,14 +806,12 @@ namespace Avalonia.Controls /// The properties. protected static void AffectsParentMeasure(params AvaloniaProperty[] properties) { - void Invalidate(AvaloniaPropertyChangedEventArgs e) - { - (e.Sender as DefinitionBase)?.Parent?.InvalidateMeasure(); - } + var invalidateObserver = new AnonymousObserver( + static e => (e.Sender as DefinitionBase)?.Parent?.InvalidateMeasure()); foreach (var property in properties) { - property.Changed.Subscribe(Invalidate); + property.Changed.Subscribe(invalidateObserver); } } } diff --git a/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs b/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs index 9fddb91231..343b628fb9 100644 --- a/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs +++ b/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs @@ -8,6 +8,6 @@ /// /// Provides access to the internal for use in DevTools. /// - public static AvaloniaProperty ToolTipProperty = ToolTip.ToolTipProperty; + public static readonly AvaloniaProperty ToolTipProperty = ToolTip.ToolTipProperty; } } diff --git a/src/Avalonia.Controls/Documents/Run.cs b/src/Avalonia.Controls/Documents/Run.cs index 5d7b8998e6..f64cf79127 100644 --- a/src/Avalonia.Controls/Documents/Run.cs +++ b/src/Avalonia.Controls/Documents/Run.cs @@ -54,6 +54,11 @@ namespace Avalonia.Controls.Documents { var text = Text ?? ""; + if (string.IsNullOrEmpty(text)) + { + return; + } + var textRunProperties = CreateTextRunProperties(); var textCharacters = new TextCharacters(text, textRunProperties); diff --git a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs index 30bdb4c60e..e4487d29fa 100644 --- a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs +++ b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs @@ -3,6 +3,7 @@ using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform; using System; +using Avalonia.Reactive; using Avalonia.Media.Immutable; namespace Avalonia.Controls diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 3ff248f0be..9d4abec549 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -7,6 +7,7 @@ using Avalonia.Input.Platform; using Avalonia.Input.Raw; using Avalonia.Layout; using Avalonia.Logging; +using Avalonia.Reactive; namespace Avalonia.Controls.Primitives { @@ -435,7 +436,7 @@ namespace Avalonia.Controls.Primitives { Size sz; // Popup.Child can't be null here, it was set in ShowAtCore. - if (Popup.Child!.DesiredSize == Size.Empty) + if (Popup.Child!.DesiredSize.IsDefault) { // Popup may not have been shown yet. Measure content sz = LayoutHelper.MeasureChild(Popup.Child, Size.Infinity, new Thickness()); @@ -457,7 +458,7 @@ namespace Avalonia.Controls.Primitives PopupPositioning.PopupPositionerConstraintAdjustment.SlideY; } - var trgtBnds = Target?.Bounds ?? Rect.Empty; + var trgtBnds = Target?.Bounds ?? default; switch (Placement) { diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index dc3c621db4..de753f0bd0 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -2,6 +2,7 @@ using System; using System.Windows.Input; using Avalonia.Controls.Utils; using Avalonia.Input; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index 5668b79e81..ce254684b7 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -4,7 +4,7 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Media; namespace Avalonia.Controls @@ -91,7 +91,7 @@ namespace Avalonia.Controls arrangedsize = TransformRoot.Bounds.Size; // This is the first opportunity under Silverlight to find out the Child's true DesiredSize - if (IsSizeSmaller(finalSizeTransformed, arrangedsize) && (Size.Empty == _childActualSize)) + if (IsSizeSmaller(finalSizeTransformed, arrangedsize) && _childActualSize.IsDefault) { //// Unfortunately, all the work so far is invalid because the wrong DesiredSize was used //// Make a note of the actual DesiredSize @@ -102,7 +102,7 @@ namespace Avalonia.Controls else { // Clear the "need to measure/arrange again" flag - _childActualSize = Size.Empty; + _childActualSize = default; } // Return result to perform the transformation @@ -122,7 +122,7 @@ namespace Avalonia.Controls } Size measureSize; - if (_childActualSize == Size.Empty) + if (_childActualSize.IsDefault) { // Determine the largest size after the transformation measureSize = ComputeLargestTransformedSize(availableSize); @@ -206,7 +206,7 @@ namespace Avalonia.Controls /// /// Actual DesiredSize of Child element (the value it returned from its MeasureOverride method). /// - private Size _childActualSize = Size.Empty; + private Size _childActualSize = default; /// /// RenderTransform/MatrixTransform applied to TransformRoot. @@ -281,7 +281,7 @@ namespace Avalonia.Controls private Size ComputeLargestTransformedSize(Size arrangeBounds) { // Computed largest transformed size - Size computedSize = Size.Empty; + Size computedSize = default; // Detect infinite bounds and constrain the scenario bool infiniteWidth = double.IsInfinity(arrangeBounds.Width); @@ -424,9 +424,9 @@ namespace Avalonia.Controls if (newTransform != null) { - _transformChangedEvent = Observable.FromEventPattern( + _transformChangedEvent = Observable.FromEventPattern( v => newTransform.Changed += v, v => newTransform.Changed -= v) - .Subscribe(onNext: v => ApplyLayoutTransform()); + .Subscribe(_ => ApplyLayoutTransform()); } ApplyLayoutTransform(); diff --git a/src/Avalonia.Controls/LoggingExtensions.cs b/src/Avalonia.Controls/LoggingExtensions.cs index ef14d0b477..f909ba8714 100644 --- a/src/Avalonia.Controls/LoggingExtensions.cs +++ b/src/Avalonia.Controls/LoggingExtensions.cs @@ -8,16 +8,14 @@ namespace Avalonia /// /// Logs Avalonia events to the sink. /// - /// The application class type. /// The app builder instance. /// The minimum level to log. /// The areas to log. Valid values are listed in . /// The app builder instance. - public static T LogToTrace( - this T builder, + public static AppBuilder LogToTrace( + this AppBuilder builder, LogEventLevel level = LogEventLevel.Warning, params string[] areas) - where T : AppBuilderBase, new() { Logger.Sink = new TraceLogSink(level, areas); return builder; diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 3e00cea430..40941d00be 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using System.Windows.Input; using Avalonia.Automation.Peers; using Avalonia.Controls.Generators; @@ -159,12 +159,13 @@ namespace Avalonia.Controls // menu layout. var parentSharedSizeScope = this.GetObservable(VisualParentProperty) - .SelectMany(x => + .Select(x => { var parent = x as Control; return parent?.GetObservable(DefinitionBase.PrivateSharedSizeScopeProperty) ?? Observable.Return(null); - }); + }) + .Switch(); this.Bind(DefinitionBase.PrivateSharedSizeScopeProperty, parentSharedSizeScope); } diff --git a/src/Avalonia.Controls/Mixins/DisposableMixin.cs b/src/Avalonia.Controls/Mixins/DisposableMixin.cs deleted file mode 100644 index 9b30b4ba4c..0000000000 --- a/src/Avalonia.Controls/Mixins/DisposableMixin.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Reactive.Disposables; - -namespace Avalonia.Controls.Mixins -{ - /// - /// Extension methods associated with the IDisposable interface. - /// - public static class DisposableMixin - { - /// - /// Ensures the provided disposable is disposed with the specified . - /// - /// - /// The type of the disposable. - /// - /// - /// The disposable we are going to want to be disposed by the CompositeDisposable. - /// - /// - /// The to which will be added. - /// - /// - /// The disposable. - /// - public static T DisposeWith(this T item, CompositeDisposable compositeDisposable) - where T : IDisposable - { - if (compositeDisposable is null) - { - throw new ArgumentNullException(nameof(compositeDisposable)); - } - - compositeDisposable.Add(item); - return item; - } - } -} diff --git a/src/Avalonia.Controls/Mixins/SelectableMixin.cs b/src/Avalonia.Controls/Mixins/SelectableMixin.cs index a04a741e3e..2a8fe7b976 100644 --- a/src/Avalonia.Controls/Mixins/SelectableMixin.cs +++ b/src/Avalonia.Controls/Mixins/SelectableMixin.cs @@ -1,7 +1,7 @@ using System; using Avalonia.Interactivity; using Avalonia.Controls.Primitives; -using Avalonia.VisualTree; +using Avalonia.Reactive; namespace Avalonia.Controls.Mixins { diff --git a/src/Avalonia.Controls/NativeControlHost.cs b/src/Avalonia.Controls/NativeControlHost.cs index bcf0866129..18dc1b1264 100644 --- a/src/Avalonia.Controls/NativeControlHost.cs +++ b/src/Avalonia.Controls/NativeControlHost.cs @@ -145,7 +145,7 @@ namespace Avalonia.Controls if (IsEffectivelyVisible && bounds.HasValue) { - if (bounds.Value.IsEmpty) + if (bounds.Value.IsDefault) return false; _attachment?.ShowInBounds(bounds.Value); } diff --git a/src/Avalonia.Controls/NativeMenu.Export.cs b/src/Avalonia.Controls/NativeMenu.Export.cs index fe650ab41e..c556ce7b02 100644 --- a/src/Avalonia.Controls/NativeMenu.Export.cs +++ b/src/Avalonia.Controls/NativeMenu.Export.cs @@ -1,7 +1,6 @@ using System; -using System.Collections.Generic; using Avalonia.Controls.Platform; -using Avalonia.Data; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/NativeMenuBar.cs b/src/Avalonia.Controls/NativeMenuBar.cs index 8b3e875e5a..3953de8165 100644 --- a/src/Avalonia.Controls/NativeMenuBar.cs +++ b/src/Avalonia.Controls/NativeMenuBar.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics.CodeAnalysis; using Avalonia.Controls.Primitives; using Avalonia.Interactivity; +using Avalonia.Reactive; namespace Avalonia.Controls { @@ -23,15 +24,6 @@ namespace Avalonia.Controls }); } - [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NativeMenu))] - [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NativeMenuItem))] - [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NativeMenuItemBase))] - [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NativeMenuItemSeparator))] - public NativeMenuBar() - { - - } - public static void SetEnableMenuItemClickForwarding(MenuItem menuItem, bool enable) { menuItem.SetValue(EnableMenuItemClickForwardingProperty, enable); diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index d22fdb2f84..526eff0f12 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -4,6 +4,7 @@ using Avalonia.Input; using Avalonia.Media.Imaging; using Avalonia.Metadata; using Avalonia.Utilities; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/NativeMenuItemSeparator.cs b/src/Avalonia.Controls/NativeMenuItemSeparator.cs index 97278130b1..f55d714884 100644 --- a/src/Avalonia.Controls/NativeMenuItemSeparator.cs +++ b/src/Avalonia.Controls/NativeMenuItemSeparator.cs @@ -1,7 +1,10 @@ namespace Avalonia.Controls { - public class NativeMenuItemSeparator : NativeMenuItemBase + public class NativeMenuItemSeparator : NativeMenuItem { - + public NativeMenuItemSeparator() + { + Header = "-"; + } } } diff --git a/src/Avalonia.Controls/Notifications/NotificationCard.cs b/src/Avalonia.Controls/Notifications/NotificationCard.cs index a103adf185..663bd3358a 100644 --- a/src/Avalonia.Controls/Notifications/NotificationCard.cs +++ b/src/Avalonia.Controls/Notifications/NotificationCard.cs @@ -1,6 +1,6 @@ using System; using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Controls.Metadata; using Avalonia.Interactivity; using Avalonia.LogicalTree; @@ -37,30 +37,29 @@ namespace Avalonia.Controls.Notifications RaiseEvent(new RoutedEventArgs(NotificationClosedEvent)); }); - // Disabling nullable checking because of https://github.com/dotnet/reactive/issues/1525 -#pragma warning disable CS8620 this.GetObservable(ContentProperty) - .OfType() -#pragma warning restore CS8620 .Subscribe(x => { - switch (x.Type) + if (x is Notification notification) { - case NotificationType.Error: - PseudoClasses.Add(":error"); - break; - - case NotificationType.Information: - PseudoClasses.Add(":information"); - break; - - case NotificationType.Success: - PseudoClasses.Add(":success"); - break; - - case NotificationType.Warning: - PseudoClasses.Add(":warning"); - break; + switch (notification.Type) + { + case NotificationType.Error: + PseudoClasses.Add(":error"); + break; + + case NotificationType.Information: + PseudoClasses.Add(":information"); + break; + + case NotificationType.Success: + PseudoClasses.Add(":success"); + break; + + case NotificationType.Warning: + PseudoClasses.Add(":warning"); + break; + } } }); } diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index 46c772f3b1..3ccddf4155 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -1,7 +1,7 @@ using System; using System.Collections; using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using System.Threading.Tasks; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index 8ed6abd52b..ac4f699313 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -9,6 +9,7 @@ using Avalonia.Data.Converters; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; +using Avalonia.Reactive; using Avalonia.Threading; using Avalonia.Utilities; diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 7134e1b9e7..007d18c813 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -5,6 +5,7 @@ using System.Linq; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; +using Avalonia.Reactive; using Avalonia.Styling; namespace Avalonia.Controls @@ -86,9 +87,11 @@ namespace Avalonia.Controls protected static void AffectsParentArrange(params AvaloniaProperty[] properties) where TPanel : Panel { + var invalidateObserver = new AnonymousObserver( + static e => AffectsParentArrangeInvalidate(e)); foreach (var property in properties) { - property.Changed.Subscribe(AffectsParentArrangeInvalidate); + property.Changed.Subscribe(invalidateObserver); } } @@ -99,9 +102,11 @@ namespace Avalonia.Controls protected static void AffectsParentMeasure(params AvaloniaProperty[] properties) where TPanel : Panel { + var invalidateObserver = new AnonymousObserver( + static e => AffectsParentMeasureInvalidate(e)); foreach (var property in properties) { - property.Changed.Subscribe(AffectsParentMeasureInvalidate); + property.Changed.Subscribe(invalidateObserver); } } diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 6d525da150..e09da02f17 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -6,9 +6,9 @@ using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.Metadata; using Avalonia.Platform; +using Avalonia.Reactive; using Avalonia.Rendering; using Avalonia.Threading; -using Avalonia.VisualTree; namespace Avalonia.Controls.Platform { diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index af9392d440..8d9d8e0e7b 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -68,7 +68,7 @@ namespace Avalonia.Platform /// Gets or sets a method called before the underlying implementation is destroyed. /// Return true to prevent the underlying implementation from closing. /// - Func Closing { get; set; } + Func Closing { get; set; } /// /// Gets a value to indicate if the platform was able to extend client area to non-client area. diff --git a/src/Avalonia.Controls/Platform/InProcessDragSource.cs b/src/Avalonia.Controls/Platform/InProcessDragSource.cs index 5b2356a7ce..bfd8bd73cb 100644 --- a/src/Avalonia.Controls/Platform/InProcessDragSource.cs +++ b/src/Avalonia.Controls/Platform/InProcessDragSource.cs @@ -1,7 +1,5 @@ -using System; -using System.Linq; -using System.Reactive.Linq; -using System.Reactive.Subjects; +using System.Linq; +using Avalonia.Reactive; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Input; @@ -17,7 +15,7 @@ namespace Avalonia.Platform private const RawInputModifiers MOUSE_INPUTMODIFIERS = RawInputModifiers.LeftMouseButton|RawInputModifiers.MiddleMouseButton|RawInputModifiers.RightMouseButton; private readonly IDragDropDevice _dragDrop; private readonly IInputManager _inputManager; - private readonly Subject _result = new Subject(); + private readonly LightweightSubject _result = new(); private DragDropEffects _allowedEffects; private IDataObject? _draggedData; @@ -41,14 +39,28 @@ namespace Avalonia.Platform { _draggedData = data; _lastRoot = null; - _lastPosition = default(Point); + _lastPosition = default; _allowedEffects = allowedEffects; - using (_inputManager.PreProcess.OfType().Subscribe(ProcessMouseEvents)) + var inputObserver = new AnonymousObserver(arg => { - using (_inputManager.PreProcess.OfType().Subscribe(ProcessKeyEvents)) + switch (arg) { - var effect = await _result.FirstAsync(); + case RawPointerEventArgs pointerEventArgs: + ProcessMouseEvents(pointerEventArgs); + break; + case RawKeyEventArgs keyEventArgs: + ProcessKeyEvents(keyEventArgs); + break; + } + }); + + using (_inputManager.PreProcess.Subscribe(inputObserver)) + { + var tcs = new TaskCompletionSource(); + using (_result.Subscribe(new AnonymousObserver(tcs))) + { + var effect = await tcs.Task; return effect; } } diff --git a/src/Avalonia.Controls/Platform/PlatformManager.cs b/src/Avalonia.Controls/Platform/PlatformManager.cs index de7708e869..6f043463d4 100644 --- a/src/Avalonia.Controls/Platform/PlatformManager.cs +++ b/src/Avalonia.Controls/Platform/PlatformManager.cs @@ -1,5 +1,5 @@ using System; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Metadata; using Avalonia.Platform; @@ -26,21 +26,14 @@ namespace Avalonia.Controls.Platform public static IWindowImpl CreateWindow() { - var platform = AvaloniaLocator.Current.GetService(); - - if (platform == null) - { - throw new Exception("Could not CreateWindow(): IWindowingPlatform is not registered."); - } + var platform = AvaloniaLocator.Current.GetRequiredService(); return s_designerMode ? platform.CreateEmbeddableWindow() : platform.CreateWindow(); } public static IWindowImpl CreateEmbeddableWindow() { - var platform = AvaloniaLocator.Current.GetService(); - if (platform == null) - throw new Exception("Could not CreateEmbeddableWindow(): IWindowingPlatform is not registered."); + var platform = AvaloniaLocator.Current.GetRequiredService(); return platform.CreateEmbeddableWindow(); } } diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index 7b435e287d..0e283c3c4b 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -1,6 +1,6 @@ using System.Collections.Specialized; using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using System.Threading.Tasks; using Avalonia.Animation; using Avalonia.Controls.Primitives; diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index 1f0f2a2f67..b9f856cf39 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -1,7 +1,7 @@ using System; using System.Collections; using System.Collections.Specialized; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; using Avalonia.Input; diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 924c4567f6..96c9b7b5d9 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -77,7 +77,7 @@ namespace Avalonia.Controls.Presenters } /// - Size IScrollable.Extent => Virtualizer?.Extent ?? Size.Empty; + Size IScrollable.Extent => Virtualizer?.Extent ?? default; /// Vector IScrollable.Offset @@ -136,12 +136,12 @@ namespace Avalonia.Controls.Presenters /// protected override Size MeasureOverride(Size availableSize) { - return Virtualizer?.MeasureOverride(availableSize) ?? Size.Empty; + return Virtualizer?.MeasureOverride(availableSize) ?? default; } protected override Size ArrangeOverride(Size finalSize) { - return Virtualizer?.ArrangeOverride(finalSize) ?? Size.Empty; + return Virtualizer?.ArrangeOverride(finalSize) ?? default; } /// diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 328facba0b..0cfe4bada1 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Utilities; @@ -94,6 +92,7 @@ namespace Avalonia.Controls.Presenters { AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested); AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture); + AddHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded); this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription); } @@ -429,6 +428,8 @@ namespace Avalonia.Controls.Presenters Offset = newOffset; e.Handled = !IsScrollChainingEnabled || offsetChanged; + + e.ShouldEndScrollGesture = !IsScrollChainingEnabled && !offsetChanged; } } @@ -500,7 +501,7 @@ namespace Avalonia.Controls.Presenters if (e.OldValue != null) { - Offset = default(Vector); + Offset = default; } } @@ -542,7 +543,7 @@ namespace Avalonia.Controls.Presenters if (logicalScroll != scrollable.IsLogicalScrollEnabled) { UpdateScrollableSubscription(Child); - Offset = default(Vector); + Offset = default; InvalidateMeasure(); } else if (scrollable.IsLogicalScrollEnabled) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index cc1fa6c513..f599511392 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -106,11 +106,10 @@ namespace Avalonia.Controls.Presenters private Rect _caretBounds; private Point _navigationPosition; private string? _preeditText; - private CharacterHit _compositionStartHit = new CharacterHit(-1); static TextPresenter() { - AffectsRender(CaretBrushProperty, SelectionBrushProperty); + AffectsRender(CaretBrushProperty, SelectionBrushProperty, TextElement.ForegroundProperty); } public TextPresenter() @@ -589,6 +588,7 @@ namespace Avalonia.Controls.Presenters protected virtual void InvalidateTextLayout() { + _textLayout?.Dispose(); _textLayout = null; InvalidateMeasure(); @@ -598,6 +598,7 @@ namespace Avalonia.Controls.Presenters { _constraint = availableSize; + _textLayout?.Dispose(); _textLayout = null; InvalidateArrange(); @@ -623,6 +624,7 @@ namespace Avalonia.Controls.Presenters _constraint = new Size(Math.Ceiling(finalSize.Width), double.PositiveInfinity); + _textLayout?.Dispose(); _textLayout = null; return finalSize; diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index 7e5b34acd9..49e76d0728 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -1,7 +1,6 @@ -using System; -using System.Diagnostics.CodeAnalysis; using Avalonia.Automation.Peers; using Avalonia.Input; +using Avalonia.Reactive; using Avalonia.Media; using Avalonia.Media.TextFormatting; diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 89abe1cdaa..c9585d50ae 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Specialized; using Avalonia.Media; +using Avalonia.Reactive; using Avalonia.Rendering; using Avalonia.VisualTree; diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index cd26ea4f6e..e265f4eb6a 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Interactivity; using Avalonia.Media; @@ -123,7 +123,7 @@ namespace Avalonia.Controls.Primitives public static IPopupHost CreatePopupHost(Visual target, IAvaloniaDependencyResolver? dependencyResolver) { - var platform = (target.GetVisualRoot() as TopLevel)?.PlatformImpl?.CreatePopup(); + var platform = TopLevel.GetTopLevel(target)?.PlatformImpl?.CreatePopup(); if (platform != null) return new PopupRoot((TopLevel)target.GetVisualRoot()!, platform, dependencyResolver); diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 3c329a9a3e..7cac12eabe 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -1,6 +1,6 @@ using System; using System.ComponentModel; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Automation.Peers; using Avalonia.Controls.Mixins; using Avalonia.Controls.Diagnostics; @@ -358,7 +358,7 @@ namespace Avalonia.Controls.Primitives return; } - var topLevel = placementTarget.VisualRoot as TopLevel; + var topLevel = TopLevel.GetTopLevel(placementTarget); if (topLevel == null) { diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs index a80a60350e..62e5f37273 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs @@ -112,7 +112,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning ?? screens.FirstOrDefault(s => s.Bounds.Intersects(parentGeometry)) ?? screens.FirstOrDefault(); - if (targetScreen != null && targetScreen.WorkingArea.IsEmpty) + if (targetScreen != null && targetScreen.WorkingArea.IsDefault) { return targetScreen.Bounds; } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 44fa78ac21..795bc05e23 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -119,8 +119,6 @@ namespace Avalonia.Controls.Primitives /// public static readonly StyledProperty WrapSelectionProperty = AvaloniaProperty.Register(nameof(WrapSelection), defaultValue: false); - - private static readonly IList Empty = Array.Empty(); private string _textSearchTerm = string.Empty; private DispatcherTimer? _textSearchTimer; private ISelectionModel? _selection; diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 165bec3a95..7e0d695264 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Layout; using Avalonia.Media; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs index d0b8178add..2c29b19d48 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs @@ -3,6 +3,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.PullToRefresh; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs index f2f735aaa9..8723304f8f 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs @@ -1,12 +1,12 @@ using System; using System.Numerics; -using System.Reactive.Linq; using Avalonia.Animation.Easings; using Avalonia.Controls.Primitives; using Avalonia.Controls.PullToRefresh; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; +using Avalonia.Reactive; using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition.Animations; diff --git a/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs index c3aebc82c5..844973bd28 100644 --- a/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs +++ b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs @@ -197,12 +197,12 @@ namespace Avalonia.Controls.PullToRefresh throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content property must be a Visual"); } - if (content.Parent is not InputElement) + if (content.Parent is not InputElement parent) { throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content parent must be an InputElement"); } - MakeInteractionSource(content.Parent as InputElement); + MakeInteractionSource(parent); if (_scrollViewer != null) { diff --git a/src/Avalonia.Controls/RadioButton.cs b/src/Avalonia.Controls/RadioButton.cs index 9dbc5f040e..68da24d79f 100644 --- a/src/Avalonia.Controls/RadioButton.cs +++ b/src/Avalonia.Controls/RadioButton.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using Avalonia.Controls.Primitives; +using Avalonia.Reactive; using Avalonia.Rendering; using Avalonia.VisualTree; diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs index e24ed37f1e..56e0cda8fe 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -4,16 +4,8 @@ // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; -using System.Collections.Generic; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Threading.Tasks; -using Avalonia.Controls.Presenters; -using Avalonia.Input; using Avalonia.Layout; using Avalonia.Logging; -using Avalonia.Media; -using Avalonia.Reactive; using Avalonia.Threading; using Avalonia.VisualTree; @@ -124,22 +116,6 @@ namespace Avalonia.Controls } } } - - private Rect GetLayoutVisibleWindowDiscardAnchor() - { - var visibleWindow = _visibleWindow; - - if (HasScroller) - { - visibleWindow = new Rect( - visibleWindow.X + _layoutExtent.X + _expectedViewportShift.X + _unshiftableShift.X, - visibleWindow.Y + _layoutExtent.Y + _expectedViewportShift.Y + _unshiftableShift.Y, - visibleWindow.Width, - visibleWindow.Height); - } - - return visibleWindow; - } public Rect GetLayoutVisibleWindow() { @@ -465,7 +441,7 @@ namespace Avalonia.Controls _pendingViewportShift = default; _unshiftableShift = default; - if (_visibleWindow.IsEmpty) + if (_visibleWindow.IsDefault) { // We got cleared. _layoutExtent = default; @@ -551,7 +527,7 @@ namespace Avalonia.Controls private void TryInvalidateMeasure() { // Don't invalidate measure if we have an invalid window. - if (!_visibleWindow.IsEmpty) + if (!_visibleWindow.IsDefault) { // We invalidate measure instead of just invalidating arrange because // we don't invalidate measure in UpdateViewport if the view is changing to diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 12fb9ba5c5..503187e2d3 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -1,5 +1,5 @@ using System; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Presenters; diff --git a/src/Avalonia.Controls/SelectableTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs index b343439f98..f4c4d54951 100644 --- a/src/Avalonia.Controls/SelectableTextBlock.cs +++ b/src/Avalonia.Controls/SelectableTextBlock.cs @@ -300,7 +300,11 @@ namespace Avalonia.Controls _wordSelectionStart = SelectionStart; - SelectionEnd = StringUtils.NextWord(text, index); + if (!StringUtils.IsEndOfWord(text, index)) + { + SelectionEnd = StringUtils.NextWord(text, index); + } + break; case 3: _wordSelectionStart = -1; diff --git a/src/Avalonia.Controls/Selection/SelectedItems.cs b/src/Avalonia.Controls/Selection/SelectedItems.cs index 4fbcfde438..ef642b7bdc 100644 --- a/src/Avalonia.Controls/Selection/SelectedItems.cs +++ b/src/Avalonia.Controls/Selection/SelectedItems.cs @@ -35,9 +35,9 @@ namespace Avalonia.Controls.Selection { return _owner.SelectedItem; } - else if (Items is object) + else if (Items is not null && Ranges is not null) { - return Items[index]; + return Items[IndexRange.GetAt(Ranges, index)]; } else { diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index 1a7218ff2a..c8bf95b3f7 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -2,6 +2,7 @@ using System; using Avalonia.Collections; using Avalonia.Media; using Avalonia.Media.Immutable; +using Avalonia.Reactive; namespace Avalonia.Controls.Shapes { @@ -292,7 +293,7 @@ namespace Avalonia.Controls.Shapes return finalSize; } - return Size.Empty; + return default; } internal static (Size size, Matrix transform) CalculateSizeAndTransform(Size availableSize, Rect shapeBounds, Stretch Stretch) diff --git a/src/Avalonia.Controls/Spinner.cs b/src/Avalonia.Controls/Spinner.cs index cfcf0ee376..bf6ed060bd 100644 --- a/src/Avalonia.Controls/Spinner.cs +++ b/src/Avalonia.Controls/Spinner.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Interactivity; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index 353674b26e..31a06d875a 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index fcadafaab5..35b135e152 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView.cs @@ -6,6 +6,7 @@ using Avalonia.Media; using Avalonia.Metadata; using Avalonia.VisualTree; using System; +using Avalonia.Reactive; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.LogicalTree; diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs index cfd4234a27..6fedf2b1cd 100644 --- a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs +++ b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs @@ -1,5 +1,5 @@ using System; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Controls.Primitives; namespace Avalonia.Controls.Templates diff --git a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs index d4ecdd6cf0..a093d976f7 100644 --- a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs +++ b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs @@ -59,7 +59,7 @@ namespace Avalonia.Controls.Templates /// The untyped function. private static Func CastMatch(Func f) { - return o => (o is T) && f((T)o); + return o => o is T arg && f(arg); } /// @@ -72,7 +72,7 @@ namespace Avalonia.Controls.Templates { return (o, s) => f((T)o!, s); } - + /// /// Casts a function with a typed parameter to an untyped function. /// diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 7a0da1ecc5..31ddf1f985 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -173,13 +173,7 @@ namespace Avalonia.Controls /// /// Gets the used to render the text. /// - public TextLayout TextLayout - { - get - { - return _textLayout ??= CreateTextLayout(_text); - } - } + public TextLayout TextLayout => _textLayout ??= CreateTextLayout(_text); /// /// Gets or sets the padding to place around the . @@ -648,6 +642,7 @@ namespace Avalonia.Controls /// protected void InvalidateTextLayout() { + _textLayout?.Dispose(); _textLayout = null; InvalidateVisual(); @@ -663,6 +658,7 @@ namespace Avalonia.Controls _constraint = availableSize.Deflate(padding); + _textLayout?.Dispose(); _textLayout = null; var inlines = Inlines; @@ -726,6 +722,7 @@ namespace Avalonia.Controls _constraint = new Size(Math.Ceiling(finalSize.Deflate(padding).Width), double.PositiveInfinity); + _textLayout?.Dispose(); _textLayout = null; if (HasComplexContent) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 1bdec878d9..f388dc871e 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -2,7 +2,7 @@ using Avalonia.Input.Platform; using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; @@ -981,7 +981,7 @@ namespace Avalonia.Controls } } - length += grapheme.Text.Length; + length += grapheme.Length; } if (length < input.Length) @@ -1475,7 +1475,11 @@ namespace Avalonia.Controls _wordSelectionStart = SelectionStart; - SelectionEnd = StringUtils.NextWord(text, index); + if (!StringUtils.IsEndOfWord(text, index)) + { + SelectionEnd = StringUtils.NextWord(text, index); + } + break; case 3: _wordSelectionStart = -1; diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index bb18bf4c64..d3339e3edf 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -2,6 +2,7 @@ using System; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 2cd55dc3ab..9970a0f1c7 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -1,5 +1,5 @@ using System; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Controls.Metadata; using Avalonia.Controls.Notifications; using Avalonia.Controls.Platform; @@ -341,6 +341,16 @@ namespace Avalonia.Controls { return PlatformImpl?.PointToScreen(p) ?? default; } + + /// + /// Gets the for which the given is hosted in. + /// + /// The visual to query its TopLevel + /// The TopLevel + public static TopLevel? GetTopLevel(Visual? visual) + { + return visual == null ? null : visual.VisualRoot as TopLevel; + } /// /// Creates the layout manager for this . diff --git a/src/Avalonia.Controls/TransitioningContentControl.cs b/src/Avalonia.Controls/TransitioningContentControl.cs index 70b21b7248..2e0a36ad19 100644 --- a/src/Avalonia.Controls/TransitioningContentControl.cs +++ b/src/Avalonia.Controls/TransitioningContentControl.cs @@ -69,6 +69,15 @@ public class TransitioningContentControl : ContentControl { Dispatcher.UIThread.Post(() => UpdateContentWithTransition(Content)); } + else if (change.Property == CurrentContentProperty) + { + UpdateLogicalTree(change.OldValue, change.NewValue); + } + } + + protected override void ContentChanged(AvaloniaPropertyChangedEventArgs e) + { + // We do nothing becuse we should not remove old Content until the animation is over } /// diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index 41a1abd838..d1a7a1f727 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -6,7 +6,7 @@ using Avalonia.Collections; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Platform; using Avalonia.Platform; -using Avalonia.Utilities; +using Avalonia.Reactive; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index ffdd32f95c..f98bab7d40 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; diff --git a/src/Avalonia.Controls/Utils/AncestorFinder.cs b/src/Avalonia.Controls/Utils/AncestorFinder.cs index d2e3feb55a..f8e06ba35d 100644 --- a/src/Avalonia.Controls/Utils/AncestorFinder.cs +++ b/src/Avalonia.Controls/Utils/AncestorFinder.cs @@ -1,8 +1,5 @@ using System; -using System.Reactive; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Reactive.Subjects; +using Avalonia.Reactive; namespace Avalonia.Controls.Utils { @@ -13,7 +10,7 @@ namespace Avalonia.Controls.Utils private readonly StyledElement _control; private readonly Type _ancestorType; public IObservable Observable => _subject; - private readonly Subject _subject = new Subject(); + private readonly LightweightSubject _subject = new(); private FinderNode? _child; private IDisposable? _disposable; @@ -31,24 +28,21 @@ namespace Avalonia.Controls.Utils private void OnValueChanged(StyledElement? next) { - if (next == null || _ancestorType.IsAssignableFrom(next.GetType())) + if (next == null || _ancestorType.IsInstanceOfType(next)) _subject.OnNext(next); else { _child?.Dispose(); _child = new FinderNode(next, _ancestorType); - _child.Observable.Subscribe(OnChildValueChanged); + _child.Observable.Subscribe(_subject); _child.Init(); } } - private void OnChildValueChanged(StyledElement? control) => _subject.OnNext(control); - - public void Dispose() { _child?.Dispose(); - _subject.Dispose(); + _subject.OnCompleted(); _disposable?.Dispose(); } } @@ -61,7 +55,7 @@ namespace Avalonia.Controls.Utils public static IObservable Create(StyledElement control, Type ancestorType) { - return new AnonymousObservable(observer => + return Observable.Create(observer => { var finder = new FinderNode(control, ancestorType); var subscription = finder.Observable.Subscribe(observer); diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs index eb9f38894d..6239a5120d 100644 --- a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs +++ b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs @@ -4,7 +4,6 @@ using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; using Avalonia.Utilities; -using JetBrains.Annotations; namespace Avalonia.Controls.Utils { diff --git a/src/Avalonia.Controls/Utils/StringUtils.cs b/src/Avalonia.Controls/Utils/StringUtils.cs index b2e56434b2..6c459d7985 100644 --- a/src/Avalonia.Controls/Utils/StringUtils.cs +++ b/src/Avalonia.Controls/Utils/StringUtils.cs @@ -67,6 +67,55 @@ namespace Avalonia.Controls.Utils } } + public static bool IsEndOfWord(string text, int index) + { + if (index >= text.Length) + { + return true; + } + + var codepoint = new Codepoint(text[index]); + + if (!codepoint.IsWhiteSpace) + { + return false; + } + // A 'word' starts with an AlphaNumeric or some punctuation symbols immediately + // preceeded by lwsp. + if (index > 0) + { + var nextCodePoint = new Codepoint(text[index + 1]); + + if (nextCodePoint.IsBreakChar) + { + return true; + } + } + + switch (codepoint.GeneralCategory) + { + case GeneralCategory.LowercaseLetter: + case GeneralCategory.TitlecaseLetter: + case GeneralCategory.UppercaseLetter: + case GeneralCategory.DecimalNumber: + case GeneralCategory.LetterNumber: + case GeneralCategory.OtherNumber: + case GeneralCategory.DashPunctuation: + case GeneralCategory.InitialPunctuation: + case GeneralCategory.OpenPunctuation: + case GeneralCategory.CurrencySymbol: + case GeneralCategory.MathSymbol: + return false; + + // TODO: How do you do this in .NET? + // case UnicodeCategory.OtherPunctuation: + // // words cannot start with '.', but they can start with '&' or '*' (for example) + // return g_unichar_break_type(buffer->text[index]) == G_UNICODE_BREAK_ALPHABETIC; + default: + return true; + } + } + public static int PreviousWord(string text, int cursor) { if (string.IsNullOrEmpty(text)) diff --git a/src/Avalonia.Controls/Utils/UndoRedoHelper.cs b/src/Avalonia.Controls/Utils/UndoRedoHelper.cs index 6ff72751a6..7b663dd017 100644 --- a/src/Avalonia.Controls/Utils/UndoRedoHelper.cs +++ b/src/Avalonia.Controls/Utils/UndoRedoHelper.cs @@ -49,7 +49,7 @@ namespace Avalonia.Controls.Utils public bool TryGetLastState(out TState? _state) { - _state = default(TState); + _state = default; if (!IsLastState) return false; diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index a893c74324..a20b4eee58 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using System.Threading.Tasks; using Avalonia.Automation.Peers; using Avalonia.Controls.Platform; @@ -169,10 +169,6 @@ namespace Avalonia.Controls /// public static readonly RoutedEvent WindowOpenedEvent = RoutedEvent.Register("WindowOpened", RoutingStrategies.Direct); - - - - private readonly NameScope _nameScope = new NameScope(); private object? _dialogResult; private readonly Size _maxPlatformClientSize; private WindowStartupLocation _windowStartupLocation; @@ -235,7 +231,7 @@ namespace Avalonia.Controls impl.GotInputWhenDisabled = OnGotInputWhenDisabled; impl.WindowStateChanged = HandleWindowStateChanged; _maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size); - impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged; + impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged; this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, PlatformResizeReason.Application)); PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar); @@ -434,14 +430,14 @@ namespace Avalonia.Controls /// /// Fired before a window is closed. /// - public event EventHandler? Closing; + public event EventHandler? Closing; /// /// Closes the window. /// public void Close() { - Close(false); + CloseCore(WindowCloseReason.WindowClosing, true); } /// @@ -457,16 +453,16 @@ namespace Avalonia.Controls public void Close(object dialogResult) { _dialogResult = dialogResult; - Close(false); + CloseCore(WindowCloseReason.WindowClosing, true); } - internal void Close(bool ignoreCancel) + internal void CloseCore(WindowCloseReason reason, bool isProgrammatic) { bool close = true; try { - if (!ignoreCancel && ShouldCancelClose()) + if (ShouldCancelClose(new WindowClosingEventArgs(reason, isProgrammatic))) { close = false; } @@ -484,9 +480,10 @@ namespace Avalonia.Controls /// Handles a closing notification from . /// true if closing is cancelled. Otherwise false. /// - protected virtual bool HandleClosing() + /// The reason the window is closing. + private protected virtual bool HandleClosing(WindowCloseReason reason) { - if (!ShouldCancelClose()) + if (!ShouldCancelClose(new WindowClosingEventArgs(reason, false))) { CloseInternal(); return false; @@ -514,20 +511,22 @@ namespace Avalonia.Controls _showingAsDialog = false; } - private bool ShouldCancelClose(CancelEventArgs? args = null) + private bool ShouldCancelClose(WindowClosingEventArgs args) { - if (args is null) - { - args = new CancelEventArgs(); - } - bool canClose = true; - foreach (var (child, _) in _children.ToArray()) + if (_children.Count > 0) { - if (child.ShouldCancelClose(args)) + var childArgs = args.CloseReason == WindowCloseReason.WindowClosing ? + new WindowClosingEventArgs(WindowCloseReason.OwnerWindowClosing, args.IsProgrammatic) : + args; + + foreach (var (child, _) in _children.ToArray()) { - canClose = false; + if (child.ShouldCancelClose(childArgs)) + { + canClose = false; + } } } @@ -801,7 +800,7 @@ namespace Avalonia.Controls Renderer?.Start(); - Observable.FromEventPattern( + Observable.FromEventPattern( x => Closed += x, x => Closed -= x) .Take(1) @@ -1037,7 +1036,7 @@ namespace Avalonia.Controls /// overridden method must call on the base class if the /// event needs to be raised. /// - protected virtual void OnClosing(CancelEventArgs e) => Closing?.Invoke(this, e); + protected virtual void OnClosing(WindowClosingEventArgs e) => Closing?.Invoke(this, e); protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index b71dc6df44..aad0482b50 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -1,14 +1,9 @@ using System; using System.ComponentModel; -using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using Avalonia.Automation.Peers; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Platform; -using JetBrains.Annotations; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/WindowClosingEventArgs.cs b/src/Avalonia.Controls/WindowClosingEventArgs.cs new file mode 100644 index 0000000000..7cfae2d005 --- /dev/null +++ b/src/Avalonia.Controls/WindowClosingEventArgs.cs @@ -0,0 +1,57 @@ +using System.ComponentModel; + +namespace Avalonia.Controls +{ + /// + /// Specifies the reason that a window was closed. + /// + public enum WindowCloseReason + { + /// + /// The cause of the closure was not provided by the underlying platform. + /// + Undefined, + + /// + /// The window itself was requested to close. + /// + WindowClosing, + + /// + /// The window is closing due to a parent/owner window closing. + /// + OwnerWindowClosing, + + /// + /// The window is closing due to the application shutting down. + /// + ApplicationShutdown, + + /// + /// The window is closing due to the operating system shutting down. + /// + OSShutdown, + } + + /// + /// Provides data for the event. + /// + public class WindowClosingEventArgs : CancelEventArgs + { + internal WindowClosingEventArgs(WindowCloseReason reason, bool isProgrammatic) + { + CloseReason = reason; + IsProgrammatic = isProgrammatic; + } + + /// + /// Gets a value that indicates why the window is being closed. + /// + public WindowCloseReason CloseReason { get; } + + /// + /// Gets a value indicating whether the window is being closed programmatically. + /// + public bool IsProgrammatic { get; } + } +} diff --git a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj index 867c94b126..c5255b22cd 100644 --- a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj +++ b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj @@ -19,6 +19,5 @@ - diff --git a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs index b4cfffcdca..eff190c39e 100644 --- a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs +++ b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs @@ -18,12 +18,9 @@ namespace Avalonia.DesignerSupport Control control; using (PlatformManager.DesignerMode()) { - var loader = AvaloniaLocator.Current.GetService(); + var loader = AvaloniaLocator.Current.GetRequiredService(); var stream = new MemoryStream(Encoding.UTF8.GetBytes(xaml)); - if (loader == null) - throw new XamlLoadException("Runtime XAML loader is not registered"); - Uri baseUri = null; if (assemblyPath != null) { diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 037c5e0c71..020e09526e 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -42,7 +42,7 @@ namespace Avalonia.DesignerSupport.Remote public Action PositionChanged { get; set; } public Action Deactivated { get; set; } public Action Activated { get; set; } - public Func Closing { get; set; } + public Func Closing { get; set; } public IPlatformHandle Handle { get; } public WindowState WindowState { get; set; } public Action WindowStateChanged { get; set; } diff --git a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs index 543d07f958..85605ccd9d 100644 --- a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs +++ b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs @@ -134,12 +134,12 @@ namespace Avalonia.DesignerSupport.Remote IAvaloniaRemoteTransportConnection ConfigureApp(IAvaloniaRemoteTransportConnection transport, CommandLineArgs args, object obj); } - class AppInitializer : IAppInitializer where T : AppBuilderBase, new() + class AppInitializer : IAppInitializer { public IAvaloniaRemoteTransportConnection ConfigureApp(IAvaloniaRemoteTransportConnection transport, CommandLineArgs args, object obj) { - var builder = (AppBuilderBase)obj; + var builder = (AppBuilder)obj; if (args.Method == Methods.AvaloniaRemote) builder.UseWindowingSubsystem(() => PreviewerWindowingPlatform.Initialize(transport)); if (args.Method == Methods.Html) @@ -191,7 +191,7 @@ namespace Avalonia.DesignerSupport.Remote Log($"Obtaining AppBuilder instance from {builderMethod.DeclaringType.FullName}.{builderMethod.Name}"); var appBuilder = builderMethod.Invoke(null, null); Log($"Initializing application in design mode"); - var initializer =(IAppInitializer)Activator.CreateInstance(typeof(AppInitializer<>).MakeGenericType(appBuilder.GetType())); + var initializer =(IAppInitializer)Activator.CreateInstance(typeof(AppInitializer)); transport = initializer.ConfigureApp(transport, args, appBuilder); s_transport = transport; transport.OnMessage += OnTransportMessage; diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 80a4c7d897..3238b394fc 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.IO; -using System.Reactive.Disposables; +using Avalonia.Reactive; using System.Threading.Tasks; using Avalonia.Automation.Peers; using Avalonia.Controls; @@ -32,7 +32,7 @@ namespace Avalonia.DesignerSupport.Remote public Action Paint { get; set; } public Action Resized { get; set; } public Action ScalingChanged { get; set; } - public Func Closing { get; set; } + public Func Closing { get; set; } public Action Closed { get; set; } public Action LostFocus { get; set; } public IMouseDevice MouseDevice { get; } = new MouseDevice(); diff --git a/src/Avalonia.Desktop/AppBuilderDesktopExtensions.cs b/src/Avalonia.Desktop/AppBuilderDesktopExtensions.cs index 7c4a489328..13a4ac51d2 100644 --- a/src/Avalonia.Desktop/AppBuilderDesktopExtensions.cs +++ b/src/Avalonia.Desktop/AppBuilderDesktopExtensions.cs @@ -1,15 +1,13 @@ +using Avalonia.Compatibility; using Avalonia.Controls; -using Avalonia.Platform; +using Avalonia.Logging; namespace Avalonia { public static class AppBuilderDesktopExtensions { - public static TAppBuilder UsePlatformDetect(this TAppBuilder builder) - where TAppBuilder : AppBuilderBase, new() + public static AppBuilder UsePlatformDetect(this AppBuilder builder) { - var os = builder.RuntimePlatform.GetRuntimeInfo().OperatingSystem; - // We don't have the ability to load every assembly right now, so we are // stuck with manual configuration here // Helpers are extracted to separate methods to take the advantage of the fact @@ -17,37 +15,39 @@ namespace Avalonia // Additionally, by having a hard reference to each assembly, // we verify that the assemblies are in the final .deps.json file // so .NET Core knows where to load the assemblies from,. - if (os == OperatingSystemType.WinNT) + if (OperatingSystemEx.IsWindows()) { LoadWin32(builder); LoadSkia(builder); } - else if(os==OperatingSystemType.OSX) + else if(OperatingSystemEx.IsMacOS()) { LoadAvaloniaNative(builder); LoadSkia(builder); } - else + else if (OperatingSystemEx.IsLinux()) { LoadX11(builder); LoadSkia(builder); } + else + { + Logger.TryGet(LogEventLevel.Warning, LogArea.Platform)?.Log(builder, + "Avalonia.Desktop package was referenced on non-desktop platform or it isn't supported"); + } + return builder; } - static void LoadAvaloniaNative(TAppBuilder builder) - where TAppBuilder : AppBuilderBase, new() + static void LoadAvaloniaNative(AppBuilder builder) => builder.UseAvaloniaNative(); - static void LoadWin32(TAppBuilder builder) - where TAppBuilder : AppBuilderBase, new() + static void LoadWin32(AppBuilder builder) => builder.UseWin32(); - static void LoadX11(TAppBuilder builder) - where TAppBuilder : AppBuilderBase, new() + static void LoadX11(AppBuilder builder) => builder.UseX11(); - static void LoadSkia(TAppBuilder builder) - where TAppBuilder : AppBuilderBase, new() + static void LoadSkia(AppBuilder builder) => builder.UseSkia(); } } diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj index fe694b5730..65d1bea298 100644 --- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj +++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj @@ -23,7 +23,6 @@ - diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs index 42c5ea1fa9..00173dbb35 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs @@ -10,8 +10,7 @@ namespace Avalonia.Diagnostics.Controls { private readonly App _application; - private static readonly Version s_version = typeof(AvaloniaObject).Assembly?.GetName()?.Version - ?? Version.Parse("0.0.00"); + public event EventHandler? Closed; public Application(App application) diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs index 8eadcee6f9..e8cdcfa37b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs @@ -1,12 +1,11 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; -using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Diagnostics.Views; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; +using Avalonia.Reactive; namespace Avalonia.Diagnostics { @@ -72,7 +71,11 @@ namespace Avalonia.Diagnostics { throw new ArgumentNullException(nameof(application)); } - var result = Disposable.Empty; + + var openedDisposable = new SerialDisposableValue(); + var result = new CompositeDisposable(2); + result.Add(openedDisposable); + // Skip if call on Design Mode if (!Avalonia.Controls.Design.IsDesignMode && !s_attachedToApplication) @@ -90,13 +93,15 @@ namespace Avalonia.Diagnostics { s_attachedToApplication = true; - application.InputManager.PreProcess.OfType().Subscribe(e => + result.Add(application.InputManager.PreProcess.Subscribe(e => { - if (options.Gesture.Matches(e)) + if (e is RawKeyEventArgs keyEventArgs + && keyEventArgs.Type == RawKeyEventType.KeyUp + && options.Gesture.Matches(keyEventArgs)) { - result = Open(application, options, owner); + openedDisposable.Disposable = Open(application, options, owner); } - }); + })); } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index 5cf9e17ecf..8bff9ccde0 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -33,12 +33,12 @@ namespace Avalonia.Diagnostics.ViewModels { _avaloniaObject = avaloniaObject; - TreePage = treePage; - Layout = avaloniaObject is Visual - ? new ControlLayoutViewModel((Visual)avaloniaObject) + TreePage = treePage; + Layout = avaloniaObject is Visual visual + ? new ControlLayoutViewModel(visual) : default; - NavigateToProperty(_avaloniaObject, (_avaloniaObject as Control)?.Name ?? _avaloniaObject.ToString()); + NavigateToProperty(_avaloniaObject, (_avaloniaObject as Control)?.Name ?? _avaloniaObject.ToString()); AppliedStyles = new ObservableCollection(); PseudoClasses = new ObservableCollection(); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs index 7da43a51b7..0140281d50 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs @@ -4,6 +4,7 @@ using Avalonia.Diagnostics.Views; using Avalonia.Interactivity; using Avalonia.Threading; using Avalonia.VisualTree; +using Avalonia.Reactive; namespace Avalonia.Diagnostics.ViewModels { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs index e71f0bcaec..3048431af9 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs @@ -1,10 +1,10 @@ using System; -using System.Reactive.Disposables; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.LogicalTree; using Lifetimes = Avalonia.Controls.ApplicationLifetimes; using System.Linq; +using Avalonia.Reactive; namespace Avalonia.Diagnostics.ViewModels { @@ -84,7 +84,7 @@ namespace Avalonia.Diagnostics.ViewModels } nodes.Add(new LogicalTreeNode(window, Owner)); } - _subscriptions = new System.Reactive.Disposables.CompositeDisposable() + _subscriptions = new CompositeDisposable(2) { Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (s,e)=> { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 52535aa991..3870cad7c5 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -1,11 +1,11 @@ using System; using System.ComponentModel; -using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Diagnostics.Models; using Avalonia.Input; using Avalonia.Metadata; using Avalonia.Threading; +using Avalonia.Reactive; namespace Avalonia.Diagnostics.ViewModels { @@ -50,15 +50,15 @@ namespace Avalonia.Diagnostics.ViewModels } else { -#nullable disable - _pointerOverSubscription = InputManager.Instance.PreProcess - .OfType() + _pointerOverSubscription = InputManager.Instance!.PreProcess .Subscribe(e => { - PointerOverRoot = e.Root; - PointerOverElement = e.Root.InputHitTest(e.Position); + if (e is Input.Raw.RawPointerEventArgs pointerEventArgs) + { + PointerOverRoot = pointerEventArgs.Root; + PointerOverElement = pointerEventArgs.Root.InputHitTest(pointerEventArgs.Position); + } }); -#nullable restore } Console = new ConsoleViewModel(UpdateConsoleContext); } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index 65bfd7fc36..aafaa096e3 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Specialized; -using System.Reactive; -using System.Reactive.Linq; +using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.LogicalTree; using Avalonia.Media; +using Avalonia.Reactive; namespace Avalonia.Diagnostics.ViewModels { @@ -28,18 +28,8 @@ namespace Avalonia.Diagnostics.ViewModels { ElementName = control.Name; - var removed = Observable.FromEventPattern( - x => control.DetachedFromLogicalTree += x, - x => control.DetachedFromLogicalTree -= x); - var classesChanged = Observable.FromEventPattern< - NotifyCollectionChangedEventHandler, - NotifyCollectionChangedEventArgs>( - x => control.Classes.CollectionChanged += x, - x => control.Classes.CollectionChanged -= x) - .TakeUntil(removed); - - _classesSubscription = classesChanged.Select(_ => Unit.Default) - .StartWith(Unit.Default) + _classesSubscription = ((IObservable)control.Classes.GetWeakCollectionChangedObservable()) + .StartWith(null) .Subscribe(_ => { if (control.Classes.Count > 0) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index a1cd01c78b..f9fb0d18ef 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -1,12 +1,11 @@ using System; -using System.Reactive.Disposables; -using System.Reactive.Linq; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; using Avalonia.Styling; using Avalonia.VisualTree; +using Avalonia.Reactive; using Lifetimes = Avalonia.Controls.ApplicationLifetimes; using System.Linq; @@ -61,9 +60,12 @@ namespace Avalonia.Diagnostics.ViewModels IPopupHostProvider popupHostProvider, string? providerName = null) { - return Observable.FromEvent( - x => popupHostProvider.PopupHostChanged += x, - x => popupHostProvider.PopupHostChanged -= x) + return Observable.Create(observer => + { + void Handler(IPopupHost? args) => observer.OnNext(args); + popupHostProvider.PopupHostChanged += Handler; + return Disposable.Create(() => popupHostProvider.PopupHostChanged -= Handler); + }) .StartWith(popupHostProvider.PopupHost) .Select(popupHost => { @@ -80,29 +82,39 @@ namespace Avalonia.Diagnostics.ViewModels { Popup p => GetPopupHostObservable(p), Control c => Observable.CombineLatest( - c.GetObservable(Control.ContextFlyoutProperty), - c.GetObservable(Control.ContextMenuProperty), - c.GetObservable(FlyoutBase.AttachedFlyoutProperty), - c.GetObservable(ToolTipDiagnostics.ToolTipProperty), - c.GetObservable(Button.FlyoutProperty), - (ContextFlyout, ContextMenu, AttachedFlyout, ToolTip, ButtonFlyout) => + new IObservable[] + { + c.GetObservable(Control.ContextFlyoutProperty), + c.GetObservable(Control.ContextMenuProperty), + c.GetObservable(FlyoutBase.AttachedFlyoutProperty), + c.GetObservable(ToolTipDiagnostics.ToolTipProperty), + c.GetObservable(Button.FlyoutProperty) + }) + .Select( + items => { - if (ContextMenu != null) + var contextFlyout = items[0]; + var contextMenu = (ContextMenu?)items[1]; + var attachedFlyout = items[2]; + var toolTip = items[3]; + var buttonFlyout = items[4]; + + if (contextMenu != null) //Note: ContextMenus are special since all the items are added as visual children. //So we don't need to go via Popup - return Observable.Return(new PopupRoot(ContextMenu)); + return Observable.Return(new PopupRoot(contextMenu)); - if (ContextFlyout != null) - return GetPopupHostObservable(ContextFlyout, "ContextFlyout"); + if (contextFlyout != null) + return GetPopupHostObservable(contextFlyout, "ContextFlyout"); - if (AttachedFlyout != null) - return GetPopupHostObservable(AttachedFlyout, "AttachedFlyout"); + if (attachedFlyout != null) + return GetPopupHostObservable(attachedFlyout, "AttachedFlyout"); - if (ToolTip != null) - return GetPopupHostObservable(ToolTip, "ToolTip"); + if (toolTip != null) + return GetPopupHostObservable(toolTip, "ToolTip"); - if (ButtonFlyout != null) - return GetPopupHostObservable(ButtonFlyout, "Flyout"); + if (buttonFlyout != null) + return GetPopupHostObservable(buttonFlyout, "Flyout"); return Observable.Return(null); }) @@ -188,7 +200,7 @@ namespace Avalonia.Diagnostics.ViewModels } nodes.Add(new VisualTreeNode(window, Owner)); } - _subscriptions = new System.Reactive.Disposables.CompositeDisposable() + _subscriptions = new CompositeDisposable(2) { Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (s,e)=> { diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml.cs index c81e3cadf4..56d8737d79 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Shapes; using Avalonia.Diagnostics.Controls; using Avalonia.Markup.Xaml; using Avalonia.VisualTree; +using Avalonia.Reactive; namespace Avalonia.Diagnostics.Views { diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 13b8cf5e8a..dbc4c98f78 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; @@ -13,13 +12,13 @@ using Avalonia.Markup.Xaml; using Avalonia.Styling; using Avalonia.Themes.Simple; using Avalonia.VisualTree; +using Avalonia.Reactive; namespace Avalonia.Diagnostics.Views { internal class MainWindow : Window, IStyleHost { - private readonly IDisposable? _keySubscription; - private readonly IDisposable? _pointerSubscription; + private readonly IDisposable? _inputSubscription; private readonly Dictionary _frozenPopupStates; private AvaloniaObject? _root; private PixelPoint _lastPointerPosition; @@ -33,15 +32,19 @@ namespace Avalonia.Diagnostics.Views if (Theme is null && this.FindResource(typeof(Window)) is ControlTheme windowTheme) Theme = windowTheme; - _keySubscription = InputManager.Instance?.Process - .OfType() - .Where(x => x.Type == RawKeyEventType.KeyDown) - .Subscribe(RawKeyDown); - _pointerSubscription = InputManager.Instance?.Process - .OfType() - .Subscribe(x => _lastPointerPosition = ((Visual)x.Root).PointToScreen(x.Position)); - - + _inputSubscription = InputManager.Instance?.Process + .Subscribe(x => + { + if (x is RawPointerEventArgs pointerEventArgs) + { + _lastPointerPosition = ((Visual)x.Root).PointToScreen(pointerEventArgs.Position); + } + else if (x is RawKeyEventArgs keyEventArgs && keyEventArgs.Type == RawKeyEventType.KeyDown) + { + RawKeyDown(keyEventArgs); + } + }); + _frozenPopupStates = new Dictionary(); EventHandler? lh = default; @@ -94,8 +97,7 @@ namespace Avalonia.Diagnostics.Views protected override void OnClosed(EventArgs e) { base.OnClosed(e); - _keySubscription?.Dispose(); - _pointerSubscription?.Dispose(); + _inputSubscription?.Dispose(); foreach (var state in _frozenPopupStates) { diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs index 7d0072c6ea..df67c06b73 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs @@ -2,8 +2,7 @@ using System; using System.Collections.Specialized; using System.IO; using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Linq; +using Avalonia.Reactive; using System.Runtime.InteropServices; using Avalonia.Collections; using Avalonia.Controls; @@ -117,13 +116,11 @@ namespace Avalonia.Dialogs.Internal ?? new ManagedFileChooserSources(); var sub1 = AvaloniaLocator.Current - .GetService() + .GetRequiredService() .Listen(ManagedFileChooserSources.MountedVolumes); - var sub2 = Observable.FromEventPattern(ManagedFileChooserSources.MountedVolumes, - nameof(ManagedFileChooserSources.MountedVolumes.CollectionChanged)) - .ObserveOn(AvaloniaScheduler.Instance) - .Subscribe(x => RefreshQuickLinks(quickSources)); + var sub2 = ManagedFileChooserSources.MountedVolumes.GetWeakCollectionChangedObservable() + .Subscribe(x => Dispatcher.UIThread.Post(() => RefreshQuickLinks(quickSources))); _disposables.Add(sub1); _disposables.Add(sub2); diff --git a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs index fe2dd99100..bc5945441c 100644 --- a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs +++ b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs @@ -24,16 +24,15 @@ namespace Avalonia.Dialogs } } - public static TAppBuilder UseManagedSystemDialogs(this TAppBuilder builder) - where TAppBuilder : AppBuilderBase, new() + public static AppBuilder UseManagedSystemDialogs(this AppBuilder builder) { builder.AfterSetup(_ => AvaloniaLocator.CurrentMutable.Bind().ToSingleton>()); return builder; } - public static TAppBuilder UseManagedSystemDialogs(this TAppBuilder builder) - where TAppBuilder : AppBuilderBase, new() where TWindow : Window, new() + public static AppBuilder UseManagedSystemDialogs(this AppBuilder builder) + where TWindow : Window, new() { builder.AfterSetup(_ => AvaloniaLocator.CurrentMutable.Bind().ToSingleton>()); diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs index 791431dfa7..0f499c6066 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics; -using System.Reactive.Concurrency; using System.Reflection; using System.Threading.Tasks; using Avalonia.Input; diff --git a/src/Avalonia.FreeDesktop/DBusMenu.cs b/src/Avalonia.FreeDesktop/DBusMenu.cs index 3a1c65e7c9..7e22988270 100644 --- a/src/Avalonia.FreeDesktop/DBusMenu.cs +++ b/src/Avalonia.FreeDesktop/DBusMenu.cs @@ -36,10 +36,10 @@ namespace Avalonia.FreeDesktop.DBusMenu [Dictionary] class DBusMenuProperties { - public uint Version { get; set; } = default (uint); - public string? TextDirection { get; set; } = default (string); - public string? Status { get; set; } = default (string); - public string[]? IconThemePath { get; set; } = default (string[]); + public uint Version { get; set; } = default; + public string? TextDirection { get; set; } = default; + public string? Status { get; set; } = default; + public string[]? IconThemePath { get; set; } = default; } diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 657e324010..0e6bee5f24 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.IO; -using System.Reactive.Disposables; +using Avalonia.Reactive; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; @@ -25,9 +25,9 @@ namespace Avalonia.FreeDesktop return new DBusMenuExporterImpl(DBusHelper.Connection, xid); } - public static INativeMenuExporter TryCreateDetachedNativeMenu(ObjectPath path, Connection currentConection) + public static INativeMenuExporter TryCreateDetachedNativeMenu(ObjectPath path, Connection currentConnection) { - return new DBusMenuExporterImpl(currentConection, path); + return new DBusMenuExporterImpl(currentConnection, path); } public static ObjectPath GenerateDBusMenuObjPath => "/net/avaloniaui/dbusmenu/" diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index e37067d05c..b44762161b 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -2,7 +2,7 @@ using System; using System.Diagnostics; -using System.Reactive.Disposables; +using Avalonia.Reactive; using System.Runtime.CompilerServices; using System.Threading.Tasks; using Avalonia.Controls.Platform; diff --git a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs index 34c1506a67..9f6b056d30 100644 --- a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs +++ b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs @@ -4,11 +4,12 @@ using System.Linq; using System.Collections.Generic; using System.Collections.ObjectModel; using Avalonia.Controls.Platform; -using System.Reactive.Disposables; -using System.Reactive.Linq; +using Avalonia.Reactive; +using Avalonia.Reactive; using System.Text.RegularExpressions; using System.Runtime.InteropServices; using System.Text; +using Avalonia.Threading; namespace Avalonia.FreeDesktop { @@ -17,21 +18,17 @@ namespace Avalonia.FreeDesktop private const string DevByLabelDir = "/dev/disk/by-label/"; private const string ProcPartitionsDir = "/proc/partitions"; private const string ProcMountsDir = "/proc/mounts"; - private CompositeDisposable _disposables; + private IDisposable _disposable; private ObservableCollection _targetObs; private bool _beenDisposed = false; public LinuxMountedVolumeInfoListener(ref ObservableCollection target) { - _disposables = new CompositeDisposable(); this._targetObs = target; - var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1)) - .Subscribe(Poll); + _disposable = DispatcherTimer.Run(Poll, TimeSpan.FromSeconds(1)); - _disposables.Add(pollTimer); - - Poll(0); + Poll(); } private static string GetSymlinkTarget(string x) => Path.GetFullPath(Path.Combine(DevByLabelDir, NativeMethods.ReadLink(x))); @@ -43,7 +40,7 @@ namespace Avalonia.FreeDesktop private static string UnescapeDeviceLabel(string input) => UnescapeString(input, @"\\x([0-9a-f]{2})", 16); - private void Poll(long _) + private bool Poll() { var fProcPartitions = File.ReadAllLines(ProcPartitionsDir) .Skip(1) @@ -77,13 +74,14 @@ namespace Avalonia.FreeDesktop var mountVolInfos = q1.ToArray(); if (_targetObs.SequenceEqual(mountVolInfos)) - return; + return true; else { _targetObs.Clear(); foreach (var i in mountVolInfos) _targetObs.Add(i); + return true; } } @@ -93,7 +91,7 @@ namespace Avalonia.FreeDesktop { if (disposing) { - _disposables.Dispose(); + _disposable.Dispose(); _targetObs.Clear(); } diff --git a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs index b69ea68a76..4624a9c340 100644 --- a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs +++ b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs @@ -9,8 +9,7 @@ namespace Avalonia.FreeDesktop { public IDisposable Listen(ObservableCollection mountedDrives) { - Contract.Requires(mountedDrives != null); - return new LinuxMountedVolumeInfoListener(ref mountedDrives!); + return new LinuxMountedVolumeInfoListener(ref mountedDrives); } } } diff --git a/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs b/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs index cc7d5ef30d..b6bb69b05d 100644 --- a/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs +++ b/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs @@ -11,11 +11,10 @@ namespace Avalonia { public static class HeadlessVncPlatformExtensions { - public static int StartWithHeadlessVncPlatform( - this T builder, + public static int StartWithHeadlessVncPlatform( + this AppBuilder builder, string host, int port, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) - where T : AppBuilderBase, new() { var tcpServer = new TcpListener(host == null ? IPAddress.Loopback : IPAddress.Parse(host), port); tcpServer.Start(); diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index 8da10fa59b..301a23b608 100644 --- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -1,6 +1,6 @@ using System; using System.Diagnostics; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Input; @@ -94,8 +94,7 @@ namespace Avalonia.Headless public static class AvaloniaHeadlessPlatformExtensions { - public static T UseHeadless(this T builder, AvaloniaHeadlessPlatformOptions opts) - where T : AppBuilderBase, new() + public static AppBuilder UseHeadless(this AppBuilder builder, AvaloniaHeadlessPlatformOptions opts) { if(opts.UseHeadlessDrawing) builder.UseRenderingSubsystem(HeadlessPlatformRenderInterface.Initialize, "Headless"); diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index be0c7ed7a7..cb79cc85db 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -128,6 +128,11 @@ namespace Avalonia.Headless public void Dispose() { } + + public IReadOnlyList GetIntersections(float lowerBound, float upperBound) + { + throw new NotImplementedException(); + } } class HeadlessGeometryStub : IGeometryImpl @@ -215,7 +220,7 @@ namespace Avalonia.Headless class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl { - public HeadlessStreamingGeometryStub() : base(Rect.Empty) + public HeadlessStreamingGeometryStub() : base(default) { } diff --git a/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs b/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs index b233b46dd0..046e4645e3 100644 --- a/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs @@ -1,5 +1,5 @@ using System; -using System.Reactive.Disposables; +using Avalonia.Reactive; using System.Threading; using Avalonia.Platform; using Avalonia.Threading; diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index 725fab1eaa..8eafce208b 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -175,7 +175,7 @@ namespace Avalonia.Headless } - public Func Closing { get; set; } + public Func Closing { get; set; } class FramebufferProxy : ILockedFramebuffer { diff --git a/src/Avalonia.Native/AvaloniaNativeDragSource.cs b/src/Avalonia.Native/AvaloniaNativeDragSource.cs index bec45c2d71..7f4c462ee0 100644 --- a/src/Avalonia.Native/AvaloniaNativeDragSource.cs +++ b/src/Avalonia.Native/AvaloniaNativeDragSource.cs @@ -6,7 +6,6 @@ using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Native.Interop; -using Avalonia.VisualTree; namespace Avalonia.Native { @@ -26,7 +25,7 @@ namespace Avalonia.Native if (element == null) return null; var visual = (Visual)element; - return visual.GetVisualRoot() as TopLevel; + return TopLevel.GetTopLevel(visual); } class DndCallback : NativeCallbackBase, IAvnDndResultCallback diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index a5b2ea30cc..6d5925c0ae 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -9,23 +9,23 @@ using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Composition; -using JetBrains.Annotations; using MicroCom.Runtime; +#nullable enable namespace Avalonia.Native { class AvaloniaNativePlatform : IWindowingPlatform { private readonly IAvaloniaNativeFactory _factory; - private AvaloniaNativePlatformOptions _options; - private AvaloniaNativeGlPlatformGraphics _platformGl; + private AvaloniaNativePlatformOptions? _options; + private AvaloniaNativeGlPlatformGraphics? _platformGl; [DllImport("libAvaloniaNative")] static extern IntPtr CreateAvaloniaNative(); internal static readonly KeyboardDevice KeyboardDevice = new KeyboardDevice(); - [CanBeNull] internal static Compositor Compositor { get; private set; } - [CanBeNull] internal static PlatformRenderInterfaceContextManager RenderInterface { get; private set; } + internal static Compositor? Compositor { get; private set; } + internal static PlatformRenderInterfaceContextManager? RenderInterface { get; private set; } public static AvaloniaNativePlatform Initialize(IntPtr factory, AvaloniaNativePlatformOptions options) { @@ -63,7 +63,7 @@ namespace Avalonia.Native public void SetupApplicationName() { - if (!string.IsNullOrWhiteSpace(Application.Current.Name)) + if (!string.IsNullOrWhiteSpace(Application.Current!.Name)) { _factory.MacOptions.SetApplicationTitle(Application.Current.Name); } @@ -118,10 +118,13 @@ namespace Avalonia.Native AvaloniaLocator.CurrentMutable.Bind().ToConstant(renderLoop); var hotkeys = AvaloniaLocator.Current.GetService(); - hotkeys.MoveCursorToTheStartOfLine.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers)); - hotkeys.MoveCursorToTheStartOfLineWithSelection.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers | hotkeys.SelectionModifiers)); - hotkeys.MoveCursorToTheEndOfLine.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers)); - hotkeys.MoveCursorToTheEndOfLineWithSelection.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers | hotkeys.SelectionModifiers)); + if (hotkeys is not null) + { + hotkeys.MoveCursorToTheStartOfLine.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers)); + hotkeys.MoveCursorToTheStartOfLineWithSelection.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers | hotkeys.SelectionModifiers)); + hotkeys.MoveCursorToTheEndOfLine.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers)); + hotkeys.MoveCursorToTheEndOfLineWithSelection.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers | hotkeys.SelectionModifiers)); + } if (_options.UseGpu) { diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs index 189f45d7c8..6613fc09be 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs @@ -6,8 +6,7 @@ namespace Avalonia { public static class AvaloniaNativePlatformExtensions { - public static T UseAvaloniaNative(this T builder) - where T : AppBuilderBase, new() + public static AppBuilder UseAvaloniaNative(this AppBuilder builder) { builder.UseWindowingSubsystem(() => { diff --git a/src/Avalonia.Native/ClipboardImpl.cs b/src/Avalonia.Native/ClipboardImpl.cs index 4ee590516b..9f1c8883aa 100644 --- a/src/Avalonia.Native/ClipboardImpl.cs +++ b/src/Avalonia.Native/ClipboardImpl.cs @@ -15,8 +15,7 @@ namespace Avalonia.Native private IAvnClipboard _native; private const string NSPasteboardTypeString = "public.utf8-plain-text"; private const string NSFilenamesPboardType = "NSFilenamesPboardType"; - private const string NSPasteboardTypeFileUrl = "public.file-url"; - + public ClipboardImpl(IAvnClipboard native) { _native = native; diff --git a/src/Avalonia.Native/Helpers.cs b/src/Avalonia.Native/Helpers.cs index 764ff789dc..6270cd30a0 100644 --- a/src/Avalonia.Native/Helpers.cs +++ b/src/Avalonia.Native/Helpers.cs @@ -1,5 +1,4 @@ using Avalonia.Native.Interop; -using JetBrains.Annotations; namespace Avalonia.Native { diff --git a/src/Avalonia.Native/IAvnMenu.cs b/src/Avalonia.Native/IAvnMenu.cs index 3e46e0c5c6..515835a3bd 100644 --- a/src/Avalonia.Native/IAvnMenu.cs +++ b/src/Avalonia.Native/IAvnMenu.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Controls; namespace Avalonia.Native.Interop @@ -46,7 +46,6 @@ namespace Avalonia.Native.Interop.Impl private AvaloniaNativeMenuExporter _exporter; private List<__MicroComIAvnMenuItemProxy> _menuItems = new List<__MicroComIAvnMenuItemProxy>(); private Dictionary _menuItemLookup = new Dictionary(); - private CompositeDisposable _propertyDisposables = new CompositeDisposable(); public void RaiseNeedsUpdate() { diff --git a/src/Avalonia.Native/IAvnMenuItem.cs b/src/Avalonia.Native/IAvnMenuItem.cs index de3be6142e..424d2c7310 100644 --- a/src/Avalonia.Native/IAvnMenuItem.cs +++ b/src/Avalonia.Native/IAvnMenuItem.cs @@ -1,6 +1,6 @@ using System; using System.IO; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Controls; using Avalonia.Media.Imaging; using Avalonia.Platform.Interop; diff --git a/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs b/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs index 92b2915e2e..6f45baeec2 100644 --- a/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs +++ b/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs @@ -2,15 +2,16 @@ using System; using System.Collections.ObjectModel; using System.IO; using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Linq; +using Avalonia.Reactive; +using Avalonia.Reactive; using Avalonia.Controls.Platform; +using Avalonia.Threading; namespace Avalonia.Native { internal class MacOSMountedVolumeInfoListener : IDisposable { - private readonly CompositeDisposable _disposables; + private readonly IDisposable _disposable; private bool _beenDisposed = false; private ObservableCollection mountedDrives; @@ -18,17 +19,12 @@ namespace Avalonia.Native { this.mountedDrives = mountedDrives; - _disposables = new CompositeDisposable(); + _disposable = DispatcherTimer.Run(Poll, TimeSpan.FromSeconds(1)); - var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1)) - .Subscribe(Poll); - - _disposables.Add(pollTimer); - - Poll(0); + Poll(); } - private void Poll(long _) + private bool Poll() { var mountVolInfos = Directory.GetDirectories("/Volumes/") .Where(p=> p != null) @@ -41,13 +37,14 @@ namespace Avalonia.Native .ToArray(); if (mountedDrives.SequenceEqual(mountVolInfos)) - return; + return true; else { mountedDrives.Clear(); foreach (var i in mountVolInfos) mountedDrives.Add(i); + return true; } } @@ -72,7 +69,6 @@ namespace Avalonia.Native { public IDisposable Listen(ObservableCollection mountedDrives) { - Contract.Requires(mountedDrives != null); return new MacOSMountedVolumeInfoListener(mountedDrives); } } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 2201503168..880a385744 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -48,7 +48,7 @@ namespace Avalonia.Native { if (_parent.Closing != null) { - return _parent.Closing().AsComBool(); + return _parent.Closing(WindowCloseReason.WindowClosing).AsComBool(); } return true.AsComBool(); @@ -207,7 +207,7 @@ namespace Avalonia.Native // NO OP on OSX } - public Func Closing { get; set; } + public Func Closing { get; set; } public ITopLevelNativeMenuExporter NativeMenuExporter { get; } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 5d66590a2b..ca57e30b1c 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -63,7 +63,6 @@ namespace Avalonia.Native private double _savedScaling; private GlPlatformSurface _glSurface; private NativeControlHostImpl _nativeControlHost; - private IGlContext _glContext; internal WindowBaseImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, AvaloniaNativeGlPlatformGraphics glFeature) diff --git a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj index a9136c2c99..9b38c150fa 100644 --- a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj +++ b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj @@ -13,4 +13,9 @@ + + + + + diff --git a/src/Avalonia.OpenGL/Egl/EglContext.cs b/src/Avalonia.OpenGL/Egl/EglContext.cs index 4d75a776c3..0b4b7d26a1 100644 --- a/src/Avalonia.OpenGL/Egl/EglContext.cs +++ b/src/Avalonia.OpenGL/Egl/EglContext.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; using System.Threading; using Avalonia.Platform; +using Avalonia.Reactive; using static Avalonia.OpenGL.Egl.EglConsts; namespace Avalonia.OpenGL.Egl diff --git a/src/Avalonia.OpenGL/Egl/EglDisplay.cs b/src/Avalonia.OpenGL/Egl/EglDisplay.cs index eea2587587..c67d7674a8 100644 --- a/src/Avalonia.OpenGL/Egl/EglDisplay.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplay.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Disposables; using System.Threading; +using Avalonia.Reactive; using static Avalonia.OpenGL.Egl.EglConsts; namespace Avalonia.OpenGL.Egl diff --git a/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs b/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs index fbfaf1bd3d..5573eb39fa 100644 --- a/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Disposables; using System.Threading; using static Avalonia.OpenGL.Egl.EglConsts; namespace Avalonia.OpenGL.Egl; @@ -132,4 +131,4 @@ class EglConfigInfo SampleCount = sampleCount; StencilSize = stencilSize; } -} \ No newline at end of file +} diff --git a/src/Avalonia.OpenGL/Egl/EglInterface.cs b/src/Avalonia.OpenGL/Egl/EglInterface.cs index ad4b55a686..06aafb4f57 100644 --- a/src/Avalonia.OpenGL/Egl/EglInterface.cs +++ b/src/Avalonia.OpenGL/Egl/EglInterface.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using Avalonia.Compatibility; using Avalonia.Platform; using Avalonia.Platform.Interop; using Avalonia.SourceGenerator; @@ -24,10 +25,9 @@ namespace Avalonia.OpenGL.Egl static Func Load() { - var os = AvaloniaLocator.Current.GetService().GetRuntimeInfo().OperatingSystem; - if(os == OperatingSystemType.Linux) + if(OperatingSystemEx.IsLinux()) return Load("libEGL.so.1"); - if (os == OperatingSystemType.Android) + if (OperatingSystemEx.IsAndroid()) return Load("libEGL.so"); throw new PlatformNotSupportedException(); @@ -35,7 +35,7 @@ namespace Avalonia.OpenGL.Egl static Func Load(string library) { - var dyn = AvaloniaLocator.Current.GetService(); + var dyn = AvaloniaLocator.Current.GetRequiredService(); var lib = dyn.LoadLibrary(library); return (s) => dyn.GetProcAddress(lib, s, true); } diff --git a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs index 359da3d7c2..3fde580160 100644 --- a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs +++ b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs @@ -12,8 +12,7 @@ namespace Avalonia.ReactiveUI /// scheduler, an activation for view fetcher, a template binding hook. Remember /// to call this method if you are using ReactiveUI in your application. /// - public static TAppBuilder UseReactiveUI(this TAppBuilder builder) - where TAppBuilder : AppBuilderBase, new() => + public static AppBuilder UseReactiveUI(this AppBuilder builder) => builder.AfterPlatformServicesSetup(_ => Locator.RegisterResolverCallbackChanged(() => { if (Locator.CurrentMutable is null) diff --git a/src/Avalonia.ReactiveUI/Attributes.cs b/src/Avalonia.ReactiveUI/Attributes.cs index 31c374392f..1b85cd2f47 100644 --- a/src/Avalonia.ReactiveUI/Attributes.cs +++ b/src/Avalonia.ReactiveUI/Attributes.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using Avalonia.Metadata; [assembly: XmlnsDefinition("http://reactiveui.net", "Avalonia.ReactiveUI")] \ No newline at end of file diff --git a/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs b/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs index bd21503a58..ae9a8787bc 100644 --- a/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs +++ b/src/Avalonia.ReactiveUI/AutoSuspendHelper.cs @@ -2,10 +2,10 @@ using Avalonia; using Avalonia.VisualTree; using Avalonia.Controls; using System.Threading; +using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Subjects; using System.Reactive.Linq; -using System.Reactive; using ReactiveUI; using System; using Avalonia.Controls.ApplicationLifetimes; diff --git a/src/Avalonia.ReactiveUI/AvaloniaObjectReactiveExtensions.cs b/src/Avalonia.ReactiveUI/AvaloniaObjectReactiveExtensions.cs new file mode 100644 index 0000000000..4cd329beb6 --- /dev/null +++ b/src/Avalonia.ReactiveUI/AvaloniaObjectReactiveExtensions.cs @@ -0,0 +1,110 @@ +using System.Reactive; +using System.Reactive.Subjects; +using Avalonia.Data; + +namespace Avalonia.ReactiveUI; + +public static class AvaloniaObjectReactiveExtensions +{ + /// + /// Gets a subject for an . + /// + /// The object. + /// The property. + /// + /// The priority with which binding values are written to the object. + /// + /// + /// An which can be used for two-way binding to/from the + /// property. + /// + public static ISubject GetSubject( + this AvaloniaObject o, + AvaloniaProperty property, + BindingPriority priority = BindingPriority.LocalValue) + { + return Subject.Create( + Observer.Create(x => o.SetValue(property, x, priority)), + o.GetObservable(property)); + } + + /// + /// Gets a subject for an . + /// + /// The property type. + /// The object. + /// The property. + /// + /// The priority with which binding values are written to the object. + /// + /// + /// An which can be used for two-way binding to/from the + /// property. + /// + public static ISubject GetSubject( + this AvaloniaObject o, + AvaloniaProperty property, + BindingPriority priority = BindingPriority.LocalValue) + { + return Subject.Create( + Observer.Create(x => o.SetValue(property, x, priority)), + o.GetObservable(property)); + } + + /// + /// Gets a subject for a . + /// + /// The object. + /// The property. + /// + /// The priority with which binding values are written to the object. + /// + /// + /// An which can be used for two-way binding to/from the + /// property. + /// + public static ISubject> GetBindingSubject( + this AvaloniaObject o, + AvaloniaProperty property, + BindingPriority priority = BindingPriority.LocalValue) + { + return Subject.Create>( + Observer.Create>(x => + { + if (x.HasValue) + { + o.SetValue(property, x.Value, priority); + } + }), + o.GetBindingObservable(property)); + } + + /// + /// Gets a subject for a . + /// + /// The property type. + /// The object. + /// The property. + /// + /// The priority with which binding values are written to the object. + /// + /// + /// An which can be used for two-way binding to/from the + /// property. + /// + public static ISubject> GetBindingSubject( + this AvaloniaObject o, + AvaloniaProperty property, + BindingPriority priority = BindingPriority.LocalValue) + { + return Subject.Create>( + Observer.Create>(x => + { + if (x.HasValue) + { + o.SetValue(property, x.Value, priority); + } + }), + o.GetBindingObservable(property)); + } +} diff --git a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs b/src/Avalonia.ReactiveUI/AvaloniaScheduler.cs similarity index 98% rename from src/Avalonia.Base/Threading/AvaloniaScheduler.cs rename to src/Avalonia.ReactiveUI/AvaloniaScheduler.cs index 6423d86e7c..61c503e7a5 100644 --- a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs +++ b/src/Avalonia.ReactiveUI/AvaloniaScheduler.cs @@ -1,8 +1,9 @@ using System; using System.Reactive.Concurrency; using System.Reactive.Disposables; +using Avalonia.Threading; -namespace Avalonia.Threading +namespace Avalonia.ReactiveUI { /// /// A reactive scheduler that uses Avalonia's . diff --git a/src/Avalonia.Remote.Protocol/MetsysBson.cs b/src/Avalonia.Remote.Protocol/MetsysBson.cs index d011a963be..c0263b3518 100644 --- a/src/Avalonia.Remote.Protocol/MetsysBson.cs +++ b/src/Avalonia.Remote.Protocol/MetsysBson.cs @@ -1370,7 +1370,7 @@ namespace Metsys.Bson var pattern = ReadName(); var optionsString = ReadName(); - var options = RegexOptions.None; + var options = RegexOptions.Compiled; if (optionsString.Contains('e')) options = options | RegexOptions.ECMAScript; if (optionsString.Contains('i')) options = options | RegexOptions.IgnoreCase; if (optionsString.Contains('l')) options = options | RegexOptions.CultureInvariant; diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml index 89841c92c0..0192fb1b54 100644 --- a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml @@ -36,6 +36,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml index 89b646fb52..a9e5ed949a 100644 --- a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml @@ -36,6 +36,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj index ab543703d9..fb52b3b147 100644 --- a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj +++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj @@ -14,7 +14,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml b/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml index 9abcf5d32b..28f8649e2d 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml @@ -50,7 +50,8 @@ Opacity="0" Fill="{DynamicResource CheckBoxCheckGlyphForegroundUnchecked}" Stretch="Uniform" - VerticalAlignment="Center" /> + VerticalAlignment="Center" + FlowDirection="LeftToRight" /> @@ -146,7 +147,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml index d1707d0af2..63b3d46a41 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml @@ -153,9 +153,13 @@ - + @@ -322,7 +326,6 @@ diff --git a/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj b/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj index 1dd6426b39..4aa6b66743 100644 --- a/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj +++ b/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj @@ -13,7 +13,6 @@ - diff --git a/src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml b/src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml index 61dae9b445..5a1fa4ec6d 100644 --- a/src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml +++ b/src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml @@ -11,6 +11,12 @@ + + + + @@ -54,6 +60,14 @@ + + + diff --git a/src/Avalonia.Themes.Simple/Controls/NativeMenuBar.xaml b/src/Avalonia.Themes.Simple/Controls/NativeMenuBar.xaml index ba1b35e2ee..945622f22f 100644 --- a/src/Avalonia.Themes.Simple/Controls/NativeMenuBar.xaml +++ b/src/Avalonia.Themes.Simple/Controls/NativeMenuBar.xaml @@ -9,17 +9,16 @@ - - diff --git a/src/Avalonia.X11/Avalonia.X11.csproj b/src/Avalonia.X11/Avalonia.X11.csproj index c762349c1c..061c516ef6 100644 --- a/src/Avalonia.X11/Avalonia.X11.csproj +++ b/src/Avalonia.X11/Avalonia.X11.csproj @@ -10,7 +10,12 @@ - - - + + + + + + + + diff --git a/src/Avalonia.X11/Glx/GlxContext.cs b/src/Avalonia.X11/Glx/GlxContext.cs index def5228e94..88006f0b47 100644 --- a/src/Avalonia.X11/Glx/GlxContext.cs +++ b/src/Avalonia.X11/Glx/GlxContext.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; using System.Threading; using Avalonia.OpenGL; +using Avalonia.Reactive; namespace Avalonia.X11.Glx { class GlxContext : IGlContext diff --git a/src/Avalonia.X11/NativeDialogs/Gtk.cs b/src/Avalonia.X11/NativeDialogs/Gtk.cs index d5eae037a9..4e56ae73cf 100644 --- a/src/Avalonia.X11/NativeDialogs/Gtk.cs +++ b/src/Avalonia.X11/NativeDialogs/Gtk.cs @@ -3,7 +3,6 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Avalonia.Platform.Interop; -using JetBrains.Annotations; // ReSharper disable IdentifierTypo namespace Avalonia.X11.NativeDialogs diff --git a/src/Avalonia.X11/X11Clipboard.cs b/src/Avalonia.X11/X11Clipboard.cs index d428d82744..2aa7797067 100644 --- a/src/Avalonia.X11/X11Clipboard.cs +++ b/src/Avalonia.X11/X11Clipboard.cs @@ -18,8 +18,6 @@ namespace Avalonia.X11 private TaskCompletionSource _requestedDataTcs; private readonly IntPtr[] _textAtoms; private readonly IntPtr _avaloniaSaveTargetsAtom; - private readonly Dictionary _formatAtoms = new Dictionary(); - private readonly Dictionary _atomFormats = new Dictionary(); public X11Clipboard(AvaloniaX11Platform platform) { diff --git a/src/Avalonia.X11/X11Framebuffer.cs b/src/Avalonia.X11/X11Framebuffer.cs index 94f930e9ec..a9fedff8b5 100644 --- a/src/Avalonia.X11/X11Framebuffer.cs +++ b/src/Avalonia.X11/X11Framebuffer.cs @@ -25,7 +25,7 @@ namespace Avalonia.X11 RowBytes = width * 4; Dpi = new Vector(96, 96) * factor; Format = PixelFormat.Bgra8888; - _blob = AvaloniaLocator.Current.GetService().AllocBlob(RowBytes * height); + _blob = AvaloniaLocator.Current.GetRequiredService().AllocBlob(RowBytes * height); Address = _blob.Address; } diff --git a/src/Avalonia.X11/X11Info.cs b/src/Avalonia.X11/X11Info.cs index 13dc460f45..72f3bf3137 100644 --- a/src/Avalonia.X11/X11Info.cs +++ b/src/Avalonia.X11/X11Info.cs @@ -2,7 +2,6 @@ using System; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; -using JetBrains.Annotations; using static Avalonia.X11.XLib; // ReSharper disable UnusedAutoPropertyAccessor.Local namespace Avalonia.X11 diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index e44b5ded14..0f9ef72a3f 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -105,7 +105,7 @@ namespace Avalonia.X11 var gl = AvaloniaLocator.Current.GetService(); if (options.UseCompositor) - Compositor = new Compositor(AvaloniaLocator.Current.GetService()!, gl); + Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), gl); else RenderInterface = new(gl); @@ -303,7 +303,7 @@ namespace Avalonia } public static class AvaloniaX11PlatformExtensions { - public static T UseX11(this T builder) where T : AppBuilderBase, new() + public static AppBuilder UseX11(this AppBuilder builder) { builder.UseWindowingSubsystem(() => new AvaloniaX11Platform().Initialize(AvaloniaLocator.Current.GetService() ?? diff --git a/src/Avalonia.X11/X11Screens.cs b/src/Avalonia.X11/X11Screens.cs index ba6029b350..87899b11ed 100644 --- a/src/Avalonia.X11/X11Screens.cs +++ b/src/Avalonia.X11/X11Screens.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Runtime.InteropServices; using Avalonia.Platform; using static Avalonia.X11.XLib; -using JetBrains.Annotations; namespace Avalonia.X11 { diff --git a/src/Avalonia.X11/X11Window.Ime.cs b/src/Avalonia.X11/X11Window.Ime.cs index 257580a5ec..e6066c7964 100644 --- a/src/Avalonia.X11/X11Window.Ime.cs +++ b/src/Avalonia.X11/X11Window.Ime.cs @@ -7,7 +7,6 @@ using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; using Avalonia.Platform.Interop; -using JetBrains.Annotations; using static Avalonia.X11.XLib; namespace Avalonia.X11 @@ -206,7 +205,7 @@ namespace Avalonia.X11 // This class is used to attach the text value of the key to an asynchronously dispatched KeyDown event class RawKeyEventArgsWithText : RawKeyEventArgs { - public RawKeyEventArgsWithText([NotNull] IKeyboardDevice device, ulong timestamp, [NotNull] IInputRoot root, + public RawKeyEventArgsWithText(IKeyboardDevice device, ulong timestamp, IInputRoot root, RawKeyEventType type, Key key, RawInputModifiers modifiers, string text) : base(device, timestamp, root, type, key, modifiers) { diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 160401c5fc..5bbcf3af8c 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Avalonia.Reactive; using System.Text; using System.Threading.Tasks; using System.Threading; @@ -57,6 +58,7 @@ namespace Avalonia.X11 private TransparencyHelper _transparencyHelper; private RawEventGrouper _rawEventGrouper; private bool _useRenderWindow = false; + private bool _usePositioningFlags = false; enum XSyncState { @@ -298,7 +300,10 @@ namespace Avalonia.X11 min_height = min.Height }; hints.height_inc = hints.width_inc = 1; - var flags = XSizeHintsFlags.PMinSize | XSizeHintsFlags.PResizeInc | XSizeHintsFlags.PPosition | XSizeHintsFlags.PSize; + var flags = XSizeHintsFlags.PMinSize | XSizeHintsFlags.PResizeInc; + if (_usePositioningFlags) + flags |= XSizeHintsFlags.PPosition | XSizeHintsFlags.PSize; + // People might be passing double.MaxValue if (max.Width < 100000 && max.Height < 100000) { @@ -354,7 +359,7 @@ namespace Avalonia.X11 public Action ScalingChanged { get; set; } public Action Deactivated { get; set; } public Action Activated { get; set; } - public Func Closing { get; set; } + public Func Closing { get; set; } public Action WindowStateChanged { get; set; } public Action TransparencyLevelChanged @@ -542,7 +547,7 @@ namespace Avalonia.X11 { if (ev.ClientMessageEvent.ptr1 == _x11.Atoms.WM_DELETE_WINDOW) { - if (Closing?.Invoke() != true) + if (Closing?.Invoke(WindowCloseReason.WindowClosing) != true) Dispose(); } else if (ev.ClientMessageEvent.ptr1 == _x11.Atoms._NET_WM_SYNC_REQUEST) @@ -958,6 +963,12 @@ namespace Avalonia.X11 get => _position ?? default; set { + if(!_usePositioningFlags) + { + _usePositioningFlags = true; + UpdateSizeHints(null); + } + var changes = new XWindowChanges { x = (int)value.X, diff --git a/src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj b/src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj index a2f1b55b6f..a9cad0538f 100644 --- a/src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj +++ b/src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Browser/Avalonia.Browser.Blazor/AvaloniaView.cs b/src/Browser/Avalonia.Browser.Blazor/AvaloniaView.cs index 2cc74273c0..68efea31d6 100644 --- a/src/Browser/Avalonia.Browser.Blazor/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser.Blazor/AvaloniaView.cs @@ -8,9 +8,10 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Rendering; using BrowserView = Avalonia.Browser.AvaloniaView; +[assembly: SupportedOSPlatform("browser")] + namespace Avalonia.Browser.Blazor; -[SupportedOSPlatform("browser")] public class AvaloniaView : ComponentBase { private Browser.AvaloniaView? _browserView; diff --git a/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs b/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs index 2432dc29a3..a7bb5a62df 100644 --- a/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs +++ b/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs @@ -5,17 +5,15 @@ using Avalonia.Controls.ApplicationLifetimes; namespace Avalonia.Browser.Blazor; -[SupportedOSPlatform("browser")] public static class WebAppBuilder { - public static T SetupWithSingleViewLifetime( - this T builder) - where T : AppBuilderBase, new() + public static AppBuilder SetupWithSingleViewLifetime( + this AppBuilder builder) { return builder.SetupWithLifetime(new BlazorSingleViewLifetime()); } - public static T UseBlazor(this T builder) where T : AppBuilderBase, new() + public static AppBuilder UseBlazor(this AppBuilder builder) { return builder .UseBrowser() diff --git a/src/Browser/Avalonia.Browser/Avalonia.Browser.csproj b/src/Browser/Avalonia.Browser/Avalonia.Browser.csproj index 014d387cb2..2fc3cc885a 100644 --- a/src/Browser/Avalonia.Browser/Avalonia.Browser.csproj +++ b/src/Browser/Avalonia.Browser/Avalonia.Browser.csproj @@ -1,7 +1,6 @@ net7.0 - preview enable true diff --git a/src/Browser/Avalonia.Browser/AvaloniaView.cs b/src/Browser/Avalonia.Browser/AvaloniaView.cs index abda618b0d..294216ee03 100644 --- a/src/Browser/Avalonia.Browser/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser/AvaloniaView.cs @@ -18,7 +18,6 @@ using SkiaSharp; namespace Avalonia.Browser { - [System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings public partial class AvaloniaView : ITextInputMethodImpl { private static readonly PooledList s_intermediatePointsPooledList = new(ClearMode.Never); @@ -107,7 +106,7 @@ namespace Avalonia.Browser _dpi = DomHelper.ObserveDpi(OnDpiChanged); - _useGL = AvaloniaLocator.Current.GetRequiredService() != null; + _useGL = AvaloniaLocator.Current.GetService() != null; if (_useGL) { diff --git a/src/Browser/Avalonia.Browser/BrowserNativeControlHost.cs b/src/Browser/Avalonia.Browser/BrowserNativeControlHost.cs index 7e91d29019..c2e54c7ed7 100644 --- a/src/Browser/Avalonia.Browser/BrowserNativeControlHost.cs +++ b/src/Browser/Avalonia.Browser/BrowserNativeControlHost.cs @@ -55,12 +55,6 @@ namespace Avalonia.Browser private class Attachment : INativeControlHostControlTopLevelAttachment { - private const string InitializeWithChildHandleSymbol = "InitializeWithChildHandle"; - private const string AttachToSymbol = "AttachTo"; - private const string ShowInBoundsSymbol = "ShowInBounds"; - private const string HideWithSizeSymbol = "HideWithSize"; - private const string ReleaseChildSymbol = "ReleaseChild"; - private JSObject? _native; private BrowserNativeControlHost? _attachedTo; diff --git a/src/Browser/Avalonia.Browser/BrowserRuntimePlatform.cs b/src/Browser/Avalonia.Browser/BrowserRuntimePlatform.cs index 0abc7703da..67bb040410 100644 --- a/src/Browser/Avalonia.Browser/BrowserRuntimePlatform.cs +++ b/src/Browser/Avalonia.Browser/BrowserRuntimePlatform.cs @@ -11,12 +11,11 @@ internal class BrowserRuntimePlatform : StandardRuntimePlatform { private static readonly Lazy Info = new(() => { + var isMobile = AvaloniaModule.IsMobile(); var result = new RuntimePlatformInfo { - IsCoreClr = true, // WASM browser is always CoreCLR - IsBrowser = true, // BrowserRuntimePlatform only runs on Browser. - OperatingSystem = OperatingSystemType.Browser, - IsMobile = AvaloniaModule.IsMobile() + IsMobile = isMobile, + IsDesktop = !isMobile }; return result; diff --git a/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs b/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs index b6c766b2a5..add69760ee 100644 --- a/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs +++ b/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs @@ -2,12 +2,9 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using System.Runtime.Versioning; -using Avalonia.Browser.Skia; -using Avalonia.Platform; namespace Avalonia.Browser; -[SupportedOSPlatform("browser")] public class BrowserSingleViewLifetime : ISingleViewApplicationLifetime { public AvaloniaView? View; @@ -24,12 +21,10 @@ public class BrowserPlatformOptions public Func FrameworkAssetPathResolver { get; set; } = new(fileName => $"./{fileName}"); } -[SupportedOSPlatform("browser")] public static class WebAppBuilder { - public static T SetupBrowserApp( - this T builder, string mainDivId) - where T : AppBuilderBase, new() + public static AppBuilder SetupBrowserApp( + this AppBuilder builder, string mainDivId) { var lifetime = new BrowserSingleViewLifetime(); @@ -42,9 +37,8 @@ public static class WebAppBuilder .SetupWithLifetime(lifetime); } - public static T UseBrowser( - this T builder) - where T : AppBuilderBase, new() + public static AppBuilder UseBrowser( + this AppBuilder builder) { return builder .UseWindowingSubsystem(BrowserWindowingPlatform.Register) diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs index 85725e4cf3..4e6be3268b 100644 --- a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs +++ b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Runtime.Versioning; using Avalonia.Browser.Skia; using Avalonia.Browser.Storage; using Avalonia.Controls; @@ -14,9 +15,10 @@ using Avalonia.Platform.Storage; using Avalonia.Rendering; using Avalonia.Rendering.Composition; +[assembly: SupportedOSPlatform("browser")] + namespace Avalonia.Browser { - [System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings internal class BrowserTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider { private Size _clientSize; @@ -201,7 +203,11 @@ namespace Avalonia.Browser public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { - + if (transparencyLevel == WindowTransparencyLevel.None + || transparencyLevel == WindowTransparencyLevel.Transparent) + { + TransparencyLevel = transparencyLevel; + } } public Size ClientSize => _clientSize; @@ -221,7 +227,7 @@ namespace Avalonia.Browser public IMouseDevice MouseDevice { get; } = new MouseDevice(); public IKeyboardDevice KeyboardDevice { get; } = BrowserWindowingPlatform.Keyboard; - public WindowTransparencyLevel TransparencyLevel { get; } + public WindowTransparencyLevel TransparencyLevel { get; private set; } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } public ITextInputMethodImpl TextInputMethod => _avaloniaView; diff --git a/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs b/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs index 8321b00658..27a2b1dcb7 100644 --- a/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs @@ -6,7 +6,6 @@ namespace Avalonia.Browser.Interop; internal record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth); -[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings internal static partial class CanvasHelper { diff --git a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs index c44af810d1..e50f8790ef 100644 --- a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs @@ -45,7 +45,7 @@ internal static partial class StorageHelper [JSImport("StorageItem.getItems", AvaloniaModule.StorageModuleName)] [return: JSMarshalAs>] - public static partial Task GetItems(JSObject item); + public static partial Task GetItems(JSObject item); [JSImport("StorageItems.itemsArray", AvaloniaModule.StorageModuleName)] public static partial JSObject[] ItemsArray(JSObject item); diff --git a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs index e56efdb4a8..4d3d8dcd97 100644 --- a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs +++ b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; using Avalonia.Platform; using Avalonia.Skia; +using Avalonia.Reactive; namespace Avalonia.Browser.Skia { diff --git a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderTarget.cs b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderTarget.cs index f69dd3c344..9424122ce8 100644 --- a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderTarget.cs +++ b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderTarget.cs @@ -14,14 +14,12 @@ namespace Avalonia.Browser.Skia _size = browserSkiaSurface.Size; var glFbInfo = new GRGlFramebufferInfo(browserSkiaSurface.GlInfo.FboId, browserSkiaSurface.ColorType.ToGlSizedFormat()); - { - _browserSkiaSurface = browserSkiaSurface; - _renderTarget = new GRBackendRenderTarget( - (int)(browserSkiaSurface.Size.Width * browserSkiaSurface.Scaling), - (int)(browserSkiaSurface.Size.Height * browserSkiaSurface.Scaling), - browserSkiaSurface.GlInfo.Samples, - browserSkiaSurface.GlInfo.Stencils, glFbInfo); - } + _browserSkiaSurface = browserSkiaSurface; + _renderTarget = new GRBackendRenderTarget( + (int)(browserSkiaSurface.Size.Width * browserSkiaSurface.Scaling), + (int)(browserSkiaSurface.Size.Height * browserSkiaSurface.Scaling), + browserSkiaSurface.GlInfo.Samples, + browserSkiaSurface.GlInfo.Stencils, glFbInfo); } public void Dispose() diff --git a/src/Browser/Avalonia.Browser/Storage/BlobReadableStream.cs b/src/Browser/Avalonia.Browser/Storage/BlobReadableStream.cs index 4fce190346..3404452bca 100644 --- a/src/Browser/Avalonia.Browser/Storage/BlobReadableStream.cs +++ b/src/Browser/Avalonia.Browser/Storage/BlobReadableStream.cs @@ -7,7 +7,6 @@ using Avalonia.Browser.Interop; namespace Avalonia.Browser.Storage; -[System.Runtime.Versioning.SupportedOSPlatform("browser")] internal class BlobReadableStream : Stream { private JSObject? _jSReference; diff --git a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs index 28de55092b..0bebcb9f63 100644 --- a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs +++ b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs @@ -13,7 +13,6 @@ namespace Avalonia.Browser.Storage; internal record FilePickerAcceptType(string Description, IReadOnlyDictionary> Accept); -[SupportedOSPlatform("browser")] internal class BrowserStorageProvider : IStorageProvider { internal const string PickerCancelMessage = "The user aborted a request"; @@ -30,11 +29,11 @@ internal class BrowserStorageProvider : IStorageProvider await _lazyModule.Value; var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; - var (types, exludeAll) = ConvertFileTypes(options.FileTypeFilter); + var (types, excludeAll) = ConvertFileTypes(options.FileTypeFilter); try { - using var items = await StorageHelper.OpenFileDialog(startIn, options.AllowMultiple, types, exludeAll); + using var items = await StorageHelper.OpenFileDialog(startIn, options.AllowMultiple, types, excludeAll); if (items is null) { return Array.Empty(); @@ -64,11 +63,11 @@ internal class BrowserStorageProvider : IStorageProvider await _lazyModule.Value; var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; - var (types, exludeAll) = ConvertFileTypes(options.FileTypeChoices); + var (types, excludeAll) = ConvertFileTypes(options.FileTypeChoices); try { - var item = await StorageHelper.SaveFileDialog(startIn, options.SuggestedFileName, types, exludeAll); + var item = await StorageHelper.SaveFileDialog(startIn, options.SuggestedFileName, types, excludeAll); return item is not null ? new JSStorageFile(item) : null; } catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) @@ -128,9 +127,9 @@ internal class BrowserStorageProvider : IStorageProvider types = null; } - var inlcudeAll = input?.Contains(FilePickerFileTypes.All) == true || types is null; + var includeAll = input?.Contains(FilePickerFileTypes.All) == true || types is null; - return (types, !inlcudeAll); + return (types, !includeAll); } } diff --git a/src/Browser/Avalonia.Browser/Storage/WriteableStream.cs b/src/Browser/Avalonia.Browser/Storage/WriteableStream.cs index f29f7420ac..b2f14b6a7a 100644 --- a/src/Browser/Avalonia.Browser/Storage/WriteableStream.cs +++ b/src/Browser/Avalonia.Browser/Storage/WriteableStream.cs @@ -7,7 +7,6 @@ using Avalonia.Browser.Interop; namespace Avalonia.Browser.Storage; -[System.Runtime.Versioning.SupportedOSPlatform("browser")] // Loose wrapper implementaion of a stream on top of FileAPI FileSystemWritableFileStream internal sealed class WriteableStream : Stream { diff --git a/src/Browser/Avalonia.Browser/WebEmbeddableControlRoot.cs b/src/Browser/Avalonia.Browser/WebEmbeddableControlRoot.cs index e389ee98ea..df1a24fa0f 100644 --- a/src/Browser/Avalonia.Browser/WebEmbeddableControlRoot.cs +++ b/src/Browser/Avalonia.Browser/WebEmbeddableControlRoot.cs @@ -18,7 +18,7 @@ namespace Avalonia.Browser _onFirstRender = onFirstRender; } - public Rect Bounds => Rect.Empty; + public Rect Bounds => default; public bool HasRendered => _hasRendered; diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts index 385cdd4c41..e8e37faf58 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts @@ -17,7 +17,6 @@ export class AvaloniaDOM { const canvas = document.createElement("canvas"); canvas.id = `canvas${randomIdPart}`; canvas.classList.add("avalonia-canvas"); - canvas.style.backgroundColor = "#ccc"; canvas.style.width = "100%"; canvas.style.position = "absolute"; diff --git a/src/Browser/Avalonia.Browser/webapp/package-lock.json b/src/Browser/Avalonia.Browser/webapp/package-lock.json index 06e94629d7..2d875e84db 100644 --- a/src/Browser/Avalonia.Browser/webapp/package-lock.json +++ b/src/Browser/Avalonia.Browser/webapp/package-lock.json @@ -2162,9 +2162,9 @@ "dev": true }, "node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "dependencies": { "minimist": "^1.2.0" @@ -4699,9 +4699,9 @@ "dev": true }, "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "requires": { "minimist": "^1.2.0" diff --git a/src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs b/src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs index ce843952e7..3439f0edae 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs @@ -1,6 +1,5 @@ using Avalonia.LinuxFramebuffer.Output; using Avalonia.Media; -using JetBrains.Annotations; namespace Avalonia.LinuxFramebuffer { diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index ac54365f51..6032f3a92c 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -25,7 +25,7 @@ namespace Avalonia.LinuxFramebuffer Surfaces = new object[] { _outputBackend }; - Invalidate(default(Rect)); + Invalidate(default); _inputBackend.Initialize(this, e => Input?.Invoke(e)); } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs index 1e3c4bed48..686050e7c2 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Threading; using Avalonia.Input; using Avalonia.Input.Raw; -using Avalonia.Threading; using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods; namespace Avalonia.LinuxFramebuffer.Input.EvDev @@ -13,7 +12,6 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev private readonly EvDevDeviceDescription[] _deviceDescriptions; private readonly List _handlers = new List(); private int _epoll; - private object _lock = new object(); private Action _onInput; private IInputRoot _inputRoot; private RawEventGroupingThreadingHelper _inputQueue; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index 77e8202fac..bff9ddc55c 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -11,7 +11,6 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput { private IScreenInfoProvider _screen; private IInputRoot _inputRoot; - private readonly Queue _inputThreadActions = new Queue(); private TouchDevice _touch = new TouchDevice(); private const string LibInput = nameof(Avalonia.LinuxFramebuffer) + "/" + nameof(Avalonia.LinuxFramebuffer.Input) + "/" + nameof(LibInput); private readonly RawEventGroupingThreadingHelper _inputQueue; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 38498951f8..4202ba821f 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Threading; +using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Embedding; @@ -12,12 +13,10 @@ using Avalonia.LinuxFramebuffer.Input; using Avalonia.LinuxFramebuffer.Input.EvDev; using Avalonia.LinuxFramebuffer.Input.LibInput; using Avalonia.LinuxFramebuffer.Output; -using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Composition; -using Avalonia.Threading; -using JetBrains.Annotations; +#nullable enable namespace Avalonia.LinuxFramebuffer { @@ -26,9 +25,9 @@ namespace Avalonia.LinuxFramebuffer IOutputBackend _fb; private static readonly Stopwatch St = Stopwatch.StartNew(); internal static uint Timestamp => (uint)St.ElapsedTicks; - public static InternalPlatformThreadingInterface Threading; + public static InternalPlatformThreadingInterface? Threading; - internal static Compositor Compositor { get; private set; } + internal static Compositor? Compositor { get; private set; } LinuxFramebufferPlatform(IOutputBackend backend) @@ -60,7 +59,7 @@ namespace Avalonia.LinuxFramebuffer } - internal static LinuxFramebufferLifetime Initialize(T builder, IOutputBackend outputBackend, IInputBackend inputBackend) where T : AppBuilderBase, new() + internal static LinuxFramebufferLifetime Initialize(AppBuilder builder, IOutputBackend outputBackend, IInputBackend? inputBackend) { var platform = new LinuxFramebufferPlatform(outputBackend); builder.UseSkia().UseWindowingSubsystem(platform.Initialize, "fbdev"); @@ -71,8 +70,8 @@ namespace Avalonia.LinuxFramebuffer class LinuxFramebufferLifetime : IControlledApplicationLifetime, ISingleViewApplicationLifetime { private readonly IOutputBackend _fb; - [CanBeNull] private readonly IInputBackend _inputBackend; - private TopLevel _topLevel; + private readonly IInputBackend? _inputBackend; + private TopLevel? _topLevel; private readonly CancellationTokenSource _cts = new CancellationTokenSource(); public CancellationToken Token => _cts.Token; @@ -81,15 +80,15 @@ namespace Avalonia.LinuxFramebuffer _fb = fb; } - public LinuxFramebufferLifetime(IOutputBackend fb, IInputBackend input) + public LinuxFramebufferLifetime(IOutputBackend fb, IInputBackend? input) { _fb = fb; _inputBackend = input; } - public Control MainView + public Control? MainView { - get => (Control)_topLevel?.Content; + get => (Control?)_topLevel?.Content; set { if (_topLevel == null) @@ -119,8 +118,8 @@ namespace Avalonia.LinuxFramebuffer } public int ExitCode { get; private set; } - public event EventHandler Startup; - public event EventHandler Exit; + public event EventHandler? Startup; + public event EventHandler? Exit; public void Start(string[] args) { @@ -140,20 +139,17 @@ namespace Avalonia.LinuxFramebuffer public static class LinuxFramebufferPlatformExtensions { - public static int StartLinuxFbDev(this T builder, string[] args, string fbdev = null, double scaling = 1, IInputBackend inputBackend = default) - where T : AppBuilderBase, new() => - StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: null) { Scaling = scaling }, inputBackend); - public static int StartLinuxFbDev(this T builder, string[] args, string fbdev, PixelFormat? format, double scaling, IInputBackend inputBackend = default) - where T : AppBuilderBase, new() => - StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: format) { Scaling = scaling }, inputBackend); - - public static int StartLinuxDrm(this T builder, string[] args, string card = null, double scaling = 1, IInputBackend inputBackend = default) - where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card) { Scaling = scaling }, inputBackend); - public static int StartLinuxDrm(this T builder, string[] args, string card = null, bool connectorsForceProbe = false, [CanBeNull] DrmOutputOptions options = null, IInputBackend inputBackend = default) - where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card, connectorsForceProbe, options), inputBackend); - - public static int StartLinuxDirect(this T builder, string[] args, IOutputBackend outputBackend, IInputBackend inputBackend = default) - where T : AppBuilderBase, new() + public static int StartLinuxFbDev(this AppBuilder builder, string[] args, string? fbdev = null, double scaling = 1, IInputBackend? inputBackend = default) + => StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: null) { Scaling = scaling }, inputBackend); + public static int StartLinuxFbDev(this AppBuilder builder, string[] args, string fbdev, PixelFormat? format, double scaling, IInputBackend? inputBackend = default) + => StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: format) { Scaling = scaling }, inputBackend); + + public static int StartLinuxDrm(this AppBuilder builder, string[] args, string? card = null, double scaling = 1, IInputBackend? inputBackend = default) + => StartLinuxDirect(builder, args, new DrmOutput(card) { Scaling = scaling }, inputBackend); + public static int StartLinuxDrm(this AppBuilder builder, string[] args, string? card = null, bool connectorsForceProbe = false, DrmOutputOptions? options = null, IInputBackend? inputBackend = default) + => StartLinuxDirect(builder, args, new DrmOutput(card, connectorsForceProbe, options), inputBackend); + + public static int StartLinuxDirect(this AppBuilder builder, string[] args, IOutputBackend outputBackend, IInputBackend? inputBackend = default) { var lifetime = LinuxFramebufferPlatform.Initialize(builder, outputBackend, inputBackend); builder.SetupWithLifetime(lifetime); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index 22dd407791..d61dcd4f91 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -8,7 +8,6 @@ using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; using Avalonia.Platform.Interop; -using JetBrains.Annotations; using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods; using static Avalonia.LinuxFramebuffer.Output.LibDrm; using static Avalonia.LinuxFramebuffer.Output.LibDrm.GbmColorFormats; @@ -50,7 +49,7 @@ namespace Avalonia.LinuxFramebuffer.Output _outputOptions = options; Init(card, resources, connector, modeInfo); } - public DrmOutput(string path = null, bool connectorsForceProbe = false, [CanBeNull] DrmOutputOptions options = null) + public DrmOutput(string path = null, bool connectorsForceProbe = false, DrmOutputOptions? options = null) { if(options != null) _outputOptions = options; diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs index 2bb91e8a32..e3d154ffdd 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs @@ -179,7 +179,7 @@ namespace Avalonia.Markup.Xaml.XamlIl } finally { - if( _sreCanSave) + if(!success && _sreCanSave) DumpRuntimeCompilationResults(); } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlOptionMarkupExtensionTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlOptionMarkupExtensionTransformer.cs index 5004e594f7..13de455b96 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlOptionMarkupExtensionTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlOptionMarkupExtensionTransformer.cs @@ -366,6 +366,7 @@ internal class AvaloniaXamlIlOptionMarkupExtensionTransformer : IXamlAstTransfor foreach (var branch in ExtensionNodeContainer.Branches) { var next = codeGen.DefineLabel(); + codeGen.Emit(OpCodes.Nop); if (branch.HasContext) { codeGen.Ldloc(context.ContextLocal); 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 ba0da4fbe0..078e23bc02 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs @@ -124,7 +124,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers $"Cannot convert '{attachedProperty.Value}' to '{targetPropertyType.GetFqn()}", node); - result = new XamlIlAttacchedPropertyEqualsSelector(result, targetPropertyField, typedValue); + result = new XamlIlAttachedPropertyEqualsSelector(result, targetPropertyField, typedValue); break; } case SelectorGrammar.ChildSyntax child: @@ -414,9 +414,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } - class XamlIlAttacchedPropertyEqualsSelector : XamlIlSelectorNode + class XamlIlAttachedPropertyEqualsSelector : XamlIlSelectorNode { - public XamlIlAttacchedPropertyEqualsSelector(XamlIlSelectorNode previous, + public XamlIlAttachedPropertyEqualsSelector(XamlIlSelectorNode previous, IXamlField propertyFiled, IXamlAstValueNode value) : base(previous) diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 267bd2520e..da739c754c 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -66,7 +66,6 @@ - diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/BitmapTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/BitmapTypeConverter.cs index dcd60f7a79..fc6a8557f3 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/BitmapTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/BitmapTypeConverter.cs @@ -24,7 +24,7 @@ namespace Avalonia.Markup.Xaml.Converters if(uri.IsAbsoluteUri && uri.IsFile) return new Bitmap(uri.LocalPath); - var assets = AvaloniaLocator.Current.GetService(); + var assets = AvaloniaLocator.Current.GetRequiredService(); return new Bitmap(assets.Open(uri, context.GetContextBaseUri())); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/IconTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/IconTypeConverter.cs index 24b690b6f1..698f5a9327 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/IconTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/IconTypeConverter.cs @@ -40,7 +40,7 @@ namespace Avalonia.Markup.Xaml.Converters if(uri.IsAbsoluteUri && uri.IsFile) return new WindowIcon(uri.LocalPath); - var assets = AvaloniaLocator.Current.GetService(); + var assets = AvaloniaLocator.Current.GetRequiredService(); return new WindowIcon(assets.Open(uri, context.GetContextBaseUri())); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CommandAccessorPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CommandAccessorPlugin.cs index 3084964d44..4478a79d27 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CommandAccessorPlugin.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CommandAccessorPlugin.cs @@ -44,9 +44,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings public CommandAccessor(WeakReference reference, Action execute, Func canExecute, ISet dependsOnProperties) { - Contract.Requires(reference != null); - - _reference = reference; + _reference = reference ?? throw new ArgumentNullException(nameof(reference)); _dependsOnProperties = dependsOnProperties; _command = new Command(reference, execute, canExecute); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/FindVisualAncestorNode.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/FindVisualAncestorNode.cs index 45e23db84f..170a31fd64 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/FindVisualAncestorNode.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/FindVisualAncestorNode.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Data.Core; using Avalonia.VisualTree; +using Avalonia.Reactive; namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs index 77ffa24687..cb21df0507 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Reactive.Linq; using System.Text; using Avalonia.Data.Core.Plugins; +using Avalonia.Reactive; namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs index ef11b06369..abb166a92b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs @@ -5,6 +5,7 @@ using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Data.Core.Plugins; using Avalonia.Utilities; +using Avalonia.Reactive; namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { @@ -28,11 +29,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings public AvaloniaPropertyAccessor(WeakReference reference, AvaloniaProperty property) { - Contract.Requires(reference != null); - Contract.Requires(property != null); - - _reference = reference; - _property = property; + _reference = reference ?? throw new ArgumentNullException(nameof(reference));; + _property = property ?? throw new ArgumentNullException(nameof(property));; } public AvaloniaObject Instance @@ -77,11 +75,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings public InpcPropertyAccessor(WeakReference reference, IPropertyInfo property) { - Contract.Requires(reference != null); - Contract.Requires(property != null); - - _reference = reference; - _property = property; + _reference = reference ?? throw new ArgumentNullException(nameof(reference)); + _property = property ?? throw new ArgumentNullException(nameof(property)); } public override Type PropertyType => _property.PropertyType; diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs index 8489dd9d19..7c9f7559c6 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs @@ -1,10 +1,9 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Reactive.Linq; -using System.Reactive.Subjects; using System.Threading.Tasks; using Avalonia.Data; using Avalonia.Data.Core.Plugins; +using Avalonia.Reactive; namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { @@ -30,7 +29,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings case TaskStatus.Faulted: return HandleCompleted(task); default: - var subject = new Subject(); + var subject = new LightweightSubject(); task.ContinueWith( x => HandleCompleted(task).Subscribe(subject), TaskScheduler.FromCurrentSynchronizationContext()) diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnFormFactorExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnFormFactorExtension.cs index 51e09eef71..c2f1100eff 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnFormFactorExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnFormFactorExtension.cs @@ -6,7 +6,7 @@ using Avalonia.Platform; namespace Avalonia.Markup.Xaml.MarkupExtensions; -public class OnFormFactorExtension : OnFormFactorExtensionBase +public sealed class OnFormFactorExtension : OnFormFactorExtensionBase { public OnFormFactorExtension() { @@ -24,7 +24,7 @@ public class OnFormFactorExtension : OnFormFactorExtensionBase } } -public class OnFormFactorExtension : OnFormFactorExtensionBase> +public sealed class OnFormFactorExtension : OnFormFactorExtensionBase> { public OnFormFactorExtension() { diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs index 1ac7a522f1..1c20020978 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs @@ -1,11 +1,11 @@ #nullable enable using System; +using Avalonia.Compatibility; using Avalonia.Metadata; -using Avalonia.Platform; namespace Avalonia.Markup.Xaml.MarkupExtensions; -public class OnPlatformExtension : OnPlatformExtensionBase +public sealed class OnPlatformExtension : OnPlatformExtensionBase { public OnPlatformExtension() { @@ -17,13 +17,13 @@ public class OnPlatformExtension : OnPlatformExtensionBase Default = defaultValue; } - public static bool ShouldProvideOption(IServiceProvider serviceProvider, OperatingSystemType option) + public static bool ShouldProvideOption(string option) { - return serviceProvider.GetService().GetRuntimeInfo().OperatingSystem == option; + return ShouldProvideOptionInternal(option); } } -public class OnPlatformExtension : OnPlatformExtensionBase> +public sealed class OnPlatformExtension : OnPlatformExtensionBase> { public OnPlatformExtension() { @@ -35,9 +35,9 @@ public class OnPlatformExtension : OnPlatformExtensionBase().GetRuntimeInfo().OperatingSystem == option; + return ShouldProvideOptionInternal(option); } } @@ -47,27 +47,44 @@ public abstract class OnPlatformExtensionBase : IAddChild [MarkupExtensionDefaultOption] public TReturn? Default { get; set; } - [MarkupExtensionOption(OperatingSystemType.WinNT)] + [MarkupExtensionOption("WINDOWS")] public TReturn? Windows { get; set; } - [MarkupExtensionOption(OperatingSystemType.OSX)] + [MarkupExtensionOption("OSX")] // ReSharper disable once InconsistentNaming public TReturn? macOS { get; set; } - [MarkupExtensionOption(OperatingSystemType.Linux)] + [MarkupExtensionOption("LINUX")] public TReturn? Linux { get; set; } - [MarkupExtensionOption(OperatingSystemType.Android)] + [MarkupExtensionOption("ANDROID")] public TReturn? Android { get; set; } - [MarkupExtensionOption(OperatingSystemType.iOS)] + [MarkupExtensionOption("IOS")] // ReSharper disable once InconsistentNaming public TReturn? iOS { get; set; } - [MarkupExtensionOption(OperatingSystemType.Browser)] + [MarkupExtensionOption("BROWSER")] public TReturn? Browser { get; set; } // Required for the compiler, will be replaced with actual method compile time. public object ProvideValue() { return this; } void IAddChild.AddChild(TOn child) {} + + private protected static bool ShouldProvideOptionInternal(string option) + { + // Instead of using OperatingSystem.IsOSPlatform(string) we use specific "Is***" methods so whole method can be trimmed by the mono linked. + // Keep in mind it works only with const "option" parameter. + // IsOSPlatform might work better with trimming in the future, so it should be re-visited after .NET 8/9. + return option switch + { + "WINDOWS" => OperatingSystemEx.IsWindows(), + "OSX" => OperatingSystemEx.IsMacOS(), + "LINUX" => OperatingSystemEx.IsLinux(), + "ANDROID" => OperatingSystemEx.IsAndroid(), + "IOS" => OperatingSystemEx.IsIOS(), + "BROWSER" => OperatingSystemEx.IsBrowser(), + _ => OperatingSystemEx.IsOSPlatform(option) + }; + } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs index 07c79d7077..23166170db 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs @@ -7,7 +7,6 @@ namespace Avalonia.Markup.Xaml.Templates public static class TemplateContent { public static ControlTemplateResult Load(object templateContent) - { if (templateContent is Func direct) { diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 753d577105..e3878b5bc6 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -12,7 +12,6 @@ - diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index 6f836a799a..66907f33d0 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -1,15 +1,8 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reactive; -using System.Reactive.Linq; using Avalonia.Controls; -using Avalonia.Data.Converters; using Avalonia.Data.Core; -using Avalonia.LogicalTree; using Avalonia.Markup.Parsers; -using Avalonia.Reactive; -using Avalonia.VisualTree; namespace Avalonia.Data { diff --git a/src/Markup/Avalonia.Markup/Data/BindingBase.cs b/src/Markup/Avalonia.Markup/Data/BindingBase.cs index c035f0b05d..90f312a249 100644 --- a/src/Markup/Avalonia.Markup/Data/BindingBase.cs +++ b/src/Markup/Avalonia.Markup/Data/BindingBase.cs @@ -1,9 +1,5 @@ - using System; using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reactive; -using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Data.Converters; using Avalonia.Data.Core; @@ -247,6 +243,7 @@ namespace Avalonia.Data // Content property is bound to a value which becomes the ContentPresenter's // DataContext - it is from this that the child hosted by the ContentPresenter needs to // inherit its DataContext. + return target.GetObservable(Visual.VisualParentProperty) .Select(x => { @@ -255,7 +252,7 @@ namespace Avalonia.Data }).Switch(); } - private class UpdateSignal : SingleSubscriberObservableBase + private class UpdateSignal : SingleSubscriberObservableBase { private readonly AvaloniaObject _target; private readonly AvaloniaProperty _property; @@ -280,7 +277,7 @@ namespace Avalonia.Data { if (e.Property == _property) { - PublishNext(Unit.Default); + PublishNext(default); } } } diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 4693b0c617..1515ff2c90 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Data.Converters; using Avalonia.Metadata; diff --git a/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs b/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs index 4dcdfb3c0e..4270063f87 100644 --- a/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs @@ -1,6 +1,5 @@ using System; using System.Globalization; -using System.Reactive.Subjects; using Avalonia.Data.Converters; using Avalonia.Reactive; using Avalonia.Styling; @@ -13,7 +12,7 @@ namespace Avalonia.Data public class TemplateBinding : SingleSubscriberObservableBase, IBinding, IDescription, - ISubject, + IAvaloniaSubject, ISetterValue { private bool _isSetterValue; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs index 6e6a163989..00f40dfcd3 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Reactive; using Avalonia.Controls; using Avalonia.Data.Core; using Avalonia.Utilities; @@ -64,7 +63,7 @@ namespace Avalonia.Markup.Parsers public static ExpressionObserver Build( Func rootGetter, string expression, - IObservable update, + IObservable update, bool enableDataValidation = false, string? description = null, Func? typeResolver = null) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs index f9a3a61736..4fc17e440b 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs @@ -2,6 +2,7 @@ using Avalonia.Controls; using Avalonia.Data.Core; using Avalonia.LogicalTree; +using Avalonia.Reactive; namespace Avalonia.Markup.Parsers.Nodes { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs index 697612ca12..ffbd34d492 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Data.Core; using Avalonia.LogicalTree; +using Avalonia.Reactive; namespace Avalonia.Markup.Parsers.Nodes { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs index c420a9df8d..ba1c1edffe 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs @@ -5,7 +5,7 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; -using System.Reactive.Linq; +using Avalonia.Reactive; using System.Reflection; using Avalonia.Data; using Avalonia.Data.Core; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index 4d6d16a3ce..dbf37f0900 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -552,7 +552,8 @@ namespace Avalonia.Markup.Parsers public override bool Equals(object? obj) { - return obj is ClassSyntax && ((ClassSyntax)obj).Class == Class; + return obj is ClassSyntax syntax && + syntax.Class == Class; } } @@ -562,7 +563,8 @@ namespace Avalonia.Markup.Parsers public override bool Equals(object? obj) { - return obj is NameSyntax && ((NameSyntax)obj).Name == Name; + return obj is NameSyntax syntax && + syntax.Name == Name; } } @@ -574,9 +576,9 @@ namespace Avalonia.Markup.Parsers public override bool Equals(object? obj) { - return obj is PropertySyntax && - ((PropertySyntax)obj).Property == Property && - ((PropertySyntax)obj).Value == Value; + return obj is PropertySyntax syntax && + syntax.Property == Property && + syntax.Value == Value; } } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index f18330ba8a..8b71f4e17e 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -10,7 +10,6 @@ using Avalonia.Rendering.SceneGraph; using Avalonia.Rendering.Utilities; using Avalonia.Utilities; using Avalonia.Media.Imaging; -using JetBrains.Annotations; using SkiaSharp; namespace Avalonia.Skia @@ -225,9 +224,9 @@ namespace Avalonia.Skia var impl = (GeometryImpl) geometry; var size = geometry.Bounds.Size; - using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default(PaintWrapper)) + using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default) using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, - size.Inflate(new Thickness(pen?.Thickness / 2 ?? 0))) : default(PaintWrapper)) + size.Inflate(new Thickness(pen?.Thickness / 2 ?? 0))) : default) { if (fill.Paint != null) { @@ -362,7 +361,7 @@ namespace Avalonia.Skia foreach (var boxShadow in boxShadows) { - if (!boxShadow.IsEmpty && !boxShadow.IsInset) + if (!boxShadow.IsDefault && !boxShadow.IsInset) { using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity)) { @@ -418,7 +417,7 @@ namespace Avalonia.Skia foreach (var boxShadow in boxShadows) { - if (!boxShadow.IsEmpty && boxShadow.IsInset) + if (!boxShadow.IsDefault && boxShadow.IsInset) { using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity)) { @@ -675,13 +674,14 @@ namespace Avalonia.Skia } } - [CanBeNull] - public object GetFeature(Type t) +#nullable enable + public object? GetFeature(Type t) { if (t == typeof(ISkiaSharpApiLeaseFeature)) return new SkiaLeaseFeature(this); return null; } +#nullable restore /// /// Configure paint wrapper for using gradient brush. @@ -1137,11 +1137,14 @@ namespace Avalonia.Skia if (pen.DashStyle?.Dashes != null && pen.DashStyle.Dashes.Count > 0) { var srcDashes = pen.DashStyle.Dashes; - var dashesArray = new float[srcDashes.Count]; - for (var i = 0; i < srcDashes.Count; ++i) + var count = srcDashes.Count % 2 == 0 ? srcDashes.Count : srcDashes.Count * 2; + + var dashesArray = new float[count]; + + for (var i = 0; i < count; ++i) { - dashesArray[i] = (float) srcDashes[i] * paint.StrokeWidth; + dashesArray[i] = (float) srcDashes[i % srcDashes.Count] * paint.StrokeWidth; } var offset = (float)(pen.DashStyle.Offset * pen.Thickness); diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index 1ae47c8b7a..05fad25f1b 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -1,5 +1,5 @@ using System; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Platform; using Avalonia.Rendering; diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index 4037cc4a35..15a3ebff40 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -244,8 +244,8 @@ namespace Avalonia.Skia public void Invalidate() { CachedStrokePath?.Dispose(); - CachedGeometryRenderBounds = Rect.Empty; - _cachedStrokeWidth = default(float); + CachedGeometryRenderBounds = default; + _cachedStrokeWidth = default; } } } diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index bdc3d075cf..dd7ed31a6e 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -1,8 +1,9 @@ using System; +using System.Collections.Generic; using Avalonia.Metadata; using Avalonia.Platform; -using JetBrains.Annotations; using SkiaSharp; +#nullable enable namespace Avalonia.Skia { @@ -10,7 +11,7 @@ namespace Avalonia.Skia [Unstable] public class GlyphRunImpl : IGlyphRunImpl { - public GlyphRunImpl([NotNull] SKTextBlob textBlob) + public GlyphRunImpl(SKTextBlob textBlob) { TextBlob = textBlob ?? throw new ArgumentNullException (nameof (textBlob)); } @@ -20,6 +21,9 @@ namespace Avalonia.Skia /// public SKTextBlob TextBlob { get; } + public IReadOnlyList GetIntersections(float upperBound, float lowerBound) => + TextBlob.GetIntercepts(lowerBound, upperBound); + void IDisposable.Dispose() { TextBlob.Dispose(); diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index 5bf1272c2f..4b3c7a016d 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs @@ -1,5 +1,5 @@ using System; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.OpenGL; using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; diff --git a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs index b4dd754822..4cb1430a3b 100644 --- a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs @@ -34,6 +34,7 @@ namespace Avalonia.Skia.Helpers /// Save Skia image to a stream. /// /// Image to save + /// The output stream to save the image. /// /// The optional quality for PNG compression. /// The quality value is interpreted from 0 - 100. If quality is null diff --git a/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs b/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs index 9236bdee8d..84a1972e01 100644 --- a/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs @@ -1,4 +1,5 @@ -using Avalonia.Platform; +using Avalonia.Compatibility; +using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia.Helpers @@ -18,10 +19,7 @@ namespace Avalonia.Skia.Helpers var colorType = format?.ToSkColorType() ?? SKImageInfo.PlatformColorType; // TODO: This looks like some leftover hack - var runtimePlatform = AvaloniaLocator.Current?.GetService(); - var runtime = runtimePlatform?.GetRuntimeInfo(); - - if (runtime?.IsDesktop == true && runtime.Value.OperatingSystem == OperatingSystemType.Linux) + if (OperatingSystemEx.IsLinux()) { colorType = SKColorType.Bgra8888; } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index 5ca7f40d17..f7a86c11ff 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -37,7 +37,7 @@ namespace Avalonia.Skia var typeFaceCollection = new SKTypefaceCollection(); - var assetLoader = AvaloniaLocator.Current.GetService(); + var assetLoader = AvaloniaLocator.Current.GetRequiredService(); foreach (var asset in fontAssets) { diff --git a/src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs b/src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs index 014298ce83..3e4fd7b385 100644 --- a/src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs +++ b/src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs @@ -12,10 +12,9 @@ namespace Avalonia /// /// Enable Skia renderer. /// - /// Builder type. /// Builder. /// Configure builder. - public static T UseSkia(this T builder) where T : AppBuilderBase, new() + public static AppBuilder UseSkia(this AppBuilder builder) { return builder.UseRenderingSubsystem(() => SkiaPlatform.Initialize( AvaloniaLocator.Current.GetService() ?? new SkiaOptions()), diff --git a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs index 86450690e6..df847d2224 100644 --- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs @@ -34,7 +34,7 @@ namespace Avalonia.Skia /// /// Initializes a new instance of the class. /// - public StreamGeometryImpl() : this(CreateEmptyPath(), Rect.Empty) + public StreamGeometryImpl() : this(CreateEmptyPath(), default) { } diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index a7998353d9..e5fb182a3b 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -1,6 +1,6 @@ using System; using System.IO; -using System.Reactive.Disposables; +using Avalonia.Reactive; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; diff --git a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs index dcb267b2a3..9864a14a9c 100644 --- a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs @@ -96,9 +96,9 @@ namespace Avalonia.Skia SKColorType colorType = format.ToSkColorType(); SKAlphaType alphaType = alphaFormat.ToSkAlphaType(); - - var runtimePlatform = AvaloniaLocator.Current?.GetService(); - + + var runtimePlatform = AvaloniaLocator.Current.GetService(); + if (runtimePlatform != null) { _bitmap = new SKBitmap(); diff --git a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj index fb7831415d..32bcdba758 100644 --- a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj +++ b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj @@ -14,10 +14,8 @@ - - diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index a5f77230b7..5887ba2172 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -16,7 +16,7 @@ namespace Avalonia { public static class Direct2DApplicationExtensions { - public static T UseDirect2D1(this T builder) where T : AppBuilderBase, new() + public static AppBuilder UseDirect2D1(this AppBuilder builder) { builder.UseRenderingSubsystem(Direct2D1.Direct2D1Platform.Initialize, "Direct2D1"); return builder; diff --git a/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs b/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs index e5f87e71a2..4663a6561f 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs @@ -22,7 +22,7 @@ namespace Avalonia.Direct2D1.Media { var factory1 = factory; - var assetLoader = AvaloniaLocator.Current.GetService(); + var assetLoader = AvaloniaLocator.Current.GetRequiredService(); foreach (var asset in fontAssets) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 180ae491b3..3f2298eb22 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -409,7 +409,7 @@ namespace Avalonia.Direct2D1.Media } else { - var platform = AvaloniaLocator.Current.GetService(); + var platform = AvaloniaLocator.Current.GetRequiredService(); var dpi = new Vector(_deviceContext.DotsPerInch.Width, _deviceContext.DotsPerInch.Height); var pixelSize = PixelSize.FromSizeWithDpi(size, dpi); return (IDrawingContextLayerImpl)platform.CreateRenderTargetBitmap(pixelSize, dpi); diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs index 0b06d5ef3e..67418613a4 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs @@ -1,4 +1,5 @@ -using Avalonia.Platform; +using System.Collections.Generic; +using Avalonia.Platform; namespace Avalonia.Direct2D1.Media { @@ -13,7 +14,13 @@ namespace Avalonia.Direct2D1.Media public void Dispose() { - GlyphRun?.Dispose(); + //SharpDX already handles this. + //GlyphRun?.Dispose(); + } + + public IReadOnlyList GetIntersections(float lowerBound, float upperBound) + { + return null; } } } diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/Direct2DImageSurface.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/Direct2DImageSurface.cs index a5474803a1..f900204504 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/Direct2DImageSurface.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/Direct2DImageSurface.cs @@ -127,7 +127,7 @@ namespace Avalonia.Win32.Interop.Wpf DeviceWindowHandle = GetDesktopWindow(), PresentationInterval = PresentInterval.Default }; - s_dxDevice = s_dxDevice ?? AvaloniaLocator.Current.GetService() + s_dxDevice = s_dxDevice ?? AvaloniaLocator.Current.GetRequiredService() .QueryInterface(); s_d3DDevice = new DeviceEx(s_d3DContext, 0, DeviceType.Hardware, IntPtr.Zero, CreateFlags.HardwareVertexProcessing | CreateFlags.Multithreaded | CreateFlags.FpuPreserve, presentparams); @@ -156,7 +156,7 @@ namespace Avalonia.Win32.Interop.Wpf _impl.ImageSource = _image; RemoveAndDispose(ref _backBuffer); - if (size == default(IntSize)) + if (size == default) { _image.Lock(); _image.SetBackBuffer(D3DResourceType.IDirect3DSurface9, IntPtr.Zero); @@ -171,7 +171,7 @@ namespace Avalonia.Win32.Interop.Wpf static void RemoveAndDispose(ref T d) where T : IDisposable { d?.Dispose(); - d = default(T); + d = default; } void Swap() diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/IntSize.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/IntSize.cs index b8648dfc50..3bfbf4bd92 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/IntSize.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/IntSize.cs @@ -28,7 +28,7 @@ namespace Avalonia.Win32.Interop.Wpf public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; - return obj is IntSize && Equals((IntSize) obj); + return obj is IntSize size && Equals(size); } public override int GetHashCode() diff --git a/src/Windows/Avalonia.Win32/ClipboardImpl.cs b/src/Windows/Avalonia.Win32/ClipboardImpl.cs index 6153c7c75c..82fd1109f4 100644 --- a/src/Windows/Avalonia.Win32/ClipboardImpl.cs +++ b/src/Windows/Avalonia.Win32/ClipboardImpl.cs @@ -1,6 +1,6 @@ using System; using System.Linq; -using System.Reactive.Disposables; +using Avalonia.Reactive; using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia.Input; diff --git a/src/Windows/Avalonia.Win32/DataObject.cs b/src/Windows/Avalonia.Win32/DataObject.cs index b7a567ea97..7d22e57a30 100644 --- a/src/Windows/Avalonia.Win32/DataObject.cs +++ b/src/Windows/Avalonia.Win32/DataObject.cs @@ -205,7 +205,7 @@ namespace Avalonia.Win32 if (string.IsNullOrEmpty(fmt) || !_wrapped.Contains(fmt)) return DV_E_FORMATETC; - * medium = default(Interop.STGMEDIUM); + * medium = default; medium->tymed = TYMED.TYMED_HGLOBAL; return WriteDataToHGlobal(fmt, ref medium->unionmember); } diff --git a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs index cf1bdc1671..998ff4a427 100644 --- a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs +++ b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs @@ -1,7 +1,6 @@ using System; using Avalonia.Controls; using Avalonia.Input; -using Avalonia.VisualTree; using Avalonia.Win32.Interop; namespace Avalonia.Win32.Input @@ -34,7 +33,7 @@ namespace Avalonia.Win32.Input protected override void PlatformCapture(IInputElement element) { - var hwnd = (((element as Visual)?.GetVisualRoot() as TopLevel)?.PlatformImpl as WindowImpl) + var hwnd = (TopLevel.GetTopLevel(element as Visual)?.PlatformImpl as WindowImpl) ?.Handle.Handle; if (hwnd.HasValue && hwnd != IntPtr.Zero) diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs b/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs index 966f996a91..08b3ee32fa 100644 --- a/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs +++ b/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs @@ -6,8 +6,6 @@ namespace Avalonia.Win32.Interop.Automation { internal static class UiaCoreTypesApi { - private const string StartListeningExportName = "SynchronizedInputPattern_StartListening"; - internal enum AutomationIdType { Property, diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index a7cca5b0f3..672f3a781a 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -820,6 +820,14 @@ namespace Avalonia.Win32.Interop DWMWA_LAST }; + public enum DwmWindowCornerPreference : uint + { + DWMWCP_DEFAULT = 0, + DWMWCP_DONOTROUND, + DWMWCP_ROUND, + DWMWCP_ROUNDSMALL + } + public enum MapVirtualKeyMapTypes : uint { MAPVK_VK_TO_VSC = 0x00, @@ -1409,7 +1417,7 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern bool TranslateMessage(ref MSG lpMsg); - [DllImport("user32.dll")] + [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern bool UnregisterClass(string lpClassName, IntPtr hInstance); [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "SetWindowTextW")] diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index b4eb6d8851..8bbdfe5fd9 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -121,7 +121,7 @@ namespace Avalonia.Win32 _dragDevice, RawDragEventType.DragLeave, _target, - default(Point), + default, null, DragDropEffects.None, RawInputModifiers.None diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs index 9b4eefd170..9a829aff92 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.ExceptionServices; using Avalonia.Logging; using Avalonia.OpenGL; using Avalonia.OpenGL.Angle; @@ -88,8 +87,6 @@ internal class AngleWin32PlatformGraphics : IPlatformGraphics return null; } - return new AngleWin32PlatformGraphics(egl, AngleWin32EglDisplay.CreateSharedD3D11Display(egl)); - foreach (var api in (options?.AllowedPlatformApis ?? new [] { AngleOptions.PlatformApi.DirectX11 diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs index d535efc5b7..b4002d8bc7 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Disposables; using System.Threading; using Avalonia.OpenGL; using Avalonia.Platform; +using Avalonia.Reactive; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; using static Avalonia.Win32.OpenGl.WglConsts; diff --git a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs index 2b8bf6f8bf..fdf36206d0 100644 --- a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs +++ b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using System.Reactive.Linq; +using Avalonia.Reactive; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Platform; diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 3f220f0f09..e3d0cd9e5f 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Reactive.Disposables; +using Avalonia.Reactive; using System.Runtime.InteropServices; using System.Threading; using Avalonia.Controls; @@ -19,16 +19,14 @@ using Avalonia.Threading; using Avalonia.Utilities; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; -using JetBrains.Annotations; using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia { +#nullable enable public static class Win32ApplicationExtensions { - public static T UseWin32( - this T builder) - where T : AppBuilderBase, new() + public static AppBuilder UseWin32(this AppBuilder builder) { return builder.UseWindowingSubsystem( () => Win32.Win32Platform.Initialize( @@ -106,9 +104,10 @@ namespace Avalonia /// /// Provides a way to use a custom-implemented graphics context such as a custom ISkiaGpu /// - [CanBeNull] public IPlatformGraphics CustomPlatformGraphics { get; set; } + public IPlatformGraphics? CustomPlatformGraphics { get; set; } } } +#nullable restore namespace Avalonia.Win32 { @@ -259,6 +258,8 @@ namespace Avalonia.Win32 public bool CurrentThreadIsLoopThread => _uiThread == Thread.CurrentThread; + public TimeSpan HoldWaitDuration { get; set; } = TimeSpan.FromMilliseconds(300); + public event Action Signaled; public event EventHandler ShutdownRequested; diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs index 32019f4c15..5be25a3d4b 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs @@ -1,8 +1,8 @@ using System; using System.Numerics; -using System.Reactive.Disposables; using System.Threading; using Avalonia.OpenGL.Egl; +using Avalonia.Reactive; using Avalonia.Win32.Interop; using MicroCom.Runtime; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 5537a0b704..8c362b0c29 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Remote; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Platform; +using Avalonia.Threading; using Avalonia.Win32.Automation; using Avalonia.Win32.Input; using Avalonia.Win32.Interop.Automation; @@ -69,7 +70,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_CLOSE: { - bool? preventClosing = Closing?.Invoke(); + bool? preventClosing = Closing?.Invoke(WindowCloseReason.WindowClosing); if (preventClosing == true) { return IntPtr.Zero; @@ -106,6 +107,9 @@ namespace Avalonia.Win32 _touchDevice?.Dispose(); //Free other resources Dispose(); + + // Schedule cleanup of anything that requires window to be destroyed + Dispatcher.UIThread.Post(AfterCloseCleanup); return IntPtr.Zero; } @@ -134,13 +138,18 @@ namespace Avalonia.Win32 case WindowsMessage.WM_KEYDOWN: case WindowsMessage.WM_SYSKEYDOWN: { - e = new RawKeyEventArgs( - WindowsKeyboardDevice.Instance, - timestamp, - _owner, - RawKeyEventType.KeyDown, - KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)), - WindowsKeyboardDevice.Instance.Modifiers); + var key = KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)); + + if (key != Key.None) + { + e = new RawKeyEventArgs( + WindowsKeyboardDevice.Instance, + timestamp, + _owner, + RawKeyEventType.KeyDown, + key, + WindowsKeyboardDevice.Instance.Modifiers); + } break; } @@ -159,13 +168,18 @@ namespace Avalonia.Win32 case WindowsMessage.WM_KEYUP: case WindowsMessage.WM_SYSKEYUP: { - e = new RawKeyEventArgs( + var key = KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)); + + if (key != Key.None) + { + e = new RawKeyEventArgs( WindowsKeyboardDevice.Instance, timestamp, _owner, RawKeyEventType.KeyUp, - KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)), + key, WindowsKeyboardDevice.Instance.Modifiers); + } break; } case WindowsMessage.WM_CHAR: diff --git a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs index d393bd304a..fb27ab7856 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs @@ -109,7 +109,7 @@ namespace Avalonia.Win32 if (_owner is Window window) { - var visual = window.Renderer.HitTestFirst(position, (Visual)_owner, x => + var visual = window.Renderer.HitTestFirst(position, window, x => { if (x is IInputElement ie && (!ie.IsHitTestVisible || !ie.IsEffectivelyVisible)) { diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 596b60a8cb..dbcc31c5f5 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -191,7 +191,7 @@ namespace Avalonia.Win32 public Action Activated { get; set; } - public Func Closing { get; set; } + public Func Closing { get; set; } public Action Closed { get; set; } @@ -643,12 +643,6 @@ namespace Avalonia.Win32 _hwnd = IntPtr.Zero; } - if (_className != null) - { - UnregisterClass(_className, GetModuleHandle(null)); - _className = null; - } - _framebuffer.Dispose(); } @@ -1035,6 +1029,12 @@ namespace Avalonia.Win32 { var margins = UpdateExtendMargins(); DwmExtendFrameIntoClientArea(_hwnd, ref margins); + + unsafe + { + int cornerPreference = (int)DwmWindowCornerPreference.DWMWCP_ROUND; + DwmSetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_WINDOW_CORNER_PREFERENCE, &cornerPreference, sizeof(int)); + } } else { @@ -1045,6 +1045,12 @@ namespace Avalonia.Win32 _extendedMargins = new Thickness(); Resize(new Size(rcWindow.Width / RenderScaling, rcWindow.Height / RenderScaling), PlatformResizeReason.Layout); + + unsafe + { + int cornerPreference = (int)DwmWindowCornerPreference.DWMWCP_DEFAULT; + DwmSetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_WINDOW_CORNER_PREFERENCE, &cornerPreference, sizeof(int)); + } } if (!_isClientAreaExtended || (_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) && @@ -1144,6 +1150,15 @@ namespace Avalonia.Win32 } } + private void AfterCloseCleanup() + { + if (_className != null) + { + UnregisterClass(_className, GetModuleHandle(null)); + _className = null; + } + } + private void MaximizeWithoutCoveringTaskbar() { IntPtr monitor = MonitorFromWindow(_hwnd, MONITOR.MONITOR_DEFAULTTONEAREST); @@ -1343,8 +1358,6 @@ namespace Avalonia.Win32 } private const int MF_BYCOMMAND = 0x0; - private const int MF_BYPOSITION = 0x400; - private const int MF_REMOVE = 0x1000; private const int MF_ENABLED = 0x0; private const int MF_GRAYED = 0x1; private const int MF_DISABLED = 0x2; diff --git a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs index ba1bfda949..94e9c0c814 100644 --- a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs +++ b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs @@ -2,33 +2,30 @@ using System; using System.Collections.ObjectModel; using System.IO; using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Linq; +using Avalonia.Reactive; +using Avalonia.Reactive; using Avalonia.Controls.Platform; using Avalonia.Logging; +using Avalonia.Threading; namespace Avalonia.Win32 { internal class WindowsMountedVolumeInfoListener : IDisposable { - private readonly CompositeDisposable _disposables; + private readonly IDisposable _disposable; private bool _beenDisposed = false; private ObservableCollection mountedDrives; public WindowsMountedVolumeInfoListener(ObservableCollection mountedDrives) { this.mountedDrives = mountedDrives; - _disposables = new CompositeDisposable(); - var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1)) - .Subscribe(Poll); + _disposable = DispatcherTimer.Run(Poll, TimeSpan.FromSeconds(1)); - _disposables.Add(pollTimer); - - Poll(0); + Poll(); } - private void Poll(long _) + private bool Poll() { var allDrives = DriveInfo.GetDrives(); @@ -56,13 +53,14 @@ namespace Avalonia.Win32 .ToArray(); if (mountedDrives.SequenceEqual(mountVolInfos)) - return; + return true; else { mountedDrives.Clear(); foreach (var i in mountVolInfos) mountedDrives.Add(i); + return true; } } @@ -72,7 +70,7 @@ namespace Avalonia.Win32 { if (disposing) { - + _disposable.Dispose(); } _beenDisposed = true; } diff --git a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoProvider.cs b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoProvider.cs index e1b5f5a3a0..4f4e0b9293 100644 --- a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoProvider.cs +++ b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoProvider.cs @@ -8,7 +8,6 @@ namespace Avalonia.Win32 { public IDisposable Listen(ObservableCollection mountedDrives) { - Contract.Requires(mountedDrives != null); return new WindowsMountedVolumeInfoListener(mountedDrives); } } diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs b/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs index fb0857e472..a1836f3ce4 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs @@ -1,6 +1,5 @@ #nullable enable using Avalonia.Input.TextInput; -using JetBrains.Annotations; using UIKit; namespace Avalonia.iOS; diff --git a/src/iOS/Avalonia.iOS/EaglDisplay.cs b/src/iOS/Avalonia.iOS/EaglDisplay.cs index 7a5e1a496d..8f7c1f3308 100644 --- a/src/iOS/Avalonia.iOS/EaglDisplay.cs +++ b/src/iOS/Avalonia.iOS/EaglDisplay.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; using Avalonia.OpenGL; using Avalonia.Platform; +using Avalonia.Reactive; using OpenGLES; namespace Avalonia.iOS diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index eb0a55734a..63025f7f0a 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -12,7 +12,7 @@ namespace Avalonia { public static class IOSApplicationExtensions { - public static T UseiOS(this T builder) where T : AppBuilderBase, new() + public static AppBuilder UseiOS(this AppBuilder builder) { return builder .UseWindowingSubsystem(iOS.Platform.Register, "iOS") diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs index 834418437b..5fdb7ecce1 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs @@ -34,7 +34,7 @@ internal class IOSStorageProvider : IStorageProvider UIDocumentPickerViewController documentPicker; if (OperatingSystem.IsIOSVersionAtLeast(14)) { - var allowedUtis = options.FileTypeFilter?.SelectMany(f => + var allowedUtils = options.FileTypeFilter?.SelectMany(f => { // We check for OS version outside of the lambda, it's safe. #pragma warning disable CA1416 @@ -56,18 +56,18 @@ internal class IOSStorageProvider : IStorageProvider UTTypes.Item, UTTypes.Data }; - documentPicker = new UIDocumentPickerViewController(allowedUtis!, false); + documentPicker = new UIDocumentPickerViewController(allowedUtils!, false); } else { - var allowedUtis = options.FileTypeFilter?.SelectMany(f => f.AppleUniformTypeIdentifiers ?? Array.Empty()) + var allowedUtils = options.FileTypeFilter?.SelectMany(f => f.AppleUniformTypeIdentifiers ?? Array.Empty()) .ToArray() ?? new[] { UTTypeLegacy.Content, UTTypeLegacy.Item, "public.data" }; - documentPicker = new UIDocumentPickerViewController(allowedUtis, UIDocumentPickerMode.Open); + documentPicker = new UIDocumentPickerViewController(allowedUtils, UIDocumentPickerMode.Open); } using (documentPicker) diff --git a/src/tools/DevAnalyzers/DevAnalyzers.csproj b/src/tools/DevAnalyzers/DevAnalyzers.csproj index 53e3e74e76..e5c2fc6cf6 100644 --- a/src/tools/DevAnalyzers/DevAnalyzers.csproj +++ b/src/tools/DevAnalyzers/DevAnalyzers.csproj @@ -2,7 +2,6 @@ netstandard2.0 - 10 enable diff --git a/src/tools/DevAnalyzers/GenericVirtualAnalyzer.cs b/src/tools/DevAnalyzers/GenericVirtualAnalyzer.cs index 8ecd9119f6..ecd6154e0b 100644 --- a/src/tools/DevAnalyzers/GenericVirtualAnalyzer.cs +++ b/src/tools/DevAnalyzers/GenericVirtualAnalyzer.cs @@ -16,7 +16,7 @@ public class GenericVirtualAnalyzer : DiagnosticAnalyzer "Performance", DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: "Generic virtual methods affect JIT startup time adversly and should be avoided."); + description: "Generic virtual methods affect JIT startup time adversely and should be avoided."); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.cs index f8d5ad826e..3b5d3d8c3f 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.cs @@ -257,7 +257,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator if (cl.Properties.Count > 0) { server = server.AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( - $"protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt){{}}") + $"protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt){{}}") !) .WithBody(ApplyDeserializeChangesEpilogue(deserializeMethodBody, cl))); server = server.AddMembers(MethodDeclaration(ParseTypeName("void"), "OnFieldsDeserialized") @@ -481,7 +481,7 @@ return; private static BlockSyntax DeserializeChangesPrologue(GClass cl) { return Block(ParseStatement($@" -base.DeserializeChangesCore(reader, commitedAt); +base.DeserializeChangesCore(reader, committedAt); DeserializeChangesExtra(reader); var changed = reader.Read<{ChangedFieldsTypeName(cl)}>(); ")); @@ -500,7 +500,7 @@ var changed = reader.Read<{ChangedFieldsTypeName(cl)}>(); { code = $@" if((changed & {changedFieldsType}.{prop.Name}Animated) == {changedFieldsType}.{prop.Name}Animated) - SetAnimatedValue({CompositionPropertyField(prop)}, ref {PropertyBackingFieldName(prop)}, commitedAt, reader.ReadObject()); + SetAnimatedValue({CompositionPropertyField(prop)}, ref {PropertyBackingFieldName(prop)}, committedAt, reader.ReadObject()); else "; } diff --git a/src/tools/DevGenerators/DevGenerators.csproj b/src/tools/DevGenerators/DevGenerators.csproj index 069ff159fc..30da940514 100644 --- a/src/tools/DevGenerators/DevGenerators.csproj +++ b/src/tools/DevGenerators/DevGenerators.csproj @@ -4,7 +4,6 @@ netstandard2.0 enable false - 10 diff --git a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj index 602e5e4496..4cdce8df26 100644 --- a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj +++ b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj @@ -3,7 +3,6 @@ net6.0 Library true - latest @@ -17,6 +16,9 @@ + + + diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index c42cb0241b..030d6ba215 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -12,6 +12,7 @@ using Avalonia.Platform; using Avalonia.Threading; using Avalonia.UnitTests; using Moq; +using Nito.AsyncEx; using Xunit; #nullable enable @@ -920,8 +921,122 @@ namespace Avalonia.Base.UnitTests } } + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + public void Typed_Bind_Executes_On_UIThread(BindingPriority priority) + { + AsyncContext.Run(async () => + { + var target = new Class1(); + var source = new Subject(); + var currentThreadId = Thread.CurrentThread.ManagedThreadId; + var raised = 0; + + var threadingInterfaceMock = new Mock(); + threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) + .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); + + var services = new TestServices( + threadingInterface: threadingInterfaceMock.Object); + + target.PropertyChanged += (s, e) => + { + Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId); + ++raised; + }; + + using (UnitTestApplication.Start(services)) + { + target.Bind(Class1.FooProperty, source, priority); + + await Task.Run(() => source.OnNext("foobar")); + Dispatcher.UIThread.RunJobs(); + + Assert.Equal("foobar", target.GetValue(Class1.FooProperty)); + Assert.Equal(1, raised); + } + }); + } + + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + public void Untyped_Bind_Executes_On_UIThread(BindingPriority priority) + { + AsyncContext.Run(async () => + { + var target = new Class1(); + var source = new Subject(); + var currentThreadId = Thread.CurrentThread.ManagedThreadId; + var raised = 0; + + var threadingInterfaceMock = new Mock(); + threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) + .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); + + var services = new TestServices( + threadingInterface: threadingInterfaceMock.Object); + + target.PropertyChanged += (s, e) => + { + Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId); + ++raised; + }; + + using (UnitTestApplication.Start(services)) + { + target.Bind(Class1.FooProperty, source, priority); + + await Task.Run(() => source.OnNext("foobar")); + Dispatcher.UIThread.RunJobs(); + + Assert.Equal("foobar", target.GetValue(Class1.FooProperty)); + Assert.Equal(1, raised); + } + }); + } + + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + public void BindingValue_Bind_Executes_On_UIThread(BindingPriority priority) + { + AsyncContext.Run(async () => + { + var target = new Class1(); + var source = new Subject>(); + var currentThreadId = Thread.CurrentThread.ManagedThreadId; + var raised = 0; + + var threadingInterfaceMock = new Mock(); + threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) + .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); + + var services = new TestServices( + threadingInterface: threadingInterfaceMock.Object); + + target.PropertyChanged += (s, e) => + { + Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId); + ++raised; + }; + + using (UnitTestApplication.Start(services)) + { + target.Bind(Class1.FooProperty, source, priority); + + await Task.Run(() => source.OnNext("foobar")); + Dispatcher.UIThread.RunJobs(); + + Assert.Equal("foobar", target.GetValue(Class1.FooProperty)); + Assert.Equal(1, raised); + } + }); + } + [Fact] - public async Task Bind_With_Scheduler_Executes_On_Scheduler() + public async Task Bind_With_Scheduler_Executes_On_UI_Thread() { var target = new Class1(); var source = new Subject(); @@ -932,7 +1047,6 @@ namespace Avalonia.Base.UnitTests .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); var services = new TestServices( - scheduler: AvaloniaScheduler.Instance, threadingInterface: threadingInterfaceMock.Object); using (UnitTestApplication.Start(services)) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs index 20172eea88..973090ee92 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs @@ -7,8 +7,10 @@ using System.Threading.Tasks; using Avalonia.Data; using Avalonia.Logging; using Avalonia.Platform; +using Avalonia.Threading; using Avalonia.UnitTests; using Moq; +using Nito.AsyncEx; using Xunit; namespace Avalonia.Base.UnitTests @@ -519,25 +521,39 @@ namespace Avalonia.Base.UnitTests } [Fact] - public async Task Bind_Executes_On_UIThread() + public void Bind_Executes_On_UIThread() { - var target = new Class1(); - var source = new Subject(); - var currentThreadId = Thread.CurrentThread.ManagedThreadId; + AsyncContext.Run(async () => + { + var target = new Class1(); + var source = new Subject(); + var currentThreadId = Thread.CurrentThread.ManagedThreadId; + var raised = 0; - var threadingInterfaceMock = new Mock(); - threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) - .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); + var threadingInterfaceMock = new Mock(); + threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) + .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); - var services = new TestServices( - threadingInterface: threadingInterfaceMock.Object); + var services = new TestServices( + threadingInterface: threadingInterfaceMock.Object); - using (UnitTestApplication.Start(services)) - { - target.Bind(Class1.FooProperty, source); + target.PropertyChanged += (s, e) => + { + Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId); + ++raised; + }; - await Task.Run(() => source.OnNext("foobar")); - } + using (UnitTestApplication.Start(services)) + { + target.Bind(Class1.FooProperty, source); + + await Task.Run(() => source.OnNext("foobar")); + Dispatcher.UIThread.RunJobs(); + + Assert.Equal("foobar", target.Foo); + Assert.Equal(1, raised); + } + }); } [Fact] diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs index f878977cf2..99c4ac3d86 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs @@ -227,7 +227,7 @@ namespace Avalonia.Base.UnitTests { Class2 target = new Class2(); - target.SetValue((AvaloniaProperty)Class2.FlobProperty, new ImplictDouble(4)); + target.SetValue((AvaloniaProperty)Class2.FlobProperty, new ImplicitDouble(4)); var value = target.GetValue(Class2.FlobProperty); Assert.IsType(value); @@ -393,16 +393,16 @@ namespace Avalonia.Base.UnitTests AvaloniaProperty.RegisterAttached("Attached"); } - private class ImplictDouble + private class ImplicitDouble { - public ImplictDouble(double value) + public ImplicitDouble(double value) { Value = value; } public double Value { get; } - public static implicit operator double (ImplictDouble v) + public static implicit operator double (ImplicitDouble v) { return v.Value; } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs index 43192584af..2a8bbaae79 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs @@ -49,7 +49,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ////} [Fact] - public void Indei_Validation_Does_Not_Subscribe_When_DataValidatation_Not_Enabled() + public void Indei_Validation_Does_Not_Subscribe_When_DataValidation_Not_Enabled() { var data = new IndeiTest { MustBePositive = 5 }; var observer = ExpressionObserver.Create(data, o => o.MustBePositive, false); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs index 9b587d7679..6cd757331b 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs @@ -42,7 +42,7 @@ namespace Avalonia.Base.UnitTests.Data.Core } [Fact] - public void Property_Acccess_Expression_Can_Set_Property() + public void Property_Access_Expression_Can_Set_Property() { var data = new Class1(); var target = ExpressionObserver.Create(data, o => o.Foo); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs index 435980bd9a..612f3ff80a 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Lifetime.cs @@ -41,7 +41,7 @@ namespace Avalonia.Base.UnitTests.Data.Core [Fact] public void Should_Complete_When_Update_Observable_Completes() { - var update = new Subject(); + var update = new Subject(); var target = ExpressionObserver.Create(() => 1, o => o, update); var completed = false; @@ -54,7 +54,7 @@ namespace Avalonia.Base.UnitTests.Data.Core [Fact] public void Should_Complete_When_Update_Observable_Errors() { - var update = new Subject(); + var update = new Subject(); var target = ExpressionObserver.Create(() => 1, o => o, update); var completed = false; @@ -87,7 +87,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Should_Unsubscribe_From_Update_Observable() { var scheduler = new TestScheduler(); - var update = scheduler.CreateColdObservable(); + var update = scheduler.CreateColdObservable(); var data = new { Foo = "foo" }; var target = ExpressionObserver.Create(() => data, o => o.Foo, update); var result = new List(); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs index 144d9e6668..22d5645f76 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs @@ -359,14 +359,14 @@ namespace Avalonia.Base.UnitTests.Data.Core public void Empty_Expression_Should_Track_Root() { var data = new Class1 { Foo = "foo" }; - var update = new Subject(); + var update = new Subject(); var target = ExpressionObserver.Create(() => data.Foo, o => o, update); var result = new List(); target.Subscribe(x => result.Add(x)); data.Foo = "bar"; - update.OnNext(Unit.Default); + update.OnNext(default); Assert.Equal(new[] { "foo", "bar" }, result); @@ -533,15 +533,15 @@ namespace Avalonia.Base.UnitTests.Data.Core var first = new Class1 { Foo = "foo" }; var second = new Class1 { Foo = "bar" }; var root = first; - var update = new Subject(); + var update = new Subject(); var target = ExpressionObserver.Create(() => root, o => o.Foo, update); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); root = second; - update.OnNext(Unit.Default); + update.OnNext(default); root = null; - update.OnNext(Unit.Default); + update.OnNext(default); Assert.Equal( new object[] @@ -640,7 +640,7 @@ namespace Avalonia.Base.UnitTests.Data.Core public void RootGetter_Is_Reevaluated_On_Subscribe() { var data = "foo"; - var target = new ExpressionObserver(() => data, new EmptyExpressionNode(), new Subject(), null); + var target = new ExpressionObserver(() => data, new EmptyExpressionNode(), new Subject(), null); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); diff --git a/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs b/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs index 508eff5f9d..d466f6a4a5 100644 --- a/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs @@ -1,7 +1,13 @@ +using System; using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Input.GestureRecognizers; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Threading; using Avalonia.UnitTests; +using Moq; using Xunit; namespace Avalonia.Base.UnitTests.Input @@ -167,6 +173,242 @@ namespace Avalonia.Base.UnitTests.Input Assert.False(raised); } + [Fact] + public void Hold_Should_Be_Raised_After_Hold_Duration() + { + using var scope = AvaloniaLocator.EnterScope(); + var iSettingsMock = new Mock(); + iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300)); + iSettingsMock.Setup(x => x.GetTapSize(It.IsAny())).Returns(new Size(16, 16)); + AvaloniaLocator.CurrentMutable.BindToSelf(this) + .Bind().ToConstant(iSettingsMock.Object); + + var scheduledTimers = new List<(TimeSpan time, Action action)>(); + using var app = UnitTestApplication.Start(new TestServices( + threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t)))); + + Border border = new Border(); + Gestures.SetIsHoldWithMouseEnabled(border, true); + var decorator = new Decorator + { + Child = border + }; + HoldingState holding = HoldingState.Cancelled; + + decorator.AddHandler(Gestures.HoldingEvent, (s, e) => holding = e.HoldingState); + + _mouse.Down(border); + Assert.False(holding != HoldingState.Cancelled); + + // Verify timer duration, but execute it immediately. + var timer = Assert.Single(scheduledTimers); + Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time); + timer.action(); + + Assert.True(holding == HoldingState.Started); + + _mouse.Up(border); + + Assert.True(holding == HoldingState.Completed); + } + + [Fact] + public void Hold_Should_Not_Raised_When_Pointer_Released_Before_Timer() + { + using var scope = AvaloniaLocator.EnterScope(); + var iSettingsMock = new Mock(); + iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300)); + AvaloniaLocator.CurrentMutable.BindToSelf(this) + .Bind().ToConstant(iSettingsMock.Object); + + var scheduledTimers = new List<(TimeSpan time, Action action)>(); + using var app = UnitTestApplication.Start(new TestServices( + threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t)))); + + Border border = new Border(); + Gestures.SetIsHoldWithMouseEnabled(border, true); + var decorator = new Decorator + { + Child = border + }; + var raised = false; + + decorator.AddHandler(Gestures.HoldingEvent, (s, e) => raised = e.HoldingState == HoldingState.Started); + + _mouse.Down(border); + Assert.False(raised); + + _mouse.Up(border); + Assert.False(raised); + + // Verify timer duration, but execute it immediately. + var timer = Assert.Single(scheduledTimers); + Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time); + timer.action(); + + Assert.False(raised); + } + + [Fact] + public void Hold_Should_Not_Raised_When_Pointer_Is_Moved_Before_Timer() + { + using var scope = AvaloniaLocator.EnterScope(); + var iSettingsMock = new Mock(); + iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300)); + AvaloniaLocator.CurrentMutable.BindToSelf(this) + .Bind().ToConstant(iSettingsMock.Object); + + var scheduledTimers = new List<(TimeSpan time, Action action)>(); + using var app = UnitTestApplication.Start(new TestServices( + threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t)))); + + Border border = new Border(); + Gestures.SetIsHoldWithMouseEnabled(border, true); + var decorator = new Decorator + { + Child = border + }; + var raised = false; + + decorator.AddHandler(Gestures.HoldingEvent, (s, e) => raised = e.HoldingState == HoldingState.Completed); + + _mouse.Down(border); + Assert.False(raised); + + _mouse.Move(border, position: new Point(20, 20)); + Assert.False(raised); + + // Verify timer duration, but execute it immediately. + var timer = Assert.Single(scheduledTimers); + Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time); + timer.action(); + + Assert.False(raised); + } + + [Fact] + public void Hold_Should_Be_Cancelled_When_Second_Contact_Is_Detected() + { + using var scope = AvaloniaLocator.EnterScope(); + var iSettingsMock = new Mock(); + iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300)); + AvaloniaLocator.CurrentMutable.BindToSelf(this) + .Bind().ToConstant(iSettingsMock.Object); + + var scheduledTimers = new List<(TimeSpan time, Action action)>(); + using var app = UnitTestApplication.Start(new TestServices( + threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t)))); + + Border border = new Border(); + Gestures.SetIsHoldWithMouseEnabled(border, true); + var decorator = new Decorator + { + Child = border + }; + var cancelled = false; + + decorator.AddHandler(Gestures.HoldingEvent, (s, e) => cancelled = e.HoldingState == HoldingState.Cancelled); + + _mouse.Down(border); + Assert.False(cancelled); + + var timer = Assert.Single(scheduledTimers); + Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time); + timer.action(); + + var secondMouse = new MouseTestHelper(); + + secondMouse.Down(border); + + Assert.True(cancelled); + } + + [Fact] + public void Hold_Should_Be_Cancelled_When_Pointer_Moves_Too_Far() + { + using var scope = AvaloniaLocator.EnterScope(); + var iSettingsMock = new Mock(); + iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300)); + iSettingsMock.Setup(x => x.GetTapSize(It.IsAny())).Returns(new Size(16, 16)); + AvaloniaLocator.CurrentMutable.BindToSelf(this) + .Bind().ToConstant(iSettingsMock.Object); + + var scheduledTimers = new List<(TimeSpan time, Action action)>(); + using var app = UnitTestApplication.Start(new TestServices( + threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t)))); + + Border border = new Border(); + Gestures.SetIsHoldWithMouseEnabled(border, true); + var decorator = new Decorator + { + Child = border + }; + var cancelled = false; + + decorator.AddHandler(Gestures.HoldingEvent, (s, e) => cancelled = e.HoldingState == HoldingState.Cancelled); + + _mouse.Down(border); + + var timer = Assert.Single(scheduledTimers); + Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time); + timer.action(); + + _mouse.Move(border, position: new Point(3, 3)); + + Assert.False(cancelled); + + _mouse.Move(border, position: new Point(20, 20)); + + Assert.True(cancelled); + } + + [Fact] + public void Hold_Should_Not_Be_Raised_For_Multiple_Contacts() + { + using var scope = AvaloniaLocator.EnterScope(); + var iSettingsMock = new Mock(); + iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300)); + AvaloniaLocator.CurrentMutable.BindToSelf(this) + .Bind().ToConstant(iSettingsMock.Object); + + var scheduledTimers = new List<(TimeSpan time, Action action)>(); + using var app = UnitTestApplication.Start(new TestServices( + threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t)))); + + Border border = new Border(); + Gestures.SetIsHoldWithMouseEnabled(border, true); + var decorator = new Decorator + { + Child = border + }; + var raised = false; + + decorator.AddHandler(Gestures.HoldingEvent, (s, e) => raised = e.HoldingState == HoldingState.Completed); + + var secondMouse = new MouseTestHelper(); + + _mouse.Down(border, MouseButton.Left); + + // Verify timer duration, but execute it immediately. + var timer = Assert.Single(scheduledTimers); + Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time); + timer.action(); + + secondMouse.Down(border, MouseButton.Left); + + Assert.False(raised); + } + + private static IPlatformThreadingInterface CreatePlatformThreadingInterface(Action<(TimeSpan, Action)> callback) + { + var threadingInterface = new Mock(); + threadingInterface.SetupGet(p => p.CurrentThreadIsLoopThread).Returns(true); + threadingInterface.Setup(p => p + .StartTimer(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((_, t, a) => callback((t, a))); + return threadingInterface.Object; + } + private static void AddHandlers( Decorator decorator, Border border, @@ -201,5 +443,101 @@ namespace Avalonia.Base.UnitTests.Input border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt")); } + + [Fact] + public void Pinched_Should_Not_Be_Raised_For_Same_Pointer() + { + var touch = new TouchTestHelper(); + + Border border = new Border() + { + Width = 100, + Height = 100, + Background = new SolidColorBrush(Colors.Red) + }; + border.GestureRecognizers.Add(new PinchGestureRecognizer()); + var decorator = new Decorator + { + Child = border + }; + var raised = false; + + decorator.AddHandler(Gestures.PinchEvent, (s, e) => raised = true); + + var firstPoint = new Point(5, 5); + var secondPoint = new Point(10, 10); + + touch.Down(border, position: firstPoint); + touch.Down(border, position: secondPoint); + touch.Down(border, position: new Point(20, 20)); + + Assert.False(raised); + } + + [Fact] + public void Pinched_Should_Be_Raised_For_Two_Pointers_Moving() + { + Border border = new Border() + { + Width = 100, + Height = 100, + Background = new SolidColorBrush(Colors.Red) + }; + border.GestureRecognizers.Add(new PinchGestureRecognizer()); + var decorator = new Decorator + { + Child = border + }; + var raised = false; + + decorator.AddHandler(Gestures.PinchEvent, (s, e) => raised = true); + + var firstPoint = new Point(5, 5); + var secondPoint = new Point(10, 10); + + var firstTouch = new TouchTestHelper(); + var secondTouch = new TouchTestHelper(); + + firstTouch.Down(border, position: firstPoint); + secondTouch.Down(border, position: secondPoint); + secondTouch.Move(border, position: new Point(20, 20)); + + Assert.True(raised); + } + + [Fact] + public void Scrolling_Should_Start_After_Start_Distance_Is_Exceeded() + { + Border border = new Border() + { + Width = 100, + Height = 100, + Background = new SolidColorBrush(Colors.Red) + }; + border.GestureRecognizers.Add(new ScrollGestureRecognizer() + { + CanHorizontallyScroll = true, + CanVerticallyScroll = true, + ScrollStartDistance = 50 + }); + var decorator = new Decorator + { + Child = border + }; + var raised = false; + + decorator.AddHandler(Gestures.ScrollGestureEvent, (s, e) => raised = true); + + var firstTouch = new TouchTestHelper(); + + firstTouch.Down(border, position: new Point(5, 5)); + firstTouch.Move(border, position: new Point(20, 20)); + + Assert.False(raised); + + firstTouch.Move(border, position: new Point(70, 20)); + + Assert.True(raised); + } } } diff --git a/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs b/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs index c0c0182622..36587ea222 100644 --- a/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs @@ -207,7 +207,7 @@ namespace Avalonia.Input.UnitTests private IDisposable UnitTestApp(TimeSpan doubleClickTime = new TimeSpan()) { var unitTestApp = UnitTestApplication.Start( - new TestServices(inputManager: new InputManager())); + new TestServices(inputManager: new InputManager(), threadingInterface: Mock.Of(x => x.CurrentThreadIsLoopThread == true))); var iSettingsMock = new Mock(); iSettingsMock.Setup(x => x.GetDoubleTapTime(It.IsAny())).Returns(doubleClickTime); iSettingsMock.Setup(x => x.GetDoubleTapSize(It.IsAny())).Returns(new Size(16, 16)); diff --git a/tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs b/tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs index 1bb2300724..afc25ab88e 100644 --- a/tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs @@ -97,7 +97,7 @@ namespace Avalonia.Base.UnitTests.Media.Fonts { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) { - var assetLoader = AvaloniaLocator.Current.GetService(); + var assetLoader = AvaloniaLocator.Current.GetRequiredService(); var fontFamily = new FontFamily("resm:Avalonia.Base.UnitTests.Assets?assembly=Avalonia.Base.UnitTests#Noto Mono"); @@ -117,7 +117,7 @@ namespace Avalonia.Base.UnitTests.Media.Fonts private static IDisposable StartWithResources(params (string, string)[] assets) { var assetLoader = new MockAssetLoader(assets); - var services = new TestServices(assetLoader: assetLoader, platform: new AppBuilder().RuntimePlatform); + var services = new TestServices(assetLoader: assetLoader, platform: new StandardRuntimePlatform()); return UnitTestApplication.Start(services); } } diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs index 4e0207a85d..c57bd6c002 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs @@ -42,7 +42,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting enumerator.MoveNext(); - var actual = enumerator.Current.Text; + var actual = text.AsSpan(enumerator.Current.Offset, enumerator.Current.Length); var pass = true; @@ -93,7 +93,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting while (enumerator.MoveNext()) { - Assert.Equal(1, enumerator.Current.Text.Length); + Assert.Equal(1, enumerator.Current.Length); count++; } diff --git a/tests/Avalonia.Base.UnitTests/RectTests.cs b/tests/Avalonia.Base.UnitTests/RectTests.cs index 95a438b287..c44b328ed5 100644 --- a/tests/Avalonia.Base.UnitTests/RectTests.cs +++ b/tests/Avalonia.Base.UnitTests/RectTests.cs @@ -52,7 +52,7 @@ namespace Avalonia.Base.UnitTests double.PositiveInfinity, double.PositiveInfinity) .Normalize(); - Assert.Equal(Rect.Empty, result); + Assert.Equal(default, result); } } } diff --git a/tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs index 27faf1e13c..3d8369faeb 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs @@ -82,6 +82,39 @@ public class CompositorHitTestingTests : CompositorTestsBase } } + [Theory, + InlineData(false, false), + InlineData(true, false), + InlineData(false, true), + InlineData(true, true), + ] + public void HitTest_Should_Find_Zero_Opacity_Controls_At_Point(bool parent, bool child) + { + + using (var s = new CompositorServices(new Size(200, 200))) + { + Border visible, border; + s.TopLevel.Content = border = new Border + { + Width = 100, + Height = 100, + Background = Brushes.Red, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Opacity = parent ? 0 : 1, + Child = visible = new Border + { + Opacity = child ? 0 : 1, + Background = Brushes.Red, + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch, + } + }; + + s.AssertHitTest(new Point(100, 100), null, visible, border); + } + } + [Fact] public void HitTest_Should_Not_Find_Control_Outside_Point() { diff --git a/tests/Avalonia.Base.UnitTests/Rendering/CompositorTestsBase.cs b/tests/Avalonia.Base.UnitTests/Rendering/CompositorTestsBase.cs index d407a09b06..7f4e160000 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/CompositorTestsBase.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/CompositorTestsBase.cs @@ -18,7 +18,6 @@ using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.UnitTests; using Avalonia.VisualTree; -using JetBrains.Annotations; using Xunit; namespace Avalonia.Base.UnitTests.Rendering; diff --git a/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs b/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs index 52a9f58006..4f11af7327 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs @@ -403,7 +403,7 @@ namespace Avalonia.Base.UnitTests.Rendering root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); - root.Renderer.Paint(Rect.Empty); + root.Renderer.Paint(default); var result = root.Renderer.HitTest(new Point(50, 150), root, null).First(); Assert.Equal(item1, result); @@ -419,7 +419,7 @@ namespace Avalonia.Base.UnitTests.Rendering container.InvalidateArrange(); container.Arrange(new Rect(container.DesiredSize)); - root.Renderer.Paint(Rect.Empty); + root.Renderer.Paint(default); result = root.Renderer.HitTest(new Point(50, 150), root, null).First(); Assert.Equal(item2, result); diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs index 2fac968206..07d2d672ae 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs @@ -13,9 +13,9 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph [Fact] public void Empty_Bounds_Remain_Empty() { - var target = new TestDrawOperation(Rect.Empty, Matrix.Identity, null); + var target = new TestDrawOperation(default, Matrix.Identity, null); - Assert.Equal(Rect.Empty, target.Bounds); + Assert.Equal(default, target.Bounds); } [Theory] diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyleActivatorExtensions.cs b/tests/Avalonia.Base.UnitTests/Styling/StyleActivatorExtensions.cs index e69eae43f0..9f7d9854fd 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyleActivatorExtensions.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyleActivatorExtensions.cs @@ -3,6 +3,7 @@ using System.Reactive.Linq; using System.Threading.Tasks; using Avalonia.Reactive; using Avalonia.Styling.Activators; +using Observable = Avalonia.Reactive.Observable; namespace Avalonia.Base.UnitTests.Styling { @@ -10,12 +11,12 @@ namespace Avalonia.Base.UnitTests.Styling { public static IDisposable Subscribe(this IStyleActivator activator, Action action) { - return activator.ToObservable().Subscribe(action); + return Observable.Subscribe(activator.ToObservable(), action); } public static async Task Take(this IStyleActivator activator, int value) { - return await activator.ToObservable().Take(value); + return await System.Reactive.Linq.Observable.Take(activator.ToObservable(), value); } public static IObservable ToObservable(this IStyleActivator activator) diff --git a/tests/Avalonia.Base.UnitTests/VisualTests.cs b/tests/Avalonia.Base.UnitTests/VisualTests.cs index 973893af2d..fb214a6b34 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTests.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTests.cs @@ -126,7 +126,7 @@ namespace Avalonia.Base.UnitTests } [Fact] - public void Root_Should_Retun_Self_As_VisualRoot() + public void Root_Should_Return_Self_As_VisualRoot() { var root = new TestRoot(); @@ -134,7 +134,7 @@ namespace Avalonia.Base.UnitTests } [Fact] - public void Descendants_Should_RetunVisualRoot() + public void Descendants_Should_ReturnVisualRoot() { var root = new TestRoot(); var child1 = new Decorator(); diff --git a/tests/Avalonia.Benchmarks/Data/AccessorTestObject.cs b/tests/Avalonia.Benchmarks/Data/AccessorTestObject.cs index 0039f5670c..cd323ededb 100644 --- a/tests/Avalonia.Benchmarks/Data/AccessorTestObject.cs +++ b/tests/Avalonia.Benchmarks/Data/AccessorTestObject.cs @@ -1,6 +1,5 @@ using System.ComponentModel; using System.Runtime.CompilerServices; -using JetBrains.Annotations; namespace Avalonia.Benchmarks.Data { @@ -38,7 +37,6 @@ namespace Avalonia.Benchmarks.Data { } - [NotifyPropertyChangedInvocator] protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); diff --git a/tests/Avalonia.Benchmarks/NullGlyphRun.cs b/tests/Avalonia.Benchmarks/NullGlyphRun.cs index 5ba0822649..1c2c7c0d7d 100644 --- a/tests/Avalonia.Benchmarks/NullGlyphRun.cs +++ b/tests/Avalonia.Benchmarks/NullGlyphRun.cs @@ -1,4 +1,5 @@ -using Avalonia.Platform; +using System.Collections.Generic; +using Avalonia.Platform; namespace Avalonia.Benchmarks { @@ -7,5 +8,10 @@ namespace Avalonia.Benchmarks public void Dispose() { } + + public IReadOnlyList GetIntersections(float lowerBound, float upperBound) + { + return null; + } } } diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs new file mode 100644 index 0000000000..bf0253e9ab --- /dev/null +++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs @@ -0,0 +1,105 @@ +using System; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.UnitTests; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Text; + +[MemoryDiagnoser] +public class HugeTextLayout : IDisposable +{ + private readonly IDisposable _app; + private string[] _manySmallStrings; + private static Random _rand = new Random(); + + private static string RandomString(int length) + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789&?%$@"; + return new string(Enumerable.Repeat(chars, length).Select(s => s[_rand.Next(s.Length)]).ToArray()); + } + + public HugeTextLayout() + { + _manySmallStrings = Enumerable.Range(0, 1000).Select(x => RandomString(_rand.Next(2, 15))).ToArray(); + _app = UnitTestApplication.Start( + TestServices.StyledWindow.With( + renderInterface: new NullRenderingPlatform(), + threadingInterface: new NullThreadingPlatform(), + standardCursorFactory: new NullCursorFactory())); + } + + private const string Text = @"Though, the objectives of the development of the prominent landmarks can be neglected in most cases, it should be realized that after the completion of the strategic decision gives rise to The Expertise of Regular Program (Carlton Cartwright in The Book of the Key Factor) +A number of key issues arise from the belief that the explicit examination of strategic management should correlate with the conceptual design. +By all means, the unification of the reliably developed techniques indicates the importance of the ultimate advantage of episodic skill over alternate practices. +Let's consider, that the portion of the continuing support can be regarded as relentlessly insignificant. The hardware maintenance focuses our attention on the structure absorption. The real reason of the permanent growth drastically the preliminary action plan the ultimate advantage of useful probability over alternate practices. +Let it not be said that a section of the essential component discards the principle of the more interconnection of critical thinking with productivity boosting of the referential arguments. +One should, however, not forget that concentration of violations of the strategic management requires urgent actions to be taken towards the comprehensive set of policy statements. Therefore, the concept of the design aspects can be treated as the only solution. +In a loose sense concentration of the center of the critical thinking provides a deep insight into the emergency planning. The comparison is quite a powerful matter. +Resulting from review or analysis of threats and opportunities, we can presume that either significant improvement or basics of planning and scheduling reveals the patterns of the final draft. Therefore, the concept of the crucial component can be treated as the only solution. +One should, nevertheless, consider that the exceptional results of the diverse sources of information gives an overview of the production cycle. It should rather be regarded as an integral part of the direct access to key resources. +Admitting that the possibility of achieving the results of the constructive criticism, as far as the strategic management is questionable, cannot rely only on the critical thinking. It may reveal how the systems approach partially the comprehensive project management. We must be ready for outline design stage and network development investigation of every contradiction between the effective time management and the efficient decision the network development. +Everyone understands what it takes to the draft analysis and prior decisions and early design solutions. In any case, we can systematically change the mechanism of the sources and influences of the continuing financing doctrine. This could exceedingly be a result of a task analysis the hardware maintenance. The real reason of the strategic planning seemingly the influence on eventual productivity. Everyone understands what it takes to the well-known practice. Therefore, the concept of the productivity boost can be treated as the only solution the driving factor. +It may reveal how the matters of peculiar interest slowly the goals and objectives or the diverse sources of information the positive influence of any major outcomes complete failure of the supposed theory. +In respect that the structure of the sufficient amount poses problems and challenges for both the set of related commands and controls and the ability bias."; + + [Benchmark] + public TextLayout BuildTextLayout() => MakeLayout(Text); + + private const string Emojis = @"😀 😁 😂 🤣 😃 😄 😅 😆 😉 😊 😋 😎 😍 😘 🥰 😗 😙 😚 ☺️ 🙂 🤗 🤩 🤔 🤨 😐 😑 😶 🙄 😏 😣 😥 😮 🤐 😯 😪 😫 😴 😌 😛 😜 😝 🤤 😒 😓 😔 😕 🙃 🤑 😲 ☹️ 🙁 😖 😞 😟 😤 😢 😭 😦 😧 😨 😩 🤯 😬 😰 😱 🥵 🥶 😳 🤪 😵 😡 😠 🤬 😷 🤒 🤕 🤢 🤮 🤧 😇 🤠 🤡 🥳 🥴 🥺 🤥 🤫 🤭 🧐 🤓 😈 👿 👹 👺 💀 👻 👽 🤖 💩 😺 😸 😹 😻 😼 😽 🙀 😿 😾 +👶 👧 🧒 👦 👩 🧑 👨 👵 🧓 👴 👲 👳‍♀️ 👳‍♂️ 🧕 🧔 👱‍♂️ 👱‍♀️ 👨‍🦰 👩‍🦰 👨‍🦱 👩‍🦱 👨‍🦲 👩‍🦲 👨‍🦳 👩‍🦳 🦸‍♀️ 🦸‍♂️ 🦹‍♀️ 🦹‍♂️ 👮‍♀️ 👮‍♂️ 👷‍♀️ 👷‍♂️ 💂‍♀️ 💂‍♂️ 🕵️‍♀️ 🕵️‍♂️ 👩‍⚕️ 👨‍⚕️ 👩‍🌾 👨‍🌾 👩‍🍳 👨‍🍳 👩‍🎓 👨‍🎓 👩‍🎤 👨‍🎤 👩‍🏫 👨‍🏫 👩‍🏭 👨‍🏭 👩‍💻 👨‍💻 👩‍💼 👨‍💼 👩‍🔧 👨‍🔧 👩‍🔬 👨‍🔬 👩‍🎨 👨‍🎨 👩‍🚒 👨‍🚒 👩‍✈️ 👨‍✈️ 👩‍🚀 👨‍🚀 👩‍⚖️ 👨‍⚖️ 👰 🤵 👸 🤴 🤶 🎅 🧙‍♀️ 🧙‍♂️ 🧝‍♀️ 🧝‍♂️ 🧛‍♀️ 🧛‍♂️ 🧟‍♀️ 🧟‍♂️ 🧞‍♀️ 🧞‍♂️ 🧜‍♀️ 🧜‍♂️ 🧚‍♀️ 🧚‍♂️ 👼 🤰 🤱 🙇‍♀️ 🙇‍♂️ 💁‍♀️ 💁‍♂️ 🙅‍♀️ 🙅‍♂️ 🙆‍♀️ 🙆‍♂️ 🙋‍♀️ 🙋‍♂️ 🤦‍♀️ 🤦‍♂️ 🤷‍♀️ 🤷‍♂️ 🙎‍♀️ 🙎‍♂️ 🙍‍♀️ 🙍‍♂️ 💇‍♀️ 💇‍♂️ 💆‍♀️ 💆‍♂️ 🧖‍♀️ 🧖‍♂️ 💅 🤳 💃 🕺 👯‍♀️ 👯‍♂️ 🕴 🚶‍♀️ 🚶‍♂️ 🏃‍♀️ 🏃‍♂️ 👫 👭 👬 💑 👩‍❤️‍👩 👨‍❤️‍👨 💏 👩‍❤️‍💋‍👩 👨‍❤️‍💋‍👨 👪 👨‍👩‍👧 👨‍👩‍👧‍👦 👨‍👩‍👦‍👦 👨‍👩‍👧‍👧 👩‍👩‍👦 👩‍👩‍👧 👩‍👩‍👧‍👦 👩‍👩‍👦‍👦 👩‍👩‍👧‍👧 👨‍👨‍👦 👨‍👨‍👧 👨‍👨‍👧‍👦 👨‍👨‍👦‍👦 👨‍👨‍👧‍👧 👩‍👦 👩‍👧 👩‍👧‍👦 👩‍👦‍👦 👩‍👧‍👧 👨‍👦 👨‍👧 👨‍👧‍👦 👨‍👦‍👦 👨‍👧‍👧 🤲 👐 🙌 👏 🤝 👍 👎 👊 ✊ 🤛 🤜 🤞 ✌️ 🤟 🤘 👌 👈 👉 👆 👇 ☝️ ✋ 🤚 🖐 🖖 👋 🤙 💪 🦵 🦶 🖕 ✍️ 🙏 💍 💄 💋 👄 👅 👂 👃 👣 👁 👀 🧠 🦴 🦷 🗣 👤 👥 +🧥 👚 👕 👖 👔 👗 👙 👘 👠 👡 👢 👞 👟 🥾 🥿 🧦 🧤 🧣 🎩 🧢 👒 🎓 ⛑ 👑 👝 👛 👜 💼 🎒 👓 🕶 🥽 🥼 🌂 🧵 🧶 +👶🏻 👦🏻 👧🏻 👨🏻 👩🏻 👱🏻‍♀️ 👱🏻 👴🏻 👵🏻 👲🏻 👳🏻‍♀️ 👳🏻 👮🏻‍♀️ 👮🏻 👷🏻‍♀️ 👷🏻 💂🏻‍♀️ 💂🏻 🕵🏻‍♀️ 🕵🏻 👩🏻‍⚕️ 👨🏻‍⚕️ 👩🏻‍🌾 👨🏻‍🌾 👩🏻‍🍳 👨🏻‍🍳 👩🏻‍🎓 👨🏻‍🎓 👩🏻‍🎤 👨🏻‍🎤 👩🏻‍🏫 👨🏻‍🏫 👩🏻‍🏭 👨🏻‍🏭 👩🏻‍💻 👨🏻‍💻 👩🏻‍💼 👨🏻‍💼 👩🏻‍🔧 👨🏻‍🔧 👩🏻‍🔬 👨🏻‍🔬 👩🏻‍🎨 👨🏻‍🎨 👩🏻‍🚒 👨🏻‍🚒 👩🏻‍✈️ 👨🏻‍✈️ 👩🏻‍🚀 👨🏻‍🚀 👩🏻‍⚖️ 👨🏻‍⚖️ 🤶🏻 🎅🏻 👸🏻 🤴🏻 👰🏻 🤵🏻 👼🏻 🤰🏻 🙇🏻‍♀️ 🙇🏻 💁🏻 💁🏻‍♂️ 🙅🏻 🙅🏻‍♂️ 🙆🏻 🙆🏻‍♂️ 🙋🏻 🙋🏻‍♂️ 🤦🏻‍♀️ 🤦🏻‍♂️ 🤷🏻‍♀️ 🤷🏻‍♂️ 🙎🏻 🙎🏻‍♂️ 🙍🏻 🙍🏻‍♂️ 💇🏻 💇🏻‍♂️ 💆🏻 💆🏻‍♂️ 🕴🏻 💃🏻 🕺🏻 🚶🏻‍♀️ 🚶🏻 🏃🏻‍♀️ 🏃🏻 🤲🏻 👐🏻 🙌🏻 👏🏻 🙏🏻 👍🏻 👎🏻 👊🏻 ✊🏻 🤛🏻 🤜🏻 🤞🏻 ✌🏻 🤟🏻 🤘🏻 👌🏻 👈🏻 👉🏻 👆🏻 👇🏻 ☝🏻 ✋🏻 🤚🏻 🖐🏻 🖖🏻 👋🏻 🤙🏻 💪🏻 🖕🏻 ✍🏻 🤳🏻 💅🏻 👂🏻 👃🏻 +👶🏼 👦🏼 👧🏼 👨🏼 👩🏼 👱🏼‍♀️ 👱🏼 👴🏼 👵🏼 👲🏼 👳🏼‍♀️ 👳🏼 👮🏼‍♀️ 👮🏼 👷🏼‍♀️ 👷🏼 💂🏼‍♀️ 💂🏼 🕵🏼‍♀️ 🕵🏼 👩🏼‍⚕️ 👨🏼‍⚕️ 👩🏼‍🌾 👨🏼‍🌾 👩🏼‍🍳 👨🏼‍🍳 👩🏼‍🎓 👨🏼‍🎓 👩🏼‍🎤 👨🏼‍🎤 👩🏼‍🏫 👨🏼‍🏫 👩🏼‍🏭 👨🏼‍🏭 👩🏼‍💻 👨🏼‍💻 👩🏼‍💼 👨🏼‍💼 👩🏼‍🔧 👨🏼‍🔧 👩🏼‍🔬 👨🏼‍🔬 👩🏼‍🎨 👨🏼‍🎨 👩🏼‍🚒 👨🏼‍🚒 👩🏼‍✈️ 👨🏼‍✈️ 👩🏼‍🚀 👨🏼‍🚀 👩🏼‍⚖️ 👨🏼‍⚖️ 🤶🏼 🎅🏼 👸🏼 🤴🏼 👰🏼 🤵🏼 👼🏼 🤰🏼 🙇🏼‍♀️ 🙇🏼 💁🏼 💁🏼‍♂️ 🙅🏼 🙅🏼‍♂️ 🙆🏼 🙆🏼‍♂️ 🙋🏼 🙋🏼‍♂️ 🤦🏼‍♀️ 🤦🏼‍♂️ 🤷🏼‍♀️ 🤷🏼‍♂️ 🙎🏼 🙎🏼‍♂️ 🙍🏼 🙍🏼‍♂️ 💇🏼 💇🏼‍♂️ 💆🏼 💆🏼‍♂️ 🕴🏼 💃🏼 🕺🏼 🚶🏼‍♀️ 🚶🏼 🏃🏼‍♀️ 🏃🏼 🤲🏼 👐🏼 🙌🏼 👏🏼 🙏🏼 👍🏼 👎🏼 👊🏼 ✊🏼 🤛🏼 🤜🏼 🤞🏼 ✌🏼 🤟🏼 🤘🏼 👌🏼 👈🏼 👉🏼 👆🏼 👇🏼 ☝🏼 ✋🏼 🤚🏼 🖐🏼 🖖🏼 👋🏼 🤙🏼 💪🏼 🖕🏼 ✍🏼 🤳🏼 💅🏼 👂🏼 👃🏼 +👶🏽 👦🏽 👧🏽 👨🏽 👩🏽 👱🏽‍♀️ 👱🏽 👴🏽 👵🏽 👲🏽 👳🏽‍♀️ 👳🏽 👮🏽‍♀️ 👮🏽 👷🏽‍♀️ 👷🏽 💂🏽‍♀️ 💂🏽 🕵🏽‍♀️ 🕵🏽 👩🏽‍⚕️ 👨🏽‍⚕️ 👩🏽‍🌾 👨🏽‍🌾 👩🏽‍🍳 👨🏽‍🍳 👩🏽‍🎓 👨🏽‍🎓 👩🏽‍🎤 👨🏽‍🎤 👩🏽‍🏫 👨🏽‍🏫 👩🏽‍🏭 👨🏽‍🏭 👩🏽‍💻 👨🏽‍💻 👩🏽‍💼 👨🏽‍💼 👩🏽‍🔧 👨🏽‍🔧 👩🏽‍🔬 👨🏽‍🔬 👩🏽‍🎨 👨🏽‍🎨 👩🏽‍🚒 👨🏽‍🚒 👩🏽‍✈️ 👨🏽‍✈️ 👩🏽‍🚀 👨🏽‍🚀 👩🏽‍⚖️ 👨🏽‍⚖️ 🤶🏽 🎅🏽 👸🏽 🤴🏽 👰🏽 🤵🏽 👼🏽 🤰🏽 🙇🏽‍♀️ 🙇🏽 💁🏽 💁🏽‍♂️ 🙅🏽 🙅🏽‍♂️ 🙆🏽 🙆🏽‍♂️ 🙋🏽 🙋🏽‍♂️ 🤦🏽‍♀️ 🤦🏽‍♂️ 🤷🏽‍♀️ 🤷🏽‍♂️ 🙎🏽 🙎🏽‍♂️ 🙍🏽 🙍🏽‍♂️ 💇🏽 💇🏽‍♂️ 💆🏽 💆🏽‍♂️ 🕴🏼 💃🏽 🕺🏽 🚶🏽‍♀️ 🚶🏽 🏃🏽‍♀️ 🏃🏽 🤲🏽 👐🏽 🙌🏽 👏🏽 🙏🏽 👍🏽 👎🏽 👊🏽 ✊🏽 🤛🏽 🤜🏽 🤞🏽 ✌🏽 🤟🏽 🤘🏽 👌🏽 👈🏽 👉🏽 👆🏽 👇🏽 ☝🏽 ✋🏽 🤚🏽 🖐🏽 🖖🏽 👋🏽 🤙🏽 💪🏽 🖕🏽 ✍🏽 🤳🏽 💅🏽 👂🏽 👃🏽 +👶🏾 👦🏾 👧🏾 👨🏾 👩🏾 👱🏾‍♀️ 👱🏾 👴🏾 👵🏾 👲🏾 👳🏾‍♀️ 👳🏾 👮🏾‍♀️ 👮🏾 👷🏾‍♀️ 👷🏾 💂🏾‍♀️ 💂🏾 🕵🏾‍♀️ 🕵🏾 👩🏾‍⚕️ 👨🏾‍⚕️ 👩🏾‍🌾 👨🏾‍🌾 👩🏾‍🍳 👨🏾‍🍳 👩🏾‍🎓 👨🏾‍🎓 👩🏾‍🎤 👨🏾‍🎤 👩🏾‍🏫 👨🏾‍🏫 👩🏾‍🏭 👨🏾‍🏭 👩🏾‍💻 👨🏾‍💻 👩🏾‍💼 👨🏾‍💼 👩🏾‍🔧 👨🏾‍🔧 👩🏾‍🔬 👨🏾‍🔬 👩🏾‍🎨 👨🏾‍🎨 👩🏾‍🚒 👨🏾‍🚒 👩🏾‍✈️ 👨🏾‍✈️ 👩🏾‍🚀 👨🏾‍🚀 👩🏾‍⚖️ 👨🏾‍⚖️ 🤶🏾 🎅🏾 👸🏾 🤴🏾 👰🏾 🤵🏾 👼🏾 🤰🏾 🙇🏾‍♀️ 🙇🏾 💁🏾 💁🏾‍♂️ 🙅🏾 🙅🏾‍♂️ 🙆🏾 🙆🏾‍♂️ 🙋🏾 🙋🏾‍♂️ 🤦🏾‍♀️ 🤦🏾‍♂️ 🤷🏾‍♀️ 🤷🏾‍♂️ 🙎🏾 🙎🏾‍♂️ 🙍🏾 🙍🏾‍♂️ 💇🏾 💇🏾‍♂️ 💆🏾 💆🏾‍♂️ 🕴🏾 💃🏾 🕺🏾 🚶🏾‍♀️ 🚶🏾 🏃🏾‍♀️ 🏃🏾 🤲🏾 👐🏾 🙌🏾 👏🏾 🙏🏾 👍🏾 👎🏾 👊🏾 ✊🏾 🤛🏾 🤜🏾 🤞🏾 ✌🏾 🤟🏾 🤘🏾 👌🏾 👈🏾 👉🏾 👆🏾 👇🏾 ☝🏾 ✋🏾 🤚🏾 🖐🏾 🖖🏾 👋🏾 🤙🏾 💪🏾 🖕🏾 ✍🏾 🤳🏾 💅🏾 👂🏾 👃🏾 +👶🏿 👦🏿 👧🏿 👨🏿 👩🏿 👱🏿‍♀️ 👱🏿 👴🏿 👵🏿 👲🏿 👳🏿‍♀️ 👳🏿 👮🏿‍♀️ 👮🏿 👷🏿‍♀️ 👷🏿 💂🏿‍♀️ 💂🏿 🕵🏿‍♀️ 🕵🏿 👩🏿‍⚕️ 👨🏿‍⚕️ 👩🏿‍🌾 👨🏿‍🌾 👩🏿‍🍳 👨🏿‍🍳 👩🏿‍🎓 👨🏿‍🎓 👩🏿‍🎤 👨🏿‍🎤 👩🏿‍🏫 👨🏿‍🏫 👩🏿‍🏭 👨🏿‍🏭 👩🏿‍💻 👨🏿‍💻 👩🏿‍💼 👨🏿‍💼 👩🏿‍🔧 👨🏿‍🔧 👩🏿‍🔬 👨🏿‍🔬 👩🏿‍🎨 👨🏿‍🎨 👩🏿‍🚒 👨🏿‍🚒 👩🏿‍✈️ 👨🏿‍✈️ 👩🏿‍🚀 👨🏿‍🚀 👩🏿‍⚖️ 👨🏿‍⚖️ 🤶🏿 🎅🏿 👸🏿 🤴🏿 👰🏿 🤵🏿 👼🏿 🤰🏿 🙇🏿‍♀️ 🙇🏿 💁🏿 💁🏿‍♂️ 🙅🏿 🙅🏿‍♂️ 🙆🏿 🙆🏿‍♂️ 🙋🏿 🙋🏿‍♂️ 🤦🏿‍♀️ 🤦🏿‍♂️ 🤷🏿‍♀️ 🤷🏿‍♂️ 🙎🏿 🙎🏿‍♂️ 🙍🏿 🙍🏿‍♂️ 💇🏿 💇🏿‍♂️ 💆🏿 💆🏿‍♂️ 🕴🏿 💃🏿 🕺🏿 🚶🏿‍♀️ 🚶🏿 🏃🏿‍♀️ 🏃🏿 🤲🏿 👐🏿 🙌🏿 👏🏿 🙏🏿 👍🏿 👎🏿 👊🏿 ✊🏿 🤛🏿 🤜🏿 🤞🏿 ✌🏿 🤟🏿 🤘🏿 👌🏿 👈🏿 👉🏿 👆🏿 👇🏿 ☝🏿 ✋🏿 🤚🏿 🖐🏿 🖖🏿 👋🏿 🤙🏿 💪🏿 🖕🏿 ✍🏿 🤳🏿 💅🏿 👂🏿 👃🏿 +🐶 🐱 🐭 🐹 🐰 🦊 🦝 🐻 🐼 🦘 🦡 🐨 🐯 🦁 🐮 🐷 🐽 🐸 🐵 🙈 🙉 🙊 🐒 🐔 🐧 🐦 🐤 🐣 🐥 🦆 🦢 🦅 🦉 🦚 🦜 🦇 🐺 🐗 🐴 🦄 🐝 🐛 🦋 🐌 🐚 🐞 🐜 🦗 🕷 🕸 🦂 🦟 🦠 🐢 🐍 🦎 🦖 🦕 🐙 🦑 🦐 🦀 🐡 🐠 🐟 🐬 🐳 🐋 🦈 🐊 🐅 🐆 🦓 🦍 🐘 🦏 🦛 🐪 🐫 🦙 🦒 🐃 🐂 🐄 🐎 🐖 🐏 🐑 🐐 🦌 🐕 🐩 🐈 🐓 🦃 🕊 🐇 🐁 🐀 🐿 🦔 🐾 🐉 🐲 🌵 🎄 🌲 🌳 🌴 🌱 🌿 ☘️ 🍀 🎍 🎋 🍃 🍂 🍁 🍄 🌾 💐 🌷 🌹 🥀 🌺 🌸 🌼 🌻 🌞 🌝 🌛 🌜 🌚 🌕 🌖 🌗 🌘 🌑 🌒 🌓 🌔 🌙 🌎 🌍 🌏 💫 ⭐️ 🌟 ✨ ⚡️ ☄️ 💥 🔥 🌪 🌈 ☀️ 🌤 ⛅️ 🌥 ☁️ 🌦 🌧 ⛈ 🌩 🌨 ❄️ ☃️ ⛄️ 🌬 💨 💧 💦 ☔️ ☂️ 🌊 🌫 +🍏 🍎 🍐 🍊 🍋 🍌 🍉 🍇 🍓 🍈 🍒 🍑 🍍 🥭 🥥 🥝 🍅 🍆 🥑 🥦 🥒 🥬 🌶 🌽 🥕 🥔 🍠 🥐 🍞 🥖 🥨 🥯 🧀 🥚 🍳 🥞 🥓 🥩 🍗 🍖 🌭 🍔 🍟 🍕 🥪 🥙 🌮 🌯 🥗 🥘 🥫 🍝 🍜 🍲 🍛 🍣 🍱 🥟 🍤 🍙 🍚 🍘 🍥 🥮 🥠 🍢 🍡 🍧 🍨 🍦 🥧 🍰 🎂 🍮 🍭 🍬 🍫 🍿 🧂 🍩 🍪 🌰 🥜 🍯 🥛 🍼 ☕️ 🍵 🥤 🍶 🍺 🍻 🥂 🍷 🥃 🍸 🍹 🍾 🥄 🍴 🍽 🥣 🥡 🥢 +⚽️ 🏀 🏈 ⚾️ 🥎 🏐 🏉 🎾 🥏 🎱 🏓 🏸 🥅 🏒 🏑 🥍 🏏 ⛳️ 🏹 🎣 🥊 🥋 🎽 ⛸ 🥌 🛷 🛹 🎿 ⛷ 🏂 🏋️‍♀️ 🏋🏻‍♀️ 🏋🏼‍♀️ 🏋🏽‍♀️ 🏋🏾‍♀️ 🏋🏿‍♀️ 🏋️‍♂️ 🏋🏻‍♂️ 🏋🏼‍♂️ 🏋🏽‍♂️ 🏋🏾‍♂️ 🏋🏿‍♂️ 🤼‍♀️ 🤼‍♂️ 🤸‍♀️ 🤸🏻‍♀️ 🤸🏼‍♀️ 🤸🏽‍♀️ 🤸🏾‍♀️ 🤸🏿‍♀️ 🤸‍♂️ 🤸🏻‍♂️ 🤸🏼‍♂️ 🤸🏽‍♂️ 🤸🏾‍♂️ 🤸🏿‍♂️ ⛹️‍♀️ ⛹🏻‍♀️ ⛹🏼‍♀️ ⛹🏽‍♀️ ⛹🏾‍♀️ ⛹🏿‍♀️ ⛹️‍♂️ ⛹🏻‍♂️ ⛹🏼‍♂️ ⛹🏽‍♂️ ⛹🏾‍♂️ ⛹🏿‍♂️ 🤺 🤾‍♀️ 🤾🏻‍♀️ 🤾🏼‍♀️ 🤾🏾‍♀️ 🤾🏾‍♀️ 🤾🏿‍♀️ 🤾‍♂️ 🤾🏻‍♂️ 🤾🏼‍♂️ 🤾🏽‍♂️ 🤾🏾‍♂️ 🤾🏿‍♂️ 🏌️‍♀️ 🏌🏻‍♀️ 🏌🏼‍♀️ 🏌🏽‍♀️ 🏌🏾‍♀️ 🏌🏿‍♀️ 🏌️‍♂️ 🏌🏻‍♂️ 🏌🏼‍♂️ 🏌🏽‍♂️ 🏌🏾‍♂️ 🏌🏿‍♂️ 🏇 🏇🏻 🏇🏼 🏇🏽 🏇🏾 🏇🏿 🧘‍♀️ 🧘🏻‍♀️ 🧘🏼‍♀️ 🧘🏽‍♀️ 🧘🏾‍♀️ 🧘🏿‍♀️ 🧘‍♂️ 🧘🏻‍♂️ 🧘🏼‍♂️ 🧘🏽‍♂️ 🧘🏾‍♂️ 🧘🏿‍♂️ 🏄‍♀️ 🏄🏻‍♀️ 🏄🏼‍♀️ 🏄🏽‍♀️ 🏄🏾‍♀️ 🏄🏿‍♀️ 🏄‍♂️ 🏄🏻‍♂️ 🏄🏼‍♂️ 🏄🏽‍♂️ 🏄🏾‍♂️ 🏄🏿‍♂️ 🏊‍♀️ 🏊🏻‍♀️ 🏊🏼‍♀️ 🏊🏽‍♀️ 🏊🏾‍♀️ 🏊🏿‍♀️ 🏊‍♂️ 🏊🏻‍♂️ 🏊🏼‍♂️ 🏊🏽‍♂️ 🏊🏾‍♂️ 🏊🏿‍♂️ 🤽‍♀️ 🤽🏻‍♀️ 🤽🏼‍♀️ 🤽🏽‍♀️ 🤽🏾‍♀️ 🤽🏿‍♀️ 🤽‍♂️ 🤽🏻‍♂️ 🤽🏼‍♂️ 🤽🏽‍♂️ 🤽🏾‍♂️ 🤽🏿‍♂️ 🚣‍♀️ 🚣🏻‍♀️ 🚣🏼‍♀️ 🚣🏽‍♀️ 🚣🏾‍♀️ 🚣🏿‍♀️ 🚣‍♂️ 🚣🏻‍♂️ 🚣🏼‍♂️ 🚣🏽‍♂️ 🚣🏾‍♂️ 🚣🏿‍♂️ 🧗‍♀️ 🧗🏻‍♀️ 🧗🏼‍♀️ 🧗🏽‍♀️ 🧗🏾‍♀️ 🧗🏿‍♀️ 🧗‍♂️ 🧗🏻‍♂️ 🧗🏼‍♂️ 🧗🏽‍♂️ 🧗🏾‍♂️ 🧗🏿‍♂️ 🚵‍♀️ 🚵🏻‍♀️ 🚵🏼‍♀️ 🚵🏽‍♀️ 🚵🏾‍♀️ 🚵🏿‍♀️ 🚵‍♂️ 🚵🏻‍♂️ 🚵🏼‍♂️ 🚵🏽‍♂️ 🚵🏾‍♂️ 🚵🏿‍♂️ 🚴‍♀️ 🚴🏻‍♀️ 🚴🏼‍♀️ 🚴🏽‍♀️ 🚴🏾‍♀️ 🚴🏿‍♀️ 🚴‍♂️ 🚴🏻‍♂️ 🚴🏼‍♂️ 🚴🏽‍♂️ 🚴🏾‍♂️ 🚴🏿‍♂️ 🏆 🥇 🥈 🥉 🏅 🎖 🏵 🎗 🎫 🎟 🎪 🤹‍♀️ 🤹🏻‍♀️ 🤹🏼‍♀️ 🤹🏽‍♀️ 🤹🏾‍♀️ 🤹🏿‍♀️ 🤹‍♂️ 🤹🏻‍♂️ 🤹🏼‍♂️ 🤹🏽‍♂️ 🤹🏾‍♂️ 🤹🏿‍♂️ 🎭 🎨 🎬 🎤 🎧 🎼 🎹 🥁 🎷 🎺 🎸 🎻 🎲 🧩 ♟ 🎯 🎳 🎮 🎰 +🚗 🚕 🚙 🚌 🚎 🏎 🚓 🚑 🚒 🚐 🚚 🚛 🚜 🛴 🚲 🛵 🏍 🚨 🚔 🚍 🚘 🚖 🚡 🚠 🚟 🚃 🚋 🚞 🚝 🚄 🚅 🚈 🚂 🚆 🚇 🚊 🚉 ✈️ 🛫 🛬 🛩 💺 🛰 🚀 🛸 🚁 🛶 ⛵️ 🚤 🛥 🛳 ⛴ 🚢 ⚓️ ⛽️ 🚧 🚦 🚥 🚏 🗺 🗿 🗽 🗼 🏰 🏯 🏟 🎡 🎢 🎠 ⛲️ ⛱ 🏖 🏝 🏜 🌋 ⛰ 🏔 🗻 🏕 ⛺️ 🏠 🏡 🏘 🏚 🏗 🏭 🏢 🏬 🏣 🏤 🏥 🏦 🏨 🏪 🏫 🏩 💒 🏛 ⛪️ 🕌 🕍 🕋 ⛩ 🛤 🛣 🗾 🎑 🏞 🌅 🌄 🌠 🎇 🎆 🌇 🌆 🏙 🌃 🌌 🌉 🌁 +⌚️ 📱 📲 💻 ⌨️ 🖥 🖨 🖱 🖲 🕹 🗜 💽 💾 💿 📀 📼 📷 📸 📹 🎥 📽 🎞 📞 ☎️ 📟 📠 📺 📻 🎙 🎚 🎛 ⏱ ⏲ ⏰ 🕰 ⌛️ ⏳ 📡 🔋 🔌 💡 🔦 🕯 🗑 🛢 💸 💵 💴 💶 💷 💰 💳 🧾 💎 ⚖️ 🔧 🔨 ⚒ 🛠 ⛏ 🔩 ⚙️ ⛓ 🔫 💣 🔪 🗡 ⚔️ 🛡 🚬 ⚰️ ⚱️ 🏺 🧭 🧱 🔮 🧿 🧸 📿 💈 ⚗️ 🔭 🧰 🧲 🧪 🧫 🧬 🧯 🔬 🕳 💊 💉 🌡 🚽 🚰 🚿 🛁 🛀 🛀🏻 🛀🏼 🛀🏽 🛀🏾 🛀🏿 🧴 🧵 🧶 🧷 🧹 🧺 🧻 🧼 🧽 🛎 🔑 🗝 🚪 🛋 🛏 🛌 🖼 🛍 🧳 🛒 🎁 🎈 🎏 🎀 🎊 🎉 🧨 🎎 🏮 🎐 🧧 ✉️ 📩 📨 📧 💌 📥 📤 📦 🏷 📪 📫 📬 📭 📮 📯 📜 📃 📄 📑 📊 📈 📉 🗒 🗓 📆 📅 📇 🗃 🗳 🗄 📋 📁 📂 🗂 🗞 📰 📓 📔 📒 📕 📗 📘 📙 📚 📖 🔖 🔗 📎 🖇 📐 📏 📌 📍 ✂️ 🖊 🖋 ✒️ 🖌 🖍 📝 ✏️ 🔍 🔎 🔏 🔐 🔒 🔓 +❤️ 🧡 💛 💚 💙 💜 🖤 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ⏏️ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ ♾ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 🔜 ✔️ ☑️ 🔘 ⚪️ ⚫️ 🔴 🔵 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 💬 💭 🗯 ♠️ ♣️ ♥️ ♦️ 🃏 🎴 🀄️ 🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛 🕜 🕝 🕞 🕟 🕠 🕡 🕢 🕣 🕤 🕥 🕦 🕧 +🏳️ 🏴 🏁 🚩 🏳️‍🌈 🏴‍☠️ 🇦🇫 🇦🇽 🇦🇱 🇩🇿 🇦🇸 🇦🇩 🇦🇴 🇦🇮 🇦🇶 🇦🇬 🇦🇷 🇦🇲 🇦🇼 🇦🇺 🇦🇹 🇦🇿 🇧🇸 🇧🇭 🇧🇩 🇧🇧 🇧🇾 🇧🇪 🇧🇿 🇧🇯 🇧🇲 🇧🇹 🇧🇴 🇧🇦 🇧🇼 🇧🇷 🇮🇴 🇻🇬 🇧🇳 🇧🇬 🇧🇫 🇧🇮 🇰🇭 🇨🇲 🇨🇦 🇮🇨 🇨🇻 🇧🇶 🇰🇾 🇨🇫 🇹🇩 🇨🇱 🇨🇳 🇨🇽 🇨🇨 🇨🇴 🇰🇲 🇨🇬 🇨🇩 🇨🇰 🇨🇷 🇨🇮 🇭🇷 🇨🇺 🇨🇼 🇨🇾 🇨🇿 🇩🇰 🇩🇯 🇩🇲 🇩🇴 🇪🇨 🇪🇬 🇸🇻 🇬🇶 🇪🇷 🇪🇪 🇪🇹 🇪🇺 🇫🇰 🇫🇴 🇫🇯 🇫🇮 🇫🇷 🇬🇫 🇵🇫 🇹🇫 🇬🇦 🇬🇲 🇬🇪 🇩🇪 🇬🇭 🇬🇮 🇬🇷 🇬🇱 🇬🇩 🇬🇵 🇬🇺 🇬🇹 🇬🇬 🇬🇳 🇬🇼 🇬🇾 🇭🇹 🇭🇳 🇭🇰 🇭🇺 🇮🇸 🇮🇳 🇮🇩 🇮🇷 🇮🇶 🇮🇪 🇮🇲 🇮🇱 🇮🇹 🇯🇲 🇯🇵 🎌 🇯🇪 🇯🇴 🇰🇿 🇰🇪 🇰🇮 🇽🇰 🇰🇼 🇰🇬 🇱🇦 🇱🇻 🇱🇧 🇱🇸 🇱🇷 🇱🇾 🇱🇮 🇱🇹 🇱🇺 🇲🇴 🇲🇰 🇲🇬 🇲🇼 🇲🇾 🇲🇻 🇲🇱 🇲🇹 🇲🇭 🇲🇶 🇲🇷 🇲🇺 🇾🇹 🇲🇽 🇫🇲 🇲🇩 🇲🇨 🇲🇳 🇲🇪 🇲🇸 🇲🇦 🇲🇿 🇲🇲 🇳🇦 🇳🇷 🇳🇵 🇳🇱 🇳🇨 🇳🇿 🇳🇮 🇳🇪 🇳🇬 🇳🇺 🇳🇫 🇰🇵 🇲🇵 🇳🇴 🇴🇲 🇵🇰 🇵🇼 🇵🇸 🇵🇦 🇵🇬 🇵🇾 🇵🇪 🇵🇭 🇵🇳 🇵🇱 🇵🇹 🇵🇷 🇶🇦 🇷🇪 🇷🇴 🇷🇺 🇷🇼 🇼🇸 🇸🇲 🇸🇦 🇸🇳 🇷🇸 🇸🇨 🇸🇱 🇸🇬 🇸🇽 🇸🇰 🇸🇮 🇬🇸 🇸🇧 🇸🇴 🇿🇦 🇰🇷 🇸🇸 🇪🇸 🇱🇰 🇧🇱 🇸🇭 🇰🇳 🇱🇨 🇵🇲 🇻🇨 🇸🇩 🇸🇷 🇸🇿 🇸🇪 🇨🇭 🇸🇾 🇹🇼 🇹🇯 🇹🇿 🇹🇭 🇹🇱 🇹🇬 🇹🇰 🇹🇴 🇹🇹 🇹🇳 🇹🇷 🇹🇲 🇹🇨 🇹🇻 🇻🇮 🇺🇬 🇺🇦 🇦🇪 🇬🇧 🏴󠁧󠁢󠁥󠁮󠁧󠁿 🏴󠁧󠁢󠁳󠁣󠁴󠁿 🏴󠁧󠁢󠁷󠁬󠁳󠁿 🇺🇳 🇺🇸 🇺🇾 🇺🇿 🇻🇺 🇻🇦 🇻🇪 🇻🇳 🇼🇫 🇪🇭 🇾🇪 🇿🇲 🇿🇼 +🥱 🤏 🦾 🦿 🦻 🧏 🧏‍♂️ 🧏‍♀️ 🧍 🧍‍♂️ 🧍‍♀️ 🧎 🧎‍♂️ 🧎‍♀️ 👨‍🦯 👩‍🦯 👨‍🦼 👩‍🦼 👨‍🦽 👩‍🦽 🦧 🦮 🐕‍🦺 🦥 🦦 🦨 🦩 🧄 🧅 🧇 🧆 🧈 🦪 🧃 🧉 🧊 🛕 🦽 🦼 🛺 🪂 🪐 🤿 🪀 🪁 🦺 🥻 🩱 🩲 🩳 🩰 🪕 🪔 🪓 🦯 🩸 🩹 🩺 🪑 🪒 🤎 🤍 🟠 🟡 🟢 🟣 🟤 🟥 🟧 🟨 🟩 🟦 🟪 🟫"; + + [Benchmark] + public TextLayout BuildEmojisTextLayout() => MakeLayout(Emojis); + + [Benchmark] + public TextLayout[] BuildManySmallTexts() => _manySmallStrings.Select(MakeLayout).ToArray(); + + [Benchmark] + public void VirtualizeTextBlocks() + { + var blocks = new TextBlock[10]; + for (var i = 0; i < blocks.Length; i++) + { + blocks[i] = new TextBlock + { + Width = 120, + Height = 32, + }; + } + + for (int i = 0, j = 0; i < _manySmallStrings.Length; i++, j = j < blocks.Length - 1 ? j + 1 : 0) + { + blocks[j].Text = _manySmallStrings[i]; + blocks[j].Measure(new Size(200, 200)); + } + } + + private static TextLayout MakeLayout(string str) + { + var layout = new TextLayout(str, Typeface.Default, 12d, Brushes.Black, maxWidth: 120); + layout.Dispose(); + return layout; + } + + public void Dispose() + { + _app?.Dispose(); + } +} diff --git a/tests/Avalonia.Benchmarks/Utilities/AvaloniaPropertyDictionaryBenchmarks.cs b/tests/Avalonia.Benchmarks/Utilities/AvaloniaPropertyDictionaryBenchmarks.cs index d43d6bd48b..e160c8dfa8 100644 --- a/tests/Avalonia.Benchmarks/Utilities/AvaloniaPropertyDictionaryBenchmarks.cs +++ b/tests/Avalonia.Benchmarks/Utilities/AvaloniaPropertyDictionaryBenchmarks.cs @@ -172,7 +172,7 @@ internal sealed class AvaloniaPropertyValueStoreOld internal class MockProperty : StyledProperty { - public MockProperty([JetBrains.Annotations.NotNull] string name) : base(name, typeof(object), new StyledPropertyMetadata()) + public MockProperty(string name) : base(name, typeof(object), new StyledPropertyMetadata()) { } } diff --git a/tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj b/tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj index eb1bf24d0c..20c2f711ad 100644 --- a/tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj +++ b/tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj @@ -1,7 +1,6 @@ net6.0 - latest Library true diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj index 12eb290fde..471f19f948 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -1,7 +1,6 @@  net6.0 - latest Library true diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index d6c521decd..baf933bd66 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -596,7 +596,7 @@ namespace Avalonia.Controls.UnitTests private static Window PreparedWindow(object content = null) { var renderer = new Mock(); - var platform = AvaloniaLocator.Current.GetService(); + 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/FlyoutTests.cs b/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs index 496b5bc1b6..02767a21eb 100644 --- a/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs +++ b/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs @@ -570,7 +570,7 @@ namespace Avalonia.Controls.UnitTests private static Window PreparedWindow(object content = null) { var renderer = new Mock(); - var platform = AvaloniaLocator.Current.GetService(); + 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/MaskedTextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs index 90c6fd3b21..c1d9fad6f4 100644 --- a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs @@ -827,7 +827,7 @@ namespace Avalonia.Controls.UnitTests { AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); - var clipboard = AvaloniaLocator.CurrentMutable.GetService(); + var clipboard = AvaloniaLocator.CurrentMutable.GetRequiredService(); clipboard.SetTextAsync(textInput).GetAwaiter().GetResult(); RaiseKeyEvent(target, Key.V, KeyModifiers.Control); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 08fd777ac6..469bcecfb9 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -569,8 +569,8 @@ namespace Avalonia.Controls.UnitTests.Presenters var target = CreateTarget(itemCount: 10); var items = (IList)target.Items; target.ApplyTemplate(); - target.Measure(Size.Empty); - target.Arrange(Rect.Empty); + target.Measure(default); + target.Arrange(default); // Check for issue #591: this should not throw. target.ScrollIntoView(0); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index d4193d33ee..65957fda6d 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -564,7 +564,7 @@ namespace Avalonia.Controls.UnitTests.Primitives using (CreateServices()) { var renderer = new Mock(); - var platform = AvaloniaLocator.Current.GetService(); + 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/Selection/SelectionModelTests_Single.cs b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs index 668af3b5d7..1b629be695 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Specialized; +using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Selection; using Avalonia.Controls.Utils; @@ -1144,6 +1145,24 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal(new[] { "foo" }, target.SelectedItems); Assert.Equal(0, target.AnchorIndex); } + + [Fact] + public void SelectedItems_Indexer_Is_Correct() + { + // Issue #7974 + var target = CreateTarget(); + var raised = 0; + + target.SelectionChanged += (s, e) => + { + Assert.Equal("bar", e.SelectedItems.First()); + Assert.Equal("bar", e.SelectedItems[0]); + ++raised; + }; + + target.Select(1); + Assert.Equal(1, raised); + } } public class BatchUpdate diff --git a/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs b/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs index 8d8ce10d4c..067d709ae1 100644 --- a/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs @@ -18,7 +18,7 @@ namespace Avalonia.Controls.UnitTests.Shapes target.Measure(new Size(100, 100)); var geometry = Assert.IsType(target.RenderedGeometry); - Assert.Equal(Rect.Empty, geometry.Rect); + Assert.Equal(default, geometry.Rect); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index bd6d5d55e2..531a2869cd 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -767,7 +767,7 @@ namespace Avalonia.Controls.UnitTests { AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); - var clipboard = AvaloniaLocator.CurrentMutable.GetService(); + var clipboard = AvaloniaLocator.CurrentMutable.GetRequiredService(); clipboard.SetTextAsync(textInput).GetAwaiter().GetResult(); RaiseKeyEvent(target, Key.V, KeyModifiers.Control); @@ -876,7 +876,7 @@ namespace Avalonia.Controls.UnitTests AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); - var clipboard = AvaloniaLocator.CurrentMutable.GetService(); + var clipboard = AvaloniaLocator.CurrentMutable.GetRequiredService(); clipboard.SetTextAsync(Environment.NewLine).GetAwaiter().GetResult(); RaiseKeyEvent(target, Key.V, KeyModifiers.Control); diff --git a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs new file mode 100644 index 0000000000..aaa1de4da4 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs @@ -0,0 +1,66 @@ +using System; +using Avalonia.LogicalTree; +using Avalonia.UnitTests; +using Xunit; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Animation; + +namespace Avalonia.Controls.UnitTests +{ + public class TransitioningContentControlTests + { + [Fact] + public void Old_Content_Shuold_Be_Removed__From_Logical_Tree_After_Out_Animation() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var testTransition = new TestTransition(); + + var target = new TransitioningContentControl(); + target.PageTransition = testTransition; + + var root = new TestRoot() { Child = target }; + + var oldControl = new Control(); + var newControl = new Control(); + + target.Content = oldControl; + Threading.Dispatcher.UIThread.RunJobs(); + + Assert.Equal(target, oldControl.GetLogicalParent()); + Assert.Equal(null, newControl.GetLogicalParent()); + + testTransition.BeginTransition += isFrom => + { + // Old out + if (isFrom) + { + Assert.Equal(target, oldControl.GetLogicalParent()); + Assert.Equal(null, newControl.GetLogicalParent()); + } + // New in + else + { + Assert.Equal(null, oldControl.GetLogicalParent()); + Assert.Equal(target, newControl.GetLogicalParent()); + } + }; + + target.Content = newControl; + Threading.Dispatcher.UIThread.RunJobs(); + } + } + } + public class TestTransition : IPageTransition + { + public event Action BeginTransition; + + public Task Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken) + { + bool isFrom = from != null && to == null; + BeginTransition?.Invoke(isFrom); + return Task.CompletedTask; + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index cd38bf556a..f526465b9b 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -17,7 +17,6 @@ using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.Styling; using Avalonia.UnitTests; -using JetBrains.Annotations; using Moq; using Xunit; @@ -1249,7 +1248,7 @@ namespace Avalonia.Controls.UnitTests using (Application()) { var focus = FocusManager.Instance; - var navigation = AvaloniaLocator.Current.GetService(); + var navigation = AvaloniaLocator.Current.GetRequiredService(); var data = CreateTestTreeData(); var target = new TreeView @@ -1294,7 +1293,6 @@ namespace Avalonia.Controls.UnitTests using (Application()) { var focus = FocusManager.Instance; - var navigation = AvaloniaLocator.Current.GetService(); var data = CreateTestTreeData(); var selectedNode = new Node { Value = "Out of Tree Selected Item" }; @@ -1354,7 +1352,7 @@ namespace Avalonia.Controls.UnitTests var rootNode = tree[0]; - var keymap = AvaloniaLocator.Current.GetService(); + var keymap = AvaloniaLocator.Current.GetRequiredService(); var selectAllGesture = keymap.SelectAll.First(); var keyEvent = new KeyEventArgs @@ -1401,7 +1399,7 @@ namespace Avalonia.Controls.UnitTests ClickContainer(fromContainer, KeyModifiers.None); ClickContainer(toContainer, KeyModifiers.Shift); - var keymap = AvaloniaLocator.Current.GetService(); + var keymap = AvaloniaLocator.Current.GetRequiredService(); var selectAllGesture = keymap.SelectAll.First(); var keyEvent = new KeyEventArgs @@ -1448,7 +1446,7 @@ namespace Avalonia.Controls.UnitTests ClickContainer(fromContainer, KeyModifiers.None); ClickContainer(toContainer, KeyModifiers.Shift); - var keymap = AvaloniaLocator.Current.GetService(); + var keymap = AvaloniaLocator.Current.GetRequiredService(); var selectAllGesture = keymap.SelectAll.First(); var keyEvent = new KeyEventArgs diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 435d0d92ce..ca245005c2 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -140,7 +140,7 @@ namespace Avalonia.Controls.UnitTests [Theory] [InlineData(true)] [InlineData(false)] - public void Child_windows_should_be_closed_before_parent(bool programaticClose) + public void Child_windows_should_be_closed_before_parent(bool programmaticClose) { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -155,12 +155,16 @@ namespace Avalonia.Controls.UnitTests window.Closing += (sender, e) => { + Assert.Equal(WindowCloseReason.WindowClosing, e.CloseReason); + Assert.Equal(programmaticClose, e.IsProgrammatic); count++; windowClosing = count; }; child.Closing += (sender, e) => { + Assert.Equal(WindowCloseReason.OwnerWindowClosing, e.CloseReason); + Assert.Equal(programmaticClose, e.IsProgrammatic); count++; childClosing = count; }; @@ -180,13 +184,13 @@ namespace Avalonia.Controls.UnitTests window.Show(); child.Show(window); - if (programaticClose) + if (programmaticClose) { window.Close(); } else { - var cancel = window.PlatformImpl.Closing(); + var cancel = window.PlatformImpl.Closing(WindowCloseReason.WindowClosing); Assert.Equal(false, cancel); } @@ -201,7 +205,7 @@ namespace Avalonia.Controls.UnitTests [Theory] [InlineData(true)] [InlineData(false)] - public void Child_windows_must_not_close_before_parent_has_chance_to_Cancel_OSCloseButton(bool programaticClose) + public void Child_windows_must_not_close_before_parent_has_chance_to_Cancel_OSCloseButton(bool programmaticClose) { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -242,13 +246,13 @@ namespace Avalonia.Controls.UnitTests window.Show(); child.Show(window); - if (programaticClose) + if (programmaticClose) { window.Close(); } else { - var cancel = window.PlatformImpl.Closing(); + var cancel = window.PlatformImpl.Closing(WindowCloseReason.WindowClosing); Assert.Equal(true, cancel); } diff --git a/tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package-lock.json b/tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package-lock.json index eb57cfb8da..0c0857b680 100644 --- a/tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package-lock.json +++ b/tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package-lock.json @@ -389,9 +389,9 @@ "dev": true }, "ansi-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", - "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "ansi-styles": { @@ -405,9 +405,9 @@ } }, "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "requires": { "normalize-path": "^3.0.0", @@ -444,18 +444,6 @@ "sprintf-js": "~1.0.2" } }, - "array.prototype.map": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.2.tgz", - "integrity": "sha512-Az3OYxgsa1g7xDYp86l0nnN4bcmuEITGe1rbdEBVkrqkzMgDcbdQ2R7r41pNzti+4NMces3H8gMmuioZUilLgw==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "es-array-method-boxes-properly": "^1.0.0", - "is-string": "^1.0.4" - } - }, "assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -469,9 +457,9 @@ "dev": true }, "binary-extensions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", - "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, "brace-expansion": { @@ -538,9 +526,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -554,19 +542,19 @@ "dev": true }, "chokidar": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", - "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, "requires": { - "anymatch": "~3.1.1", + "anymatch": "~3.1.2", "braces": "~3.0.2", - "fsevents": "~2.1.2", - "glob-parent": "~5.1.0", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", - "readdirp": "~3.4.0" + "readdirp": "~3.6.0" } }, "clean-stack": { @@ -576,42 +564,14 @@ "dev": true }, "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, "color-convert": { @@ -708,15 +668,6 @@ "strip-bom": "^4.0.0" } }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -724,68 +675,23 @@ "dev": true }, "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "es-abstract": { - "version": "1.17.6", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", - "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.0", - "is-regex": "^1.1.0", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - }, - "es-array-method-boxes-properly": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", - "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, - "es-get-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.0.tgz", - "integrity": "sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==", - "dev": true, - "requires": { - "es-abstract": "^1.17.4", - "has-symbols": "^1.0.1", - "is-arguments": "^1.0.4", - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-string": "^1.0.5", - "isarray": "^2.0.5" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, "es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -829,13 +735,10 @@ } }, "flat": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", - "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", - "dev": true, - "requires": { - "is-buffer": "~2.0.3" - } + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true }, "foreground-child": { "version": "2.0.0", @@ -860,18 +763,12 @@ "dev": true }, "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "optional": true }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, "gensync": { "version": "1.0.0-beta.1", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", @@ -911,9 +808,9 @@ } }, "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "requires": { "is-glob": "^4.0.1" @@ -931,33 +828,12 @@ "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", "dev": true }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "has-symbols": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", - "dev": true - }, "hasha": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.0.tgz", @@ -1008,12 +884,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "is-arguments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", - "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", - "dev": true - }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1023,51 +893,27 @@ "binary-extensions": "^2.0.0" } }, - "is-buffer": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", - "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", - "dev": true - }, - "is-callable": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.1.tgz", - "integrity": "sha512-wliAfSzx6V+6WfMOmus1xy0XvSgf/dlStkvTfq7F0g4bOIW0PSUbnyse3NhDwdyYS1ozfUtAAySqTws3z9Eqgg==", - "dev": true - }, - "is-date-object": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", - "dev": true - }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "requires": { "is-extglob": "^2.1.1" } }, - "is-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz", - "integrity": "sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==", - "dev": true - }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -1075,24 +921,9 @@ "dev": true }, "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true - }, - "is-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", - "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", - "dev": true, - "requires": { - "has-symbols": "^1.0.1" - } - }, - "is-set": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz", - "integrity": "sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true }, "is-stream": { @@ -1101,39 +932,24 @@ "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", "dev": true }, - "is-string": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", - "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", - "dev": true - }, - "is-symbol": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", - "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", - "dev": true, - "requires": { - "has-symbols": "^1.0.1" - } - }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "dev": true }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true }, - "isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1222,22 +1038,6 @@ "istanbul-lib-report": "^3.0.0" } }, - "iterate-iterator": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.1.tgz", - "integrity": "sha512-3Q6tudGN05kbkDQDI4CqjaBf4qf85w6W6GnuZDtUVYwKgtC1q8yxYX7CZed7N+tLzQqS6roujWvszf13T+n9aw==", - "dev": true - }, - "iterate-value": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz", - "integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==", - "dev": true, - "requires": { - "es-get-iterator": "^1.0.2", - "iterate-iterator": "^1.0.1" - } - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1261,13 +1061,10 @@ "dev": true }, "json5": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", - "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz", + "integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==", + "dev": true }, "locate-path": { "version": "6.0.0", @@ -1291,12 +1088,13 @@ "dev": true }, "log-symbols": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", - "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, "requires": { - "chalk": "^4.0.0" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" } }, "loose-envify": { @@ -1332,43 +1130,133 @@ "brace-expansion": "^1.1.7" } }, - "minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true - }, "mocha": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.1.3.tgz", - "integrity": "sha512-ZbaYib4hT4PpF4bdSO2DohooKXIn4lDeiYqB+vTmCdr6l2woW0b6H3pf5x4sM5nwQMru9RvjjHYWVGltR50ZBw==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", "dev": true, "requires": { "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", - "chokidar": "3.4.2", - "debug": "4.1.1", - "diff": "4.0.2", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", "escape-string-regexp": "4.0.0", "find-up": "5.0.0", - "glob": "7.1.6", - "growl": "1.10.5", + "glob": "7.2.0", "he": "1.2.0", - "js-yaml": "3.14.0", - "log-symbols": "4.0.0", - "minimatch": "3.0.4", - "ms": "2.1.2", - "object.assign": "4.1.0", - "promise.allsettled": "1.0.2", - "serialize-javascript": "4.0.0", - "strip-json-comments": "3.0.1", - "supports-color": "7.1.0", - "which": "2.0.2", - "wide-align": "1.1.3", - "workerpool": "6.0.0", - "yargs": "13.3.2", - "yargs-parser": "13.1.2", - "yargs-unparser": "1.6.1" + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + } + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "moq.ts": { @@ -1386,6 +1274,12 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true + }, "node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -1570,30 +1464,6 @@ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true }, - "object-inspect": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", - "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", - "dev": true - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1604,12 +1474,12 @@ } }, "p-limit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz", - "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "requires": { - "p-try": "^2.0.0" + "yocto-queue": "^0.1.0" } }, "p-locate": { @@ -1679,9 +1549,9 @@ "dev": true }, "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, "pkg-dir": { @@ -1741,19 +1611,6 @@ "fromentries": "^1.2.0" } }, - "promise.allsettled": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.2.tgz", - "integrity": "sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg==", - "dev": true, - "requires": { - "array.prototype.map": "^1.0.1", - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "function-bind": "^1.1.1", - "iterate-value": "^1.0.0" - } - }, "prop-types": { "version": "15.7.2", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", @@ -1792,9 +1649,9 @@ "dev": true }, "readdirp": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", - "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "requires": { "picomatch": "^2.2.1" @@ -1858,9 +1715,9 @@ "dev": true }, "serialize-javascript": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", - "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", "dev": true, "requires": { "randombytes": "^2.1.0" @@ -1938,42 +1795,23 @@ "dev": true }, "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "string.prototype.trimend": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", - "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "string.prototype.trimstart": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", - "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" } }, "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^5.0.1" } }, "strip-bom": { @@ -1983,9 +1821,9 @@ "dev": true }, "strip-json-comments": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", - "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, "supports-color": { @@ -2090,82 +1928,21 @@ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "dev": true }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, "workerpool": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.0.tgz", - "integrity": "sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", "dev": true }, "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } }, "wrappy": { @@ -2193,214 +1970,57 @@ "dev": true }, "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" }, "dependencies": { - "ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } } } }, "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true }, "yargs-unparser": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.1.tgz", - "integrity": "sha512-qZV14lK9MWsGCmcr7u5oXGH0dbGqZAIxTDrWXZDo5zUr6b6iUmelNKO6x6R1dQT24AH3LgRxJpr8meWy2unolA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, "requires": { - "camelcase": "^5.3.1", - "decamelize": "^1.2.0", - "flat": "^4.1.0", - "is-plain-obj": "^1.1.0", - "yargs": "^14.2.3" + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" }, "dependencies": { - "ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "yargs": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz", - "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^15.0.1" - } - }, - "yargs-parser": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz", - "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } } } }, @@ -2409,6 +2029,12 @@ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true } } } diff --git a/tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package.json b/tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package.json index dbb12e192a..580c4059ba 100644 --- a/tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package.json +++ b/tests/Avalonia.DesignerSupport.Tests/Remote/HtmlTransport/webapp/package.json @@ -16,7 +16,7 @@ "@types/mocha": "8.0.3", "@types/react": "^16.3.14", "chai": "^4.2.0", - "mocha": "^8.1.3", + "mocha": "^10.2.0", "moq.ts": "^6.4.0", "nyc": "^15.1.0", "react": "^16.3.2", diff --git a/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj b/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj index 03d9332051..57338a1e08 100644 --- a/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj +++ b/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj @@ -15,4 +15,5 @@ + diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index 53c186111d..656c2cbbbc 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -334,7 +334,7 @@ namespace Avalonia.Markup.UnitTests.Data Path = "Foo", }; - var result = binding.Initiate(target, TextBox.TextProperty).Subject; + var result = binding.Initiate(target, TextBox.TextProperty).Value; Assert.IsType(((BindingExpression)result).Converter); } @@ -350,7 +350,7 @@ namespace Avalonia.Markup.UnitTests.Data Path = "Foo", }; - var result = binding.Initiate(target, TextBox.TextProperty).Subject; + var result = binding.Initiate(target, TextBox.TextProperty).Value; Assert.Same(converter.Object, ((BindingExpression)result).Converter); } @@ -367,7 +367,7 @@ namespace Avalonia.Markup.UnitTests.Data Path = "Bar", }; - var result = binding.Initiate(target, TextBox.TextProperty).Subject; + var result = binding.Initiate(target, TextBox.TextProperty).Value; Assert.Same("foo", ((BindingExpression)result).ConverterParameter); } diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs index 44829aae1e..45deb97f51 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs @@ -20,7 +20,7 @@ namespace Avalonia.Markup.UnitTests.Data var target = new Binding(nameof(Class1.Foo)); var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: false); - var subject = (BindingExpression)instanced.Subject; + var subject = (BindingExpression)instanced.Value; object result = null; subject.Subscribe(x => result = x); @@ -38,7 +38,7 @@ namespace Avalonia.Markup.UnitTests.Data var target = new Binding(nameof(Class1.Foo)); var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true); - var subject = (BindingExpression)instanced.Subject; + var subject = (BindingExpression)instanced.Value; object result = null; subject.Subscribe(x => result = x); @@ -56,7 +56,7 @@ namespace Avalonia.Markup.UnitTests.Data var target = new Binding(nameof(Class1.Foo)) { Priority = BindingPriority.Template }; var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true); - var subject = (BindingExpression)instanced.Subject; + var subject = (BindingExpression)instanced.Value; object result = null; subject.Subscribe(x => result = x); diff --git a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs index cb5c625c35..a7ef2c4e4d 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs @@ -84,6 +84,7 @@ namespace Avalonia.Markup.UnitTests.Data target.Bind(TextBlock.TextProperty, binding); + Assert.NotNull(target.Text); Assert.Equal("fallback", target.Text); } @@ -106,6 +107,7 @@ namespace Avalonia.Markup.UnitTests.Data target.Bind(TextBlock.TextProperty, binding); + Assert.NotNull(target.Text); Assert.Equal("(null)", target.Text); } @@ -128,6 +130,7 @@ namespace Avalonia.Markup.UnitTests.Data target.Bind(TextBlock.TextProperty, binding); + Assert.NotNull(target.Text); Assert.Equal("1,2,(unset)", target.Text); } @@ -150,6 +153,7 @@ namespace Avalonia.Markup.UnitTests.Data target.Bind(TextBlock.TextProperty, binding); + Assert.NotNull(target.Text); Assert.Equal("1,2,Fallback", target.Text); } 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 6dce5eaab5..fa4957c24c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -3,7 +3,6 @@ net6.0;net47 Library true - latest diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/GeometryTypeConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/GeometryTypeConverterTests.cs new file mode 100644 index 0000000000..329a14afa6 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/GeometryTypeConverterTests.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Converters +{ + public class GeometryTypeConverterTests: XamlTestBase + { + public class StringDataViewModel + { + public string PathData { get; set; } + } + + public class IntDataViewModel + { + public int PathData { get; set; } + } + + + [Theory] + [MemberData(nameof(Get_GeometryTypeConverter_Data))] + public void GeometryTypeConverter_Value_Work(object vm, bool nullData) + { + using(UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var path = window.FindControl("path"); + window.DataContext = vm; + Assert.Equal(nullData, path.Data is null); + } + } + + public static IEnumerable Get_GeometryTypeConverter_Data() + { + yield return new object[] { new StringDataViewModel { }, true }; + yield return new object[] { new StringDataViewModel { PathData = "M406.39,333.45l205.93,0" }, false }; + yield return new object[] { new IntDataViewModel { }, true }; + yield return new object[] { new IntDataViewModel { PathData = 100 }, true }; + } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs index d51d6122cd..59a17a8cd1 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs @@ -166,6 +166,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data [Fact] public void Binding_Method_To_Command_Collected() { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + WeakReference MakeRef() { var weakVm = new WeakReference(null); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OnPlatformExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OnPlatformExtensionTests.cs deleted file mode 100644 index 1d37378010..0000000000 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OnPlatformExtensionTests.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Platform; -using Xunit; - -namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions; - -public class OnPlatformExtensionTests : XamlTestBase -{ - [Fact] - public void Should_Resolve_Default_Value() - { - using (AvaloniaLocator.EnterScope()) - { - AvaloniaLocator.CurrentMutable.Bind() - .ToConstant(new TestRuntimePlatform(OperatingSystemType.Unknown)); - - var xaml = @" - - -"; - - var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); - var textBlock = (TextBlock)userControl.Content!; - - Assert.Equal("Hello World", textBlock.Text); - } - } - - [Theory] - [InlineData(OperatingSystemType.WinNT, "Im Windows")] - [InlineData(OperatingSystemType.OSX, "Im macOS")] - [InlineData(OperatingSystemType.Linux, "Im Linux")] - [InlineData(OperatingSystemType.Android, "Im Android")] - [InlineData(OperatingSystemType.iOS, "Im iOS")] - [InlineData(OperatingSystemType.Browser, "Im Browser")] - [InlineData(OperatingSystemType.Unknown, "Default value")] - public void Should_Resolve_Expected_Value_Per_Platform(OperatingSystemType currentPlatform, string expectedResult) - { - using (AvaloniaLocator.EnterScope()) - { - AvaloniaLocator.CurrentMutable.Bind() - .ToConstant(new TestRuntimePlatform(currentPlatform)); - - var xaml = @" - - -"; - - var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); - var textBlock = (TextBlock)userControl.Content!; - - Assert.Equal(expectedResult, textBlock.Text); - } - } - - private class TestRuntimePlatform : StandardRuntimePlatform - { - private readonly OperatingSystemType _operatingSystemType; - - public TestRuntimePlatform(OperatingSystemType operatingSystemType) - { - _operatingSystemType = operatingSystemType; - } - - public override RuntimePlatformInfo GetRuntimeInfo() - { - return new RuntimePlatformInfo() { OperatingSystem = _operatingSystemType }; - } - } -} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OptionsMarkupExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OptionsMarkupExtensionTests.cs index 2d1f961743..0c67f385e1 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OptionsMarkupExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OptionsMarkupExtensionTests.cs @@ -434,7 +434,7 @@ public class OptionsMarkupExtensionTests : XamlTestBase "; + Text='{local:OptionsMarkupExtensionNoServiceProvider OptionB=""Im Option 2"", OptionA=""Im Option 1""}' />"; var textBlock = (TextBlock)AvaloniaRuntimeXamlLoader.Load(xaml); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index 18a6dd9803..0cdc9ee3b1 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -59,7 +59,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } [Fact] - public void Attached_Property_Is_Set_On_Control_Outside_Avalonia_Namspace() + public void Attached_Property_Is_Set_On_Control_Outside_Avalonia_Namespace() { // Test for issue #1548 var xaml = @@ -729,7 +729,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } [Fact] - public void DeferedXamlLoader_Should_Preserve_NamespacesContext() + public void DeferredXamlLoader_Should_Preserve_NamespacesContext() { var xaml = @" - - + + - - + + @@ -41,7 +41,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml window.ApplyTemplate(); button.ApplyTemplate(); - var listBoxHierarchyLine = button.GetVisualChildren().ElementAt(0) as ListBoxHierachyLine; + var listBoxHierarchyLine = button.GetVisualChildren().ElementAt(0) as ListBoxHierarchyLine; Assert.Equal(1, listBoxHierarchyLine.LineDashStyle.Offset); Assert.Equal(2, listBoxHierarchyLine.LineDashStyle.Dashes.Count); Assert.Equal(2, listBoxHierarchyLine.LineDashStyle.Dashes[0]); @@ -309,10 +309,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal("Bar", bar.Name); } } - public class ListBoxHierachyLine : Panel + public class ListBoxHierarchyLine : Panel { public static readonly StyledProperty LineDashStyleProperty = - AvaloniaProperty.Register(nameof(LineDashStyle)); + AvaloniaProperty.Register(nameof(LineDashStyle)); public DashStyle LineDashStyle { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs index 520abee59a..92807b2cb9 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs @@ -1,6 +1,8 @@ using System; +using System.Runtime.CompilerServices; using System.Xml; using Avalonia.Controls; +using Avalonia.Data; using Avalonia.Media; using Avalonia.Styling; using Xunit; @@ -9,6 +11,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml; public class MergeResourceIncludeTests { + static MergeResourceIncludeTests() + { + RuntimeHelpers.RunClassConstructor(typeof(RelativeSource).TypeHandle); + } + [Fact] public void MergeResourceInclude_Works_With_Single_Resource() { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs index d76e51f419..5d6d4a78e4 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using Avalonia.Controls; +using Avalonia.Data; using Avalonia.Markup.Xaml.Styling; using Avalonia.Markup.Xaml.XamlIl.Runtime; using Avalonia.Media; @@ -15,6 +17,12 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml; public class StyleIncludeTests { + static StyleIncludeTests() + { + RuntimeHelpers.RunClassConstructor(typeof(RelativeSource).TypeHandle); + AssetLoader.RegisterResUriParsers(); + } + [Fact] public void StyleInclude_Is_Built() { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 70a5295008..ae1b12572e 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -272,7 +272,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } [Fact] - public void Style_Can_Use_NthChild_Selector_After_Reoder() + public void Style_Can_Use_NthChild_Selector_After_Reorder() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -311,7 +311,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } [Fact] - public void Style_Can_Use_NthLastChild_Selector_After_Reoder() + public void Style_Can_Use_NthLastChild_Selector_After_Reorder() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs index 4b32a8cdca..f42f787117 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs @@ -15,7 +15,6 @@ using Avalonia.Styling; using Avalonia.Threading; using Avalonia.UnitTests; using Avalonia.VisualTree; -using JetBrains.Annotations; using Xunit; namespace Avalonia.Markup.Xaml.UnitTests @@ -389,7 +388,6 @@ namespace Avalonia.Markup.Xaml.UnitTests public bool IsPressed { get; set; } public event PropertyChangedEventHandler PropertyChanged; - [NotifyPropertyChangedInvocator] protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs index 196375fb40..30326adba5 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoSuspendHelperTest.cs @@ -47,7 +47,7 @@ namespace Avalonia.ReactiveUI.UnitTests using (var lifetime = new ClassicDesktopStyleApplicationLifetime()) { var isLaunchingReceived = false; - var application = AvaloniaLocator.Current.GetService(); + var application = AvaloniaLocator.Current.GetRequiredService(); application.ApplicationLifetime = lifetime; // Initialize ReactiveUI Suspension as in real-world scenario. @@ -65,7 +65,7 @@ namespace Avalonia.ReactiveUI.UnitTests using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) using (var lifetime = new ExoticApplicationLifetimeWithoutLifecycleEvents()) { - var application = AvaloniaLocator.Current.GetService(); + var application = AvaloniaLocator.Current.GetRequiredService(); application.ApplicationLifetime = lifetime; Assert.Throws(() => new AutoSuspendHelper(application.ApplicationLifetime)); } @@ -88,7 +88,7 @@ namespace Avalonia.ReactiveUI.UnitTests using (var lifetime = new ClassicDesktopStyleApplicationLifetime()) { var shouldPersistReceived = false; - var application = AvaloniaLocator.Current.GetService(); + var application = AvaloniaLocator.Current.GetRequiredService(); application.ApplicationLifetime = lifetime; // Initialize ReactiveUI Suspension as in real-world scenario. diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetSubject.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaObjectTests_GetSubject.cs similarity index 95% rename from tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetSubject.cs rename to tests/Avalonia.ReactiveUI.UnitTests/AvaloniaObjectTests_GetSubject.cs index 0f2e8ebc21..81f04dc53e 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_GetSubject.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaObjectTests_GetSubject.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; -using System.Reactive.Linq; using Xunit; -namespace Avalonia.Base.UnitTests +namespace Avalonia.ReactiveUI.UnitTests { public class AvaloniaObjectTests_GetSubject { diff --git a/tests/Avalonia.RenderTests/Controls/AdornerTests.cs b/tests/Avalonia.RenderTests/Controls/AdornerTests.cs new file mode 100644 index 0000000000..c833017212 --- /dev/null +++ b/tests/Avalonia.RenderTests/Controls/AdornerTests.cs @@ -0,0 +1,73 @@ +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Layout; +using Avalonia.Media; +using Xunit; + +#if AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests; +#else +namespace Avalonia.Direct2D1.RenderTests.Controls; +#endif + +public class AdornerTests : TestBase +{ + public AdornerTests() + : base(@"Controls\Adorner") + { + } + + [Fact] + public async Task Focus_Adorner_Is_Properly_Clipped() + { + Border adorned; + var tree = new Decorator + { + Child = new VisualLayerManager + { + Child = new Border + { + Background = Brushes.Red, + Padding = new Thickness(10, 50, 10,10), + Child = new Border() + { + Background = Brushes.White, + ClipToBounds = true, + Padding = new Thickness(0, -30, 0, 0), + Child = adorned = new Border + { + Background = Brushes.Green, + VerticalAlignment = VerticalAlignment.Top, + Height = 100, + Width = 50 + } + } + } + }, + Width = 200, + Height = 200 + }; + var adorner = new Border + { + BorderThickness = new Thickness(2), + BorderBrush = Brushes.Black + }; + + var size = new Size(tree.Width, tree.Height); + tree.Measure(size); + tree.Arrange(new Rect(size)); + + + adorned.AttachedToVisualTree += delegate + { + AdornerLayer.SetAdornedElement(adorner, adorned); + AdornerLayer.GetAdornerLayer(adorned)!.Children.Add(adorner); + }; + tree.Measure(size); + tree.Arrange(new Rect(size)); + + await RenderToFile(tree); + CompareImages(skipImmediate: true, skipDeferred: true); + } +} \ No newline at end of file diff --git a/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs b/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs index bddde3c3c6..c11bd2b816 100644 --- a/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs +++ b/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs @@ -45,7 +45,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls [Win32Fact("Has text")] public async Task RestrictedHeight_VerticalAlign() { - Control text(VerticalAlignment verticalAlingnment, bool clip = true, bool restrictHeight = true) + Control text(VerticalAlignment verticalAlignment, bool clip = true, bool restrictHeight = true) { return new Border() { @@ -62,7 +62,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls FontSize = 24, Foreground = Brushes.Black, Text = "L", - VerticalAlignment = verticalAlingnment, + VerticalAlignment = verticalAlignment, ClipToBounds = clip } }; diff --git a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs index f921d9fa64..6f47aa58d8 100644 --- a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs @@ -186,7 +186,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media [InlineData("x", 0, 200, 200 - 7.20, 0, 7.20, FontSizeHeight)] [InlineData(stringword, 0, 200, 171.20, 0, 7.20, FontSizeHeight)] [InlineData(stringword, 3, 200, 200 - 7.20, 0, 7.20, FontSizeHeight)] - public void Should_HitTestPosition_RigthAlign_Correctly( + public void Should_HitTestPosition_RightAlign_Correctly( string input, int index, double widthConstraint, double x, double y, double width, double height) { diff --git a/tests/Avalonia.RenderTests/OpacityMaskTests.cs b/tests/Avalonia.RenderTests/OpacityMaskTests.cs index 2f01b03db6..6ce7e72df1 100644 --- a/tests/Avalonia.RenderTests/OpacityMaskTests.cs +++ b/tests/Avalonia.RenderTests/OpacityMaskTests.cs @@ -57,7 +57,7 @@ namespace Avalonia.Direct2D1.RenderTests } [Fact] - public async Task RenderTansform_Applies_To_Opacity_Mask() + public async Task RenderTransform_Applies_To_Opacity_Mask() { var target = new Canvas { diff --git a/tests/Avalonia.RenderTests/TestBase.cs b/tests/Avalonia.RenderTests/TestBase.cs index 8a127897d7..edde62f041 100644 --- a/tests/Avalonia.RenderTests/TestBase.cs +++ b/tests/Avalonia.RenderTests/TestBase.cs @@ -91,7 +91,7 @@ namespace Avalonia.Direct2D1.RenderTests var immediatePath = Path.Combine(OutputPath, testName + ".immediate.out.png"); var deferredPath = Path.Combine(OutputPath, testName + ".deferred.out.png"); var compositedPath = Path.Combine(OutputPath, testName + ".composited.out.png"); - var factory = AvaloniaLocator.Current.GetService(); + var factory = AvaloniaLocator.Current.GetRequiredService(); var pixelSize = new PixelSize((int)target.Width, (int)target.Height); var size = new Size(target.Width, target.Height); var dpiVector = new Vector(dpi, dpi); @@ -156,7 +156,8 @@ namespace Avalonia.Direct2D1.RenderTests public ILockedFramebuffer Lock() => _bitmap.Lock(); } - protected void CompareImages([CallerMemberName] string testName = "") + protected void CompareImages([CallerMemberName] string testName = "", + bool skipImmediate = false, bool skipDeferred = false, bool skipCompositor = false) { var expectedPath = Path.Combine(OutputPath, testName + ".expected.png"); var immediatePath = Path.Combine(OutputPath, testName + ".immediate.out.png"); @@ -172,17 +173,17 @@ namespace Avalonia.Direct2D1.RenderTests var deferredError = CompareImages(deferred, expected); var compositedError = CompareImages(composited, expected); - if (immediateError > 0.022) + if (immediateError > 0.022 && !skipImmediate) { Assert.True(false, immediatePath + ": Error = " + immediateError); } - if (deferredError > 0.022) + if (deferredError > 0.022 && !skipDeferred) { Assert.True(false, deferredPath + ": Error = " + deferredError); } - if (compositedError > 0.022) + if (compositedError > 0.022 && !skipCompositor) { Assert.True(false, compositedPath + ": Error = " + compositedError); } diff --git a/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj index ccde66a50e..ea91b8c196 100644 --- a/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj +++ b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj @@ -1,7 +1,6 @@  net6.0 - latest diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs index aa499bb135..81d7b1854b 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs @@ -1,6 +1,4 @@ -using System; -using Avalonia.Media.TextFormatting; -using Avalonia.Utilities; +using Avalonia.Media.TextFormatting; namespace Avalonia.Skia.UnitTests.Media.TextFormatting { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 33d4fba5f1..1b6fd537eb 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -62,6 +62,69 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + private class TextSourceWithDummyRuns : ITextSource + { + private readonly TextRunProperties _properties; + private readonly List> _textRuns; + + public TextSourceWithDummyRuns(TextRunProperties properties) + { + _properties = properties; + + _textRuns = new List> + { + new ValueSpan(0, 5, new TextCharacters("Hello", _properties)), + new ValueSpan(5, 1, new DummyRun()), + new ValueSpan(6, 1, new DummyRun()), + new ValueSpan(7, 6, new TextCharacters(" World", _properties)) + }; + } + + public TextRun GetTextRun(int textSourceIndex) + { + foreach (var run in _textRuns) + { + if (textSourceIndex < run.Start + run.Length) + { + return run.Value; + } + } + + return new TextEndOfParagraph(); + } + + private class DummyRun : TextRun + { + public DummyRun() + { + Length = DefaultTextSourceLength; + } + + public override int Length { get; } + } + } + + [Fact] + public void Should_Format_TextLine_With_Non_Text_TextRuns() + { + using (Start()) + { + var defaultProperties = + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black); + + var textSource = new TextSourceWithDummyRuns(defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.Equal(5, textLine.TextRuns.Count); + + Assert.Equal(14, textLine.Length); + } + } + [Fact] public void Should_Format_TextRuns_With_TextRunStyles() { @@ -520,7 +583,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var expectedTextLine = formatter.FormatLine(new SingleBufferTextSource(text, defaultProperties), 0, double.PositiveInfinity, paragraphProperties); - var expectedRuns = expectedTextLine.TextRuns.Cast().ToList(); + var expectedRuns = expectedTextLine.TextRuns.Cast().ToList(); var expectedGlyphs = expectedRuns.SelectMany(x => x.GlyphRun.GlyphIndices).ToList(); @@ -539,7 +602,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties); - var shapedRuns = textLine.TextRuns.Cast().ToList(); + var shapedRuns = textLine.TextRuns.Cast().ToList(); var actualGlyphs = shapedRuns.SelectMany(x => x.GlyphRun.GlyphIndices).ToList(); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index a407b38eb1..7501bf21fe 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -141,7 +141,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting black, textWrapping: TextWrapping.Wrap); - var expectedGlyphs = expected.TextLines.Select(x => string.Join('|', x.TextRuns.Cast() + var expectedGlyphs = expected.TextLines.Select(x => string.Join('|', x.TextRuns.Cast() .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList(); var outer = new GraphemeEnumerator(new CharacterBufferRange(text)); @@ -153,7 +153,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { while (inner.MoveNext()) { - j += inner.Current.Text.Length; + j += inner.Current.Length; if (j + i > text.Length) { @@ -174,7 +174,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textWrapping: TextWrapping.Wrap, textStyleOverrides: spans); - var actualGlyphs = actual.TextLines.Select(x => string.Join('|', x.TextRuns.Cast() + var actualGlyphs = actual.TextLines.Select(x => string.Join('|', x.TextRuns.Cast() .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList(); Assert.Equal(expectedGlyphs.Count, actualGlyphs.Count); @@ -192,7 +192,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting inner = new GraphemeEnumerator(new CharacterBufferRange(text)); - i += outer.Current.Text.Length; + i += outer.Current.Length; } } } @@ -447,7 +447,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting 12.0f, Brushes.Black.ToImmutable()); - var shapedRun = (ShapedTextCharacters)layout.TextLines[0].TextRuns[0]; + var shapedRun = (ShapedTextRun)layout.TextLines[0].TextRuns[0]; var glyphRun = shapedRun.GlyphRun; @@ -481,7 +481,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting foreach (var textRun in textLine.TextRuns) { - var shapedRun = (ShapedTextCharacters)textRun; + var shapedRun = (ShapedTextRun)textRun; var glyphClusters = shapedRun.ShapedBuffer.GlyphClusters; @@ -514,13 +514,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(1, layout.TextLines[0].TextRuns.Count); - Assert.Equal(expectedLength, ((ShapedTextCharacters)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters.Count); + Assert.Equal(expectedLength, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters.Count); - Assert.Equal(5, ((ShapedTextCharacters)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphClusters[5]); + Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphClusters[5]); if (expectedLength == 7) { - Assert.Equal(5, ((ShapedTextCharacters)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphClusters[6]); + Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphClusters[6]); } } } @@ -555,7 +555,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = layout.TextLines[0]; - var textRun = (ShapedTextCharacters)textLine.TextRuns[0]; + var textRun = (ShapedTextRun)textLine.TextRuns[0]; Assert.Equal(7, textRun.Length); @@ -775,7 +775,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(textLine.WidthIncludingTrailingWhitespace, rect.Width); } - var rects = layout.TextLines.SelectMany(x => x.TextRuns.Cast()) + var rects = layout.TextLines.SelectMany(x => x.TextRuns.Cast()) .SelectMany(x => x.ShapedBuffer.GlyphAdvances).ToArray(); for (var i = 0; i < SingleLineText.Length; i++) @@ -814,7 +814,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { Assert.True(textLine.Width <= maxWidth); - var actual = new string(textLine.TextRuns.Cast() + var actual = new string(textLine.TextRuns.Cast() .OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar) .SelectMany(x => new CharacterBufferRange(x.CharacterBufferReference, x.Length)).ToArray()); @@ -855,7 +855,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Brushes.Black, flowDirection: FlowDirection.RightToLeft); - var firstRun = layout.TextLines[0].TextRuns[0] as ShapedTextCharacters; + var firstRun = layout.TextLines[0].TextRuns[0] as ShapedTextRun; var hit = layout.HitTestPoint(new Point()); @@ -881,7 +881,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting currentX += advance; } - var secondRun = layout.TextLines[0].TextRuns[1] as ShapedTextCharacters; + var secondRun = layout.TextLines[0].TextRuns[1] as ShapedTextRun; hit = layout.HitTestPoint(new Point(firstRun.Size.Width, 0)); @@ -928,7 +928,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = layout.TextLines[0]; - var firstRun = (ShapedTextCharacters)textLine.TextRuns[0]; + var firstRun = (ShapedTextRun)textLine.TextRuns[0]; var firstCluster = firstRun.ShapedBuffer.GlyphClusters[0]; @@ -974,9 +974,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var grapheme = graphemeEnumerator.Current; - var textStyleOverrides = new[] { new ValueSpan(i, grapheme.Text.Length, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Red)) }; + var textStyleOverrides = new[] { new ValueSpan(i, grapheme.Length, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Red)) }; - i += grapheme.Text.Length; + i += grapheme.Length; var layout = new TextLayout( text, @@ -987,7 +987,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = layout.TextLines[0]; - var shapedRuns = textLine.TextRuns.Cast().ToList(); + var shapedRuns = textLine.TextRuns.Cast().ToList(); var clusters = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphClusters).ToList(); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index d6257a0de8..2c6ccfa896 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -92,7 +92,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting foreach (var textRun in textLine.TextRuns.OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar)) { - var shapedRun = (ShapedTextCharacters)textRun; + var shapedRun = (ShapedTextRun)textRun; clusters.AddRange(shapedRun.IsReversed ? shapedRun.ShapedBuffer.GlyphClusters.Reverse() : @@ -139,7 +139,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting foreach (var textRun in textLine.TextRuns.OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar)) { - var shapedRun = (ShapedTextCharacters)textRun; + var shapedRun = (ShapedTextRun)textRun; clusters.AddRange(shapedRun.IsReversed ? shapedRun.ShapedBuffer.GlyphClusters.Reverse() : @@ -246,7 +246,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); - var clusters = textLine.TextRuns.Cast().SelectMany(x => x.ShapedBuffer.GlyphClusters) + var clusters = textLine.TextRuns.Cast().SelectMany(x => x.ShapedBuffer.GlyphClusters) .ToArray(); var previousCharacterHit = new CharacterHit(text.Length); @@ -308,7 +308,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting foreach (var run in textLine.TextRuns) { - var textRun = (ShapedTextCharacters)run; + var textRun = (ShapedTextRun)run; var glyphRun = textRun.GlyphRun; @@ -326,7 +326,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } - Assert.Equal(currentDistance, textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length))); + var actualDistance = textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length)); + + Assert.Equal(currentDistance, actualDistance); } } @@ -634,7 +636,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); - var textRuns = textLine.TextRuns.Cast().ToList(); + var textRuns = textLine.TextRuns.Cast().ToList(); var lineWidth = textLine.WidthIncludingTrailingWhitespace; @@ -732,14 +734,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting private static bool IsRightToLeft(TextLine textLine) { - return textLine.TextRuns.Cast().Any(x => !x.ShapedBuffer.IsLeftToRight); + return textLine.TextRuns.Cast().Any(x => !x.ShapedBuffer.IsLeftToRight); } private static List BuildGlyphClusters(TextLine textLine) { var glyphClusters = new List(); - var shapedTextRuns = textLine.TextRuns.Cast().ToList(); + var shapedTextRuns = textLine.TextRuns.Cast().ToList(); var lastCluster = -1; @@ -774,7 +776,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var lastCluster = -1; - var shapedTextRuns = textLine.TextRuns.Cast().ToList(); + var shapedTextRuns = textLine.TextRuns.Cast().ToList(); foreach (var textRun in shapedTextRuns) { @@ -820,16 +822,16 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var text = "0123"; var shaperOption = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture); - var firstRun = new ShapedTextCharacters(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties); + var firstRun = new ShapedTextRun(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties); var textRuns = new List { new CustomDrawableRun(), firstRun, new CustomDrawableRun(), - new ShapedTextCharacters(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties), + new ShapedTextRun(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties), new CustomDrawableRun(), - new ShapedTextCharacters(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties) + new ShapedTextRun(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties) }; var textSource = new FixedRunsTextSource(textRuns); @@ -885,14 +887,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textBounds = textLine.GetTextBounds(0, 3); - var firstRun = textLine.TextRuns[0] as ShapedTextCharacters; + var firstRun = textLine.TextRuns[0] as ShapedTextRun; Assert.Equal(1, textBounds.Count); Assert.Equal(firstRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(3, 4); - var secondRun = textLine.TextRuns[1] as ShapedTextCharacters; + var secondRun = textLine.TextRuns[1] as ShapedTextRun; Assert.Equal(1, textBounds.Count); Assert.Equal(secondRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width)); @@ -932,14 +934,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textBounds = textLine.GetTextBounds(0, 4); - var secondRun = textLine.TextRuns[1] as ShapedTextCharacters; + var secondRun = textLine.TextRuns[1] as ShapedTextRun; Assert.Equal(1, textBounds.Count); Assert.Equal(secondRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(4, 3); - var firstRun = textLine.TextRuns[0] as ShapedTextCharacters; + var firstRun = textLine.TextRuns[0] as ShapedTextRun; Assert.Equal(1, textBounds.Count); diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj index cb6884cad8..17448ade76 100644 --- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj +++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj @@ -1,11 +1,9 @@  netstandard2.0 - latest false Library false - latest ..\..\build\avalonia.snk false true diff --git a/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs index 2b1685f358..55ac16054d 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs +++ b/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs @@ -76,13 +76,8 @@ namespace Avalonia.UnitTests var asset = fontAssets.First(); - var assetLoader = AvaloniaLocator.Current.GetService(); + var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - if (assetLoader == null) - { - throw new NotSupportedException("IAssetLoader is not registered."); - } - var stream = assetLoader.Open(asset); return new HarfBuzzGlyphTypefaceImpl(stream); diff --git a/tests/Avalonia.UnitTests/MockGlyphRun.cs b/tests/Avalonia.UnitTests/MockGlyphRun.cs index 24948aff01..f525e4736b 100644 --- a/tests/Avalonia.UnitTests/MockGlyphRun.cs +++ b/tests/Avalonia.UnitTests/MockGlyphRun.cs @@ -1,4 +1,5 @@ -using Avalonia.Platform; +using System.Collections.Generic; +using Avalonia.Platform; namespace Avalonia.UnitTests { @@ -8,5 +9,10 @@ namespace Avalonia.UnitTests { } + + public IReadOnlyList GetIntersections(float lowerBound, float upperBound) + { + return null; + } } } diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index c421adaf21..40306a4513 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -20,7 +20,7 @@ namespace Avalonia.UnitTests { public static readonly TestServices StyledWindow = new TestServices( assetLoader: new AssetLoader(), - platform: new AppBuilder().RuntimePlatform, + platform: new StandardRuntimePlatform(), renderInterface: new MockPlatformRenderInterface(), standardCursorFactory: Mock.Of(), theme: () => CreateSimpleTheme(), @@ -71,7 +71,6 @@ namespace Avalonia.UnitTests IRuntimePlatform platform = null, IPlatformRenderInterface renderInterface = null, IRenderTimer renderLoop = null, - IScheduler scheduler = null, ICursorFactory standardCursorFactory = null, Func theme = null, IPlatformThreadingInterface threadingInterface = null, @@ -91,7 +90,6 @@ namespace Avalonia.UnitTests RenderInterface = renderInterface; FontManagerImpl = fontManagerImpl; TextShaperImpl = textShaperImpl; - Scheduler = scheduler; StandardCursorFactory = standardCursorFactory; Theme = theme; ThreadingInterface = threadingInterface; @@ -110,7 +108,6 @@ namespace Avalonia.UnitTests public IPlatformRenderInterface RenderInterface { get; } public IFontManagerImpl FontManagerImpl { get; } public ITextShaperImpl TextShaperImpl { get; } - public IScheduler Scheduler { get; } public ICursorFactory StandardCursorFactory { get; } public Func Theme { get; } public IPlatformThreadingInterface ThreadingInterface { get; } @@ -149,7 +146,6 @@ namespace Avalonia.UnitTests renderInterface: renderInterface ?? RenderInterface, fontManagerImpl: fontManagerImpl ?? FontManagerImpl, textShaperImpl: textShaperImpl ?? TextShaperImpl, - scheduler: scheduler ?? Scheduler, standardCursorFactory: standardCursorFactory ?? StandardCursorFactory, theme: theme ?? Theme, threadingInterface: threadingInterface ?? ThreadingInterface, @@ -169,16 +165,4 @@ namespace Avalonia.UnitTests y => y.Open() == Mock.Of())); } } - - public class AppBuilder : AppBuilderBase - { - public AppBuilder() - : base(new StandardRuntimePlatform(), - builder => StandardRuntimePlatformServices.Register(builder.Instance?.GetType() - ?.GetTypeInfo().Assembly)) - { - } - - protected override bool CheckSetup => false; - } } diff --git a/tests/Avalonia.UnitTests/TouchTestHelper.cs b/tests/Avalonia.UnitTests/TouchTestHelper.cs new file mode 100644 index 0000000000..db70f570a2 --- /dev/null +++ b/tests/Avalonia.UnitTests/TouchTestHelper.cs @@ -0,0 +1,53 @@ +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace Avalonia.UnitTests +{ + public class TouchTestHelper + { + private readonly Pointer _pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Touch, true); + private ulong _nextStamp = 1; + private ulong Timestamp() => _nextStamp++; + public IInputElement Captured => _pointer.Captured; + + public void Down(Interactive target, Point position = default, KeyModifiers modifiers = default) + { + Down(target, target, position, modifiers); + } + + public void Down(Interactive target, Interactive source, Point position = default, KeyModifiers modifiers = default) + { + _pointer.Capture((IInputElement)target); + source.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (Visual)source, position, Timestamp(), PointerPointProperties.None, + modifiers)); + } + + public void Move(Interactive target, in Point position, KeyModifiers modifiers = default) => Move(target, target, position, modifiers); + + public void Move(Interactive target, Interactive source, in Point position, KeyModifiers modifiers = default) + { + target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, (Visual)target, position, + Timestamp(), PointerPointProperties.None, modifiers)); + } + + public void Up(Interactive target, Point position = default, KeyModifiers modifiers = default) + => Up(target, target, position, modifiers); + + public void Up(Interactive target, Interactive source, Point position = default, KeyModifiers modifiers = default) + { + source.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (Visual)target, position, Timestamp(), PointerPointProperties.None, + modifiers, MouseButton.None)); + _pointer.Capture(null); + } + + public void Tap(Interactive target, Point position = default, KeyModifiers modifiers = default) + => Tap(target, target, position, modifiers); + + public void Tap(Interactive target, Interactive source, Point position = default, KeyModifiers modifiers = default) + { + Down(target, source, position, modifiers); + Up(target, source, position, modifiers); + } + } +} diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs index 03e19359c3..82626877d0 100644 --- a/tests/Avalonia.UnitTests/UnitTestApplication.cs +++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs @@ -66,7 +66,6 @@ namespace Avalonia.UnitTests .Bind().ToConstant(Services.FontManagerImpl) .Bind().ToConstant(Services.TextShaperImpl) .Bind().ToConstant(Services.ThreadingInterface) - .Bind().ToConstant(Services.Scheduler) .Bind().ToConstant(Services.StandardCursorFactory) .Bind().ToConstant(Services.WindowingPlatform) .Bind().ToSingleton(); diff --git a/tests/TestFiles/Direct2D1/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped.expected.png b/tests/TestFiles/Direct2D1/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped.expected.png new file mode 100644 index 0000000000..6a67087d41 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped.expected.png differ diff --git a/tests/TestFiles/Direct2D1/OpacityMask/RenderTansform_Applies_To_Opacity_Mask.expected.png b/tests/TestFiles/Direct2D1/OpacityMask/RenderTransform_Applies_To_Opacity_Mask.expected.png similarity index 100% rename from tests/TestFiles/Direct2D1/OpacityMask/RenderTansform_Applies_To_Opacity_Mask.expected.png rename to tests/TestFiles/Direct2D1/OpacityMask/RenderTransform_Applies_To_Opacity_Mask.expected.png diff --git a/tests/TestFiles/Skia/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped.expected.png b/tests/TestFiles/Skia/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped.expected.png new file mode 100644 index 0000000000..6a67087d41 Binary files /dev/null and b/tests/TestFiles/Skia/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped.expected.png differ diff --git a/tests/TestFiles/Skia/OpacityMask/RenderTansform_Applies_To_Opacity_Mask.expected.png b/tests/TestFiles/Skia/OpacityMask/RenderTransform_Applies_To_Opacity_Mask.expected.png similarity index 100% rename from tests/TestFiles/Skia/OpacityMask/RenderTansform_Applies_To_Opacity_Mask.expected.png rename to tests/TestFiles/Skia/OpacityMask/RenderTransform_Applies_To_Opacity_Mask.expected.png