diff --git a/.editorconfig b/.editorconfig index 238e9887bd..3620896f34 100644 --- a/.editorconfig +++ b/.editorconfig @@ -140,16 +140,22 @@ 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 dotnet_diagnostic.CA1802.severity = warning +# CA1815: Override equals and operator equals on value types +dotnet_diagnostic.CA1815.severity = warning # CA1820: Test for empty strings using string length dotnet_diagnostic.CA1820.severity = warning # CA1821: Remove empty finalizers 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 @@ -163,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 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. +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/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/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/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/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/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index 8d56086470..c885a7768c 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs @@ -167,7 +167,7 @@ namespace Avalonia.Android } } - public readonly struct ComposingRegion + public readonly record struct ComposingRegion { private readonly int _start = -1; private readonly int _end = -1; diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 2b6d29e7c5..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") @@ -32,6 +32,7 @@ namespace Avalonia.Android public static AndroidPlatformOptions Options { get; private set; } internal static Compositor Compositor { get; private set; } + internal static PlatformRenderInterfaceContextManager RenderInterface { get; private set; } public static void Initialize() { @@ -51,15 +52,19 @@ namespace Avalonia.Android if (Options.UseGpu) { - EglPlatformOpenGlInterface.TryInitialize(); + EglPlatformGraphics.TryInitialize(); } if (Options.UseCompositor) { Compositor = new Compositor( AvaloniaLocator.Current.GetRequiredService(), - AvaloniaLocator.Current.GetService()); + AvaloniaLocator.Current.GetService()); } + else + RenderInterface = + new PlatformRenderInterfaceContextManager(AvaloniaLocator.Current + .GetService()); } } 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/OpenGL/GlPlatformSurface.cs b/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs deleted file mode 100644 index e85ed11028..0000000000 --- a/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Avalonia.OpenGL; -using Avalonia.OpenGL.Egl; -using Avalonia.OpenGL.Surfaces; - -namespace Avalonia.Android.OpenGL -{ - internal sealed class GlPlatformSurface : EglGlPlatformSurfaceBase - { - private readonly EglPlatformOpenGlInterface _egl; - private readonly IEglWindowGlPlatformSurfaceInfo _info; - - private GlPlatformSurface(EglPlatformOpenGlInterface egl, IEglWindowGlPlatformSurfaceInfo info) - { - _egl = egl; - _info = info; - } - - public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() => - new GlRenderTarget(_egl, _info, _egl.CreateWindowSurface(_info.Handle), _info.Handle); - - public static GlPlatformSurface TryCreate(IEglWindowGlPlatformSurfaceInfo info) - { - var feature = AvaloniaLocator.Current.GetService(); - if (feature is EglPlatformOpenGlInterface egl) - { - return new GlPlatformSurface(egl, info); - } - - return null; - } - } -} diff --git a/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs b/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs deleted file mode 100644 index f9071d9b27..0000000000 --- a/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; - -using Avalonia.OpenGL.Egl; -using Avalonia.OpenGL.Surfaces; - -namespace Avalonia.Android.OpenGL -{ - internal sealed class GlRenderTarget : EglPlatformSurfaceRenderTargetBase, IGlPlatformSurfaceRenderTargetWithCorruptionInfo - { - private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info; - private readonly EglSurface _surface; - private readonly IntPtr _handle; - - public GlRenderTarget( - EglPlatformOpenGlInterface egl, - EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, - EglSurface surface, - IntPtr handle) - : base(egl) - { - _info = info; - _surface = surface; - _handle = handle; - } - - public bool IsCorrupted => _handle != _info.Handle; - - public override IGlPlatformSurfaceRenderingSession BeginDraw() => BeginDraw(_surface, _info); - } -} 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 4150b52946..1aac567dda 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -8,7 +8,6 @@ using Android.Runtime; using Android.Text; using Android.Views; using Android.Views.InputMethods; -using Avalonia.Android.OpenGL; using Avalonia.Android.Platform.Specific; using Avalonia.Android.Platform.Specific.Helpers; using Avalonia.Android.Platform.Storage; @@ -27,10 +26,11 @@ 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 { - class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo, + class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo, ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider { private readonly IGlPlatformSurface _gl; @@ -47,7 +47,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform _textInputMethod = new AndroidInputMethod(_view); _keyboardHelper = new AndroidKeyboardEventsHelper(this); _pointerHelper = new AndroidMotionEventsHelper(this); - _gl = GlPlatformSurface.TryCreate(this); + _gl = new EglGlPlatformSurface(this); _framebuffer = new FramebufferManager(this); RenderScaling = _view.Scaling; @@ -106,10 +106,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform public IRenderer CreateRenderer(IRenderRoot root) => AndroidPlatform.Options.UseCompositor - ? new CompositingRenderer(root, AndroidPlatform.Compositor) + ? new CompositingRenderer(root, AndroidPlatform.Compositor, () => Surfaces) : AndroidPlatform.Options.UseDeferredRendering - ? new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService()) { RenderOnlyOnRenderThread = true } - : new ImmediateRenderer((Visual)root); + ? new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService(), + () => AndroidPlatform.RenderInterface.CreateRenderTarget(Surfaces), + AndroidPlatform.RenderInterface) + { RenderOnlyOnRenderThread = true } + : new ImmediateRenderer((Visual)root, + () => AndroidPlatform.RenderInterface.CreateRenderTarget(Surfaces), + AndroidPlatform.RenderInterface); public virtual void Hide() { @@ -279,11 +284,11 @@ 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); - IntPtr EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo.Handle => ((IPlatformHandle)_view).Handle; + IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => ((IPlatformHandle)_view).Handle; public PixelSize Size => _view.Size; @@ -297,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/Animation/Cue.cs b/src/Avalonia.Base/Animation/Cue.cs index 6578148b07..c48f2ab6b0 100644 --- a/src/Avalonia.Base/Animation/Cue.cs +++ b/src/Avalonia.Base/Animation/Cue.cs @@ -8,7 +8,7 @@ namespace Avalonia.Animation /// Determines the time index for a . /// [TypeConverter(typeof(CueTypeConverter))] - public readonly struct Cue : IEquatable, IEquatable + public readonly record struct Cue : IEquatable, IEquatable { /// /// The normalized percent value, ranging from 0.0 to 1.0 @@ -49,15 +49,6 @@ namespace Avalonia.Animation } } - /// - /// Checks for equality between two s. - /// - /// The second cue. - public bool Equals(Cue other) - { - return CueValue == other.CueValue; - } - /// /// Checks for equality between a /// and a value. 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/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs index 4e07ebf445..3bc172f596 100644 --- a/src/Avalonia.Base/Data/BindingValue.cs +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -80,7 +80,7 @@ namespace Avalonia.Data /// - For an unset value, use or simply `default` /// - For other types, call one of the static factory methods /// - public readonly struct BindingValue + public readonly record struct BindingValue { private readonly T _value; 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..cec98ec66b 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 @@ -167,9 +191,18 @@ namespace Avalonia.Input.GestureRecognizers var distance = speed * elapsedSinceLastTick.TotalSeconds; _target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance)); - - - 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/PointerPoint.cs b/src/Avalonia.Base/Input/PointerPoint.cs index c51f286053..bd36b73b26 100644 --- a/src/Avalonia.Base/Input/PointerPoint.cs +++ b/src/Avalonia.Base/Input/PointerPoint.cs @@ -5,7 +5,7 @@ namespace Avalonia.Input /// /// Provides basic properties for the input pointer associated with a single mouse, pen/stylus, or touch contact. /// - public struct PointerPoint + public record struct PointerPoint { public PointerPoint(IPointer pointer, Point position, PointerPointProperties properties) { @@ -33,7 +33,7 @@ namespace Avalonia.Input /// /// Provides extended properties for a PointerPoint object. /// - public struct PointerPointProperties + public record struct PointerPointProperties { /// /// Gets a value that indicates whether the pointer input was triggered by the primary action mode of an input device. 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 b8f6f99ae8..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; @@ -130,7 +124,7 @@ namespace Avalonia.Input.Raw internal IInputElement? InputHitTestResult { get; set; } } - public struct RawPointerPoint + public record struct RawPointerPoint { /// /// Pointer position, in client DIPs. diff --git a/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs b/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs index 4a60f8a046..531cf3c704 100644 --- a/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs +++ b/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs @@ -46,7 +46,7 @@ namespace Avalonia.Input.TextInput void SelectInSurroundingText(int start, int end); } - public struct TextInputMethodSurroundingText + public record struct TextInputMethodSurroundingText { public string Text { get; set; } public int CursorOffset { get; set; } 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/Logging/ParametrizedLogger.cs b/src/Avalonia.Base/Logging/ParametrizedLogger.cs index 6b7331b504..a82af41a75 100644 --- a/src/Avalonia.Base/Logging/ParametrizedLogger.cs +++ b/src/Avalonia.Base/Logging/ParametrizedLogger.cs @@ -5,7 +5,7 @@ namespace Avalonia.Logging /// /// Logger sink parametrized for given logging level. /// - public readonly struct ParametrizedLogger + public readonly record struct ParametrizedLogger { private readonly ILogSink _sink; private readonly LogEventLevel _level; diff --git a/src/Avalonia.Base/Matrix.cs b/src/Avalonia.Base/Matrix.cs index df6439b1b3..a72710ffd4 100644 --- a/src/Avalonia.Base/Matrix.cs +++ b/src/Avalonia.Base/Matrix.cs @@ -571,7 +571,7 @@ namespace Avalonia return true; } - public struct Decomposed + public record struct Decomposed { public Vector Translate; public Vector Scale; diff --git a/src/Avalonia.Base/Media/BoxShadow.cs b/src/Avalonia.Base/Media/BoxShadow.cs index 2139516be6..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"; } @@ -171,5 +178,11 @@ namespace Avalonia.Media public Rect TransformBounds(in Rect rect) => IsInset ? rect : rect.Translate(new Vector(OffsetX, OffsetY)).Inflate(Spread + Blur); + + public static bool operator ==(BoxShadow left, BoxShadow right) => + left.Equals(right); + + public static bool operator !=(BoxShadow left, BoxShadow right) => + !(left == right); } } diff --git a/src/Avalonia.Base/Media/BoxShadows.cs b/src/Avalonia.Base/Media/BoxShadows.cs index ab2694389f..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) @@ -62,7 +62,9 @@ namespace Avalonia.Media } [EditorBrowsable(EditorBrowsableState.Never)] +#pragma warning disable CA1815 // Override equals and operator equals on value types public struct BoxShadowsEnumerator +#pragma warning restore CA1815 // Override equals and operator equals on value types { private int _index; private BoxShadows _shadows; @@ -118,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; } @@ -149,5 +151,11 @@ namespace Avalonia.Media return hashCode; } } + + public static bool operator ==(BoxShadows left, BoxShadows right) => + left.Equals(right); + + public static bool operator !=(BoxShadows left, BoxShadows right) => + !(left == right); } } 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 d0f3b9e21a..077816c645 100644 --- a/src/Avalonia.Base/Media/DrawingContext.cs +++ b/src/Avalonia.Base/Media/DrawingContext.cs @@ -261,7 +261,7 @@ namespace Avalonia.Media DrawRectangle(brush, null, rect, cornerRadius, cornerRadius); } - public readonly struct PushedState : IDisposable + public readonly record struct PushedState : IDisposable { private readonly int _level; private readonly DrawingContext _context; @@ -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/FontMetrics.cs b/src/Avalonia.Base/Media/FontMetrics.cs index 1cd01675db..6d952a6b93 100644 --- a/src/Avalonia.Base/Media/FontMetrics.cs +++ b/src/Avalonia.Base/Media/FontMetrics.cs @@ -3,7 +3,7 @@ /// /// The font metrics is holding information about a font's ascent, descent, etc. in design em units. /// - public readonly struct FontMetrics + public readonly record struct FontMetrics { /// /// Gets the font design units per em. 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..9ef32c7d09 100644 --- a/src/Avalonia.Base/Media/Geometry.cs +++ b/src/Avalonia.Base/Media/Geometry.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Platform; +using Avalonia.Reactive; namespace Avalonia.Media { @@ -30,7 +31,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 +85,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 +118,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); } } 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/GlyphMetrics.cs b/src/Avalonia.Base/Media/GlyphMetrics.cs index 2ee1f87d38..e9b5a112ac 100644 --- a/src/Avalonia.Base/Media/GlyphMetrics.cs +++ b/src/Avalonia.Base/Media/GlyphMetrics.cs @@ -1,6 +1,6 @@ namespace Avalonia.Media; -public readonly struct GlyphMetrics +public readonly record struct GlyphMetrics { /// /// Distance from the x-origin to the left extremum of the glyph. 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/GlyphRunMetrics.cs b/src/Avalonia.Base/Media/GlyphRunMetrics.cs index 983f029c7a..492b5214cd 100644 --- a/src/Avalonia.Base/Media/GlyphRunMetrics.cs +++ b/src/Avalonia.Base/Media/GlyphRunMetrics.cs @@ -1,6 +1,6 @@ namespace Avalonia.Media { - public readonly struct GlyphRunMetrics + public readonly record struct GlyphRunMetrics { public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, double height, int trailingWhitespaceLength, int newLineLength, int firstCluster, int lastCluster) 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/Imaging/RenderTargetBitmap.cs b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs index d56711ad68..88e5e627ee 100644 --- a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs @@ -60,5 +60,7 @@ namespace Avalonia.Media.Imaging /// public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? vbr) => PlatformImpl.Item.CreateDrawingContext(vbr); + + bool IRenderTarget.IsCorrupted => false; } } 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/TextCollapsingCreateInfo.cs b/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs index 78f15b724a..40ba613717 100644 --- a/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs +++ b/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs @@ -2,7 +2,7 @@ namespace Avalonia.Media { - public readonly struct TextCollapsingCreateInfo + public readonly record struct TextCollapsingCreateInfo { public readonly double Width; public readonly TextRunProperties TextRunProperties; diff --git a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs index d76f212f26..f5d39e4371 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 CharacterBufferReference.CharacterBuffer.Span[CharacterBufferReference.OffsetToFirstChar + index]; } } @@ -163,27 +163,18 @@ namespace Avalonia.Media.TextFormatting /// /// 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 => CharacterBufferReference.CharacterBuffer.Length == 0 || Length <= 0; internal CharacterBufferRange Take(int length) { @@ -280,14 +271,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 644c0ecbe1..902b897240 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs @@ -262,7 +262,7 @@ namespace Avalonia.Media.TextFormatting } } - public readonly struct GlyphInfo + public readonly record struct GlyphInfo { public GlyphInfo(ushort glyphIndex, int glyphCluster, double glyphAdvance = 0, Vector glyphOffset = default) { diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs similarity index 92% rename from src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs rename to src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index 3035eb7b18..3149bc2cda 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 { 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() diff --git a/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs b/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs index 03b93cfaf0..53021c4656 100644 --- a/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs +++ b/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs @@ -1,6 +1,8 @@ namespace Avalonia.Media.TextFormatting { +#pragma warning disable CA1815 // Override equals and operator equals on value types public readonly struct SplitResult +#pragma warning restore CA1815 // Override equals and operator equals on value types { public SplitResult(T first, T? second) { 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/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 574f92568c..9c201bda22 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -33,7 +33,7 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { - case ShapedTextCharacters shapedRun: + case ShapedTextRun shapedRun: { currentWidth += shapedRun.Size.Width; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 07aa5e1550..544800ecea 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -123,7 +123,7 @@ namespace Avalonia.Media.TextFormatting var second = new List(secondCount); - if (currentRun is ShapedTextCharacters shapedTextCharacters) + if (currentRun is ShapedTextRun shapedTextCharacters) { var split = shapedTextCharacters.Split(length - currentLength); @@ -198,16 +198,16 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { - 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; } @@ -256,10 +256,10 @@ namespace Avalonia.Media.TextFormatting 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); @@ -269,7 +269,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!; } @@ -278,9 +278,9 @@ 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) @@ -476,7 +476,7 @@ namespace Avalonia.Media.TextFormatting { switch (currentRun) { - case ShapedTextCharacters shapedTextCharacters: + case ShapedTextRun shapedTextCharacters: { if(shapedTextCharacters.ShapedBuffer.Length > 0) { @@ -546,7 +546,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(); } @@ -752,7 +752,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; @@ -768,7 +768,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/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index 9c085eba17..42a84ec137 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -67,7 +67,7 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { - case ShapedTextCharacters shapedRun: + case ShapedTextRun shapedRun: { currentWidth += shapedRun.Size.Width; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 48f5fec2c0..5a71bde522 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -202,14 +202,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) { @@ -279,7 +279,7 @@ namespace Avalonia.Media.TextFormatting switch (run) { - case ShapedTextCharacters shapedRun: + case ShapedTextRun shapedRun: { characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _); @@ -331,7 +331,7 @@ 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; @@ -341,7 +341,7 @@ namespace Avalonia.Media.TextFormatting { var nextRun = _textRuns[i + 1]; - if (nextRun is ShapedTextCharacters nextShapedRun && !nextShapedRun.ShapedBuffer.IsLeftToRight) + if (nextRun is ShapedTextRun nextShapedRun && !nextShapedRun.ShapedBuffer.IsLeftToRight) { i++; @@ -447,7 +447,7 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { - case ShapedTextCharacters shapedTextCharacters: + case ShapedTextRun shapedTextCharacters: { currentGlyphRun = shapedTextCharacters.GlyphRun; @@ -520,7 +520,7 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { - case ShapedTextCharacters shapedRun: + case ShapedTextRun shapedRun: { nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit); break; @@ -576,7 +576,7 @@ namespace Avalonia.Media.TextFormatting var startX = Start; double currentWidth = 0; - var currentRect = Rect.Empty; + var currentRect = default(Rect); TextRunBounds lastRunBounds = default; @@ -594,7 +594,7 @@ namespace Avalonia.Media.TextFormatting double combinedWidth; - if (currentRun is ShapedTextCharacters currentShapedRun) + if (currentRun is ShapedTextRun currentShapedRun) { var firstCluster = currentShapedRun.GlyphRun.Metrics.FirstCluster; @@ -636,7 +636,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) { @@ -668,12 +668,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); @@ -792,7 +792,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--) { @@ -813,7 +813,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); @@ -927,7 +927,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; @@ -989,7 +989,7 @@ namespace Avalonia.Media.TextFormatting private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection) { - if (run is ShapedTextCharacters shapedTextCharacters) + if (run is ShapedTextRun shapedTextCharacters) { return shapedTextCharacters.BidiLevel; } @@ -1071,7 +1071,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(); } @@ -1191,7 +1191,7 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { - case ShapedTextCharacters shapedRun: + case ShapedTextRun shapedRun: { var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _); @@ -1276,7 +1276,7 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { - case ShapedTextCharacters shapedRun: + case ShapedTextRun shapedRun: { var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _); @@ -1340,7 +1340,7 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { - case ShapedTextCharacters shapedRun: + case ShapedTextRun shapedRun: { var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster; @@ -1349,7 +1349,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) { @@ -1440,7 +1440,7 @@ namespace Avalonia.Media.TextFormatting { switch (_textRuns[index]) { - case ShapedTextCharacters textRun: + case ShapedTextRun textRun: { var textMetrics = new TextMetrics(textRun.Properties.Typeface.GlyphTypeface, textRun.Properties.FontRenderingEmSize); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs index 40a7f6167a..cb21e27696 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs @@ -4,7 +4,7 @@ /// Represents a metric for a objects, /// that holds information about ascent, descent, line gap, size and origin of the text line. /// - public readonly struct TextLineMetrics + public readonly record struct TextLineMetrics { public TextLineMetrics(bool hasOverflowed, double height, int newlineLength, double start, double textBaseline, int trailingWhitespaceLength, double width, diff --git a/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs b/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs index dc21c9b6f2..c83ae89320 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs @@ -3,7 +3,7 @@ /// /// A metric that holds information about text specific measurements. /// - public readonly struct TextMetrics + public readonly record struct TextMetrics { public TextMetrics(IGlyphTypeface glyphTypeface, double fontRenderingEmSize) { 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/TextRange.cs b/src/Avalonia.Base/Media/TextFormatting/TextRange.cs index 1177c758f4..e8bab55aff 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRange.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRange.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media.TextFormatting /// /// References a portion of a text buffer. /// - public readonly struct TextRange + public readonly record struct TextRange { public TextRange(int start, int length) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRun.cs b/src/Avalonia.Base/Media/TextFormatting/TextRun.cs index b55a0d627b..21f85b898f 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRun.cs @@ -44,7 +44,7 @@ namespace Avalonia.Media.TextFormatting fixed (char* charsPtr = characterBuffer.Span) { - return new string(charsPtr, 0, characterBuffer.Span.Length); + return new string(charsPtr, _textRun.CharacterBufferReference.OffsetToFirstChar, _textRun.Length); } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs b/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs index bdc7a1ca89..707c9048dc 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs @@ -3,7 +3,7 @@ /// /// The bounding rectangle of text run /// - public readonly struct TextRunBounds + public readonly record struct TextRunBounds { /// /// Constructing TextRunBounds 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/TextShaperOptions.cs b/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs index 80bbbcdbfe..610fc3dbc9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media.TextFormatting /// /// Options to customize text shaping. /// - public readonly struct TextShaperOptions + public readonly record struct TextShaperOptions { public TextShaperOptions( IGlyphTypeface typeface, diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs index 9e5186552e..8b9d1c1d02 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs @@ -1,9 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Runtime.CompilerServices; namespace Avalonia.Media.TextFormatting.Unicode { - public readonly struct Codepoint + public readonly record struct Codepoint { private readonly uint _value; @@ -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/Unicode/LineBreak.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs index 59c4df0a2e..0e470341f3 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs @@ -24,7 +24,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// Information about a potential line break position /// [DebuggerDisplay("{PositionMeasure}/{PositionWrap} @ {Required}")] - public readonly struct LineBreak + public readonly record struct LineBreak { /// /// Constructor 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/TextHitTestResult.cs b/src/Avalonia.Base/Media/TextHitTestResult.cs index c8922f06c8..d19adc2807 100644 --- a/src/Avalonia.Base/Media/TextHitTestResult.cs +++ b/src/Avalonia.Base/Media/TextHitTestResult.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media /// /// Holds a hit test result from a . /// - public readonly struct TextHitTestResult + public readonly record struct TextHitTestResult { public TextHitTestResult(CharacterHit characterHit, int textPosition, bool isInside, bool isTrailing) { diff --git a/src/Avalonia.Base/Media/Transformation/TransformOperation.cs b/src/Avalonia.Base/Media/Transformation/TransformOperation.cs index a8c0fe9b12..542d71963d 100644 --- a/src/Avalonia.Base/Media/Transformation/TransformOperation.cs +++ b/src/Avalonia.Base/Media/Transformation/TransformOperation.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media.Transformation /// /// Represents a single primitive transform (like translation, rotation, scale, etc.). /// - public struct TransformOperation + public record struct TransformOperation { public OperationType Type; public Matrix Matrix; @@ -196,7 +196,7 @@ namespace Avalonia.Media.Transformation } [StructLayout(LayoutKind.Explicit)] - public struct DataLayout + public record struct DataLayout { [FieldOffset(0)] public SkewLayout Skew; @@ -206,25 +206,25 @@ namespace Avalonia.Media.Transformation [FieldOffset(0)] public RotateLayout Rotate; - public struct SkewLayout + public record struct SkewLayout { public double X; public double Y; } - public struct ScaleLayout + public record struct ScaleLayout { public double X; public double Y; } - public struct TranslateLayout + public record struct TranslateLayout { public double X; public double Y; } - public struct RotateLayout + public record struct RotateLayout { public double Angle; } diff --git a/src/Avalonia.Base/Media/Transformation/TransformOperations.cs b/src/Avalonia.Base/Media/Transformation/TransformOperations.cs index 334bb93562..46ca41386a 100644 --- a/src/Avalonia.Base/Media/Transformation/TransformOperations.cs +++ b/src/Avalonia.Base/Media/Transformation/TransformOperations.cs @@ -165,7 +165,7 @@ namespace Avalonia.Media.Transformation return Math.Max(from._operations.Count, to._operations.Count); } - public readonly struct Builder + public readonly record struct Builder { private readonly List _operations; 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 344b85bae9..4cab7b4a14 100644 --- a/src/Avalonia.Base/Media/UnicodeRange.cs +++ b/src/Avalonia.Base/Media/UnicodeRange.cs @@ -7,7 +7,7 @@ namespace Avalonia.Media /// /// The descripes a set of Unicode characters. /// - public readonly struct UnicodeRange + public readonly record struct UnicodeRange { public readonly static UnicodeRange Default = Parse("0-10FFFD"); @@ -102,9 +102,9 @@ namespace Avalonia.Media } } - public readonly struct UnicodeRangeSegment + 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/IOptionalFeatureProvider.cs b/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs new file mode 100644 index 0000000000..b6464dea58 --- /dev/null +++ b/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs @@ -0,0 +1,18 @@ +using System; + +namespace Avalonia.Platform; + +public interface IOptionalFeatureProvider +{ + /// + /// Queries for an optional feature + /// + /// Feature type + public object? TryGetFeature(Type featureType); +} + +public static class OptionalFeatureProviderExtensions +{ + public static T? TryGetFeature(this IOptionalFeatureProvider provider) where T : class => + (T?)provider.TryGetFeature(typeof(T)); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/IPlatformGpu.cs b/src/Avalonia.Base/Platform/IPlatformGpu.cs index 0507dea1d7..f6953619d2 100644 --- a/src/Avalonia.Base/Platform/IPlatformGpu.cs +++ b/src/Avalonia.Base/Platform/IPlatformGpu.cs @@ -4,13 +4,21 @@ using Avalonia.Metadata; namespace Avalonia.Platform; [Unstable] -public interface IPlatformGpu +public interface IPlatformGraphics { - IPlatformGpuContext PrimaryContext { get; } + bool UsesSharedContext { get; } + IPlatformGraphicsContext CreateContext(); + IPlatformGraphicsContext GetSharedContext(); } [Unstable] -public interface IPlatformGpuContext : IDisposable +public interface IPlatformGraphicsContext : IDisposable, IOptionalFeatureProvider { + bool IsLost { get; } IDisposable EnsureCurrent(); +} + +public class PlatformGraphicsContextLostException : Exception +{ + } \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 518c5f37b8..1828f24aff 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -65,15 +65,6 @@ namespace Avalonia.Platform /// The geometry returned contains the combined geometry of all glyphs in the glyph run. IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun); - /// - /// Creates a renderer. - /// - /// - /// The list of native platform surfaces that can be used for output. - /// - /// An . - IRenderTarget CreateRenderTarget(IEnumerable surfaces); - /// /// Creates a render target bitmap implementation. /// @@ -181,6 +172,13 @@ namespace Avalonia.Platform /// IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList? glyphAdvances, IReadOnlyList? glyphOffsets); + /// + /// Creates a backend-specific object using a low-level API graphics context + /// + /// An underlying low-level graphics context (e. g. wrapped OpenGL context, Vulkan device, D3DDevice, etc) + /// + IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext? graphicsApiContext); + /// /// Gets a value indicating whether the platform directly supports rectangles with rounded corners. /// @@ -200,4 +198,17 @@ namespace Avalonia.Platform /// public PixelFormat DefaultPixelFormat { get; } } + + [Unstable] + public interface IPlatformRenderInterfaceContext : IOptionalFeatureProvider, IDisposable + { + /// + /// Creates a renderer. + /// + /// + /// The list of native platform surfaces that can be used for output. + /// + /// An . + IRenderTarget CreateRenderTarget(IEnumerable surfaces); + } } 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/IRenderTarget.cs b/src/Avalonia.Base/Platform/IRenderTarget.cs index 7023f2ca51..73e9e58da4 100644 --- a/src/Avalonia.Base/Platform/IRenderTarget.cs +++ b/src/Avalonia.Base/Platform/IRenderTarget.cs @@ -19,10 +19,10 @@ namespace Avalonia.Platform /// to be drawn. /// IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer); - } - - public interface IRenderTargetWithCorruptionInfo : IRenderTarget - { - bool IsCorrupted { get; } + + /// + /// Indicates if the render target is no longer usable and needs to be recreated + /// + public bool IsCorrupted { get; } } } diff --git a/src/Avalonia.Base/Platform/IRuntimePlatform.cs b/src/Avalonia.Base/Platform/IRuntimePlatform.cs index 3f8983479f..64b504c479 100644 --- a/src/Avalonia.Base/Platform/IRuntimePlatform.cs +++ b/src/Avalonia.Base/Platform/IRuntimePlatform.cs @@ -21,31 +21,12 @@ namespace Avalonia.Platform } [Unstable] - public struct RuntimePlatformInfo + 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/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/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index ea8a408e6f..b2080aeb87 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -38,13 +38,12 @@ public class CompositingRenderer : IRendererWithCompositor /// public bool RenderOnlyOnRenderThread { get; set; } = true; - public CompositingRenderer(IRenderRoot root, - Compositor compositor) + public CompositingRenderer(IRenderRoot root, Compositor compositor, Func> surfaces) { _root = root; _compositor = compositor; _recordingContext = new DrawingContext(_recorder); - CompositionTarget = compositor.CreateCompositionTarget(root.CreateRenderTarget); + CompositionTarget = compositor.CreateCompositionTarget(surfaces); CompositionTarget.Root = ((Visual)root).AttachToCompositor(compositor); _update = Update; } @@ -301,7 +300,9 @@ public class CompositingRenderer : IRendererWithCompositor { CompositionTarget.IsEnabled = false; } - + + public ValueTask TryGetRenderInterfaceFeature(Type featureType) => Compositor.TryGetRenderInterfaceFeature(featureType); + public void Dispose() { Stop(); 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 00fa7b3315..2b1b3f461f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia.Platform; using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Server; @@ -10,11 +11,11 @@ public partial class Compositor /// /// Creates a new CompositionTarget /// - /// A factory method to create IRenderTarget to be called from the render thread + /// A factory method to create IRenderTarget to be called from the render thread /// - public CompositionTarget CreateCompositionTarget(Func renderTargetFactory) + public CompositionTarget CreateCompositionTarget(Func> surfaces) { - return new CompositionTarget(this, new ServerCompositionTarget(_server, renderTargetFactory)); + return new CompositionTarget(this, new ServerCompositionTarget(_server, surfaces)); } public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server)); @@ -32,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 e6b9600e45..7fc5487171 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -34,6 +34,7 @@ namespace Avalonia.Rendering.Composition internal ServerCompositor Server => _server; private Task? _pendingBatch; private readonly object _pendingBatchLock = new(); + private List _pendingServerCompositorJobs = new(); internal IEasing DefaultEasing { get; } @@ -43,7 +44,7 @@ namespace Avalonia.Rendering.Composition /// /// /// - public Compositor(IRenderLoop loop, IPlatformGpu? gpu) + public Compositor(IRenderLoop loop, IPlatformGraphics? gpu) { Loop = loop; _server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool); @@ -101,9 +102,16 @@ namespace Avalonia.Rendering.Composition #endif } _objectsForSerialization.Clear(); + if (_pendingServerCompositorJobs.Count > 0) + { + writer.WriteObject(ServerCompositor.RenderThreadJobsStartMarker); + foreach (var job in _pendingServerCompositorJobs) + writer.WriteObject(job); + writer.WriteObject(ServerCompositor.RenderThreadJobsEndMarker); + } } - batch.CommitedAt = Server.Clock.Elapsed; + batch.CommittedAt = Server.Clock.Elapsed; _server.EnqueueBatch(batch); lock (_pendingBatchLock) @@ -136,5 +144,28 @@ namespace Avalonia.Rendering.Composition _invokeBeforeCommit.Enqueue(action); RequestCommitAsync(); } + + /// + /// Attempts to query for a feature from the platform render interface + /// + public ValueTask TryGetRenderInterfaceFeature(Type featureType) + { + var tcs = new TaskCompletionSource(); + _pendingServerCompositorJobs.Add(() => + { + try + { + using (Server.RenderInterface.EnsureCurrent()) + { + tcs.TrySetResult(Server.RenderInterface.Value.TryGetFeature(featureType)); + } + } + catch (Exception e) + { + tcs.TrySetException(e); + } + }); + return new ValueTask(tcs.Task); + } } } 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 f5a46506a3..b172430fbb 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -20,7 +20,7 @@ namespace Avalonia.Rendering.Composition.Server internal partial class ServerCompositionTarget : IDisposable { private readonly ServerCompositor _compositor; - private readonly Func _renderTargetFactory; + private readonly Func> _surfaces; private static long s_nextId = 1; public long Id { get; } public ulong Revision { get; private set; } @@ -39,11 +39,11 @@ namespace Avalonia.Rendering.Composition.Server public ReadbackIndices Readback { get; } = new(); public int RenderedVisuals { get; set; } - public ServerCompositionTarget(ServerCompositor compositor, Func renderTargetFactory) : + public ServerCompositionTarget(ServerCompositor compositor, Func> surfaces) : base(compositor) { _compositor = compositor; - _renderTargetFactory = renderTargetFactory; + _surfaces = surfaces; Id = Interlocked.Increment(ref s_nextId); } @@ -79,17 +79,16 @@ namespace Avalonia.Rendering.Composition.Server if (Root == null) return; - if ((_renderTarget as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true) + if (_renderTarget?.IsCorrupted == true) { _renderTarget!.Dispose(); _renderTarget = null; + _redrawRequested = true; } - _renderTarget ??= _renderTargetFactory(); - - Compositor.UpdateServerTime(); + _renderTarget ??= _compositor.CreateRenderTarget(_surfaces()); - if(_dirtyRect.IsEmpty && !_redrawRequested) + if(_dirtyRect.IsDefault && !_redrawRequested) return; Revision++; @@ -109,15 +108,16 @@ namespace Avalonia.Rendering.Composition.Server using (var targetContext = _renderTarget.CreateDrawingContext(null)) { var layerSize = Size * Scaling; - if (layerSize != _layerSize || _layer == null) + if (layerSize != _layerSize || _layer == null || _layer.IsCorrupted) { _layer?.Dispose(); _layer = null; _layer = targetContext.CreateLayer(Size); _layerSize = layerSize; + _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)) @@ -160,7 +160,7 @@ namespace Avalonia.Rendering.Composition.Server } RenderedVisuals = 0; - _dirtyRect = Rect.Empty; + _dirtyRect = default; } } @@ -179,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); @@ -197,7 +197,7 @@ namespace Avalonia.Rendering.Composition.Server if(_disposed) return; _disposed = true; - using (_compositor.GpuContext?.EnsureCurrent()) + using (_compositor.RenderInterface.EnsureCurrent()) { if (_layer != null) { 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 bfc2b2d626..e7405995f5 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Threading; +using Avalonia.Logging; using Avalonia.Platform; using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Expressions; @@ -19,23 +21,27 @@ namespace Avalonia.Rendering.Composition.Server internal class ServerCompositor : IRenderLoopTask { private readonly IRenderLoop _renderLoop; + private readonly Queue _batches = new Queue(); public long LastBatchId { get; private set; } 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(); - public IPlatformGpuContext? GpuContext { get; } + private Thread? _safeThread; + public PlatformRenderInterfaceContextManager RenderInterface { get; } + internal static readonly object RenderThreadJobsStartMarker = new(); + internal static readonly object RenderThreadJobsEndMarker = new(); - public ServerCompositor(IRenderLoop renderLoop, IPlatformGpu? platformGpu, + public ServerCompositor(IRenderLoop renderLoop, IPlatformGraphics? platformGraphics, BatchStreamObjectPool batchObjectPool, BatchStreamMemoryPool batchMemoryPool) { - GpuContext = platformGpu?.PrimaryContext; _renderLoop = renderLoop; + RenderInterface = new PlatformRenderInterfaceContextManager(platformGraphics); BatchObjectPool = batchObjectPool; BatchMemoryPool = batchMemoryPool; _renderLoop.Add(this); @@ -66,7 +72,14 @@ namespace Avalonia.Rendering.Composition.Server { while (!stream.IsObjectEof) { - var target = (ServerObject)stream.ReadObject()!; + var readObject = stream.ReadObject(); + if (readObject == RenderThreadJobsStartMarker) + { + ReadAndExecuteJobs(stream); + continue; + } + + var target = (ServerObject)readObject!; target.DeserializeChanges(stream, batch); #if DEBUG_COMPOSITOR_SERIALIZATION if (stream.ReadObject() != BatchStreamDebugMarkers.ObjectEndMarker) @@ -84,6 +97,23 @@ namespace Avalonia.Rendering.Composition.Server } } + void ReadAndExecuteJobs(BatchStreamReader reader) + { + object? readObject; + while ((readObject = reader.ReadObject()) != RenderThreadJobsEndMarker) + { + var job = (Action)readObject!; + try + { + job(); + } + catch + { + // Ignore + } + } + } + void CompletePendingBatches() { foreach(var batch in _reusableToCompleteList) @@ -101,25 +131,42 @@ 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(); - foreach (var t in _activeTargets) - t.Render(); + try + { + RenderInterface.EnsureValidBackendContext(); + foreach (var t in _activeTargets) + t.Render(); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, LogArea.Visual)?.Log(this, "Exception when rendering: {Error}", e); + } } public void AddCompositionTarget(ServerCompositionTarget target) @@ -132,10 +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 8b68900994..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; @@ -21,12 +22,43 @@ internal class BatchStreamData public Queue> Structs { get; } = new(); } -public struct BatchStreamSegment +public record struct BatchStreamSegment { public TData Data { get; set; } 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 c3bf861c47..f0b993a2b0 100644 --- a/src/Avalonia.Base/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Base/Rendering/DeferredRenderer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Threading.Tasks; using Avalonia.Logging; using Avalonia.Media; using Avalonia.Media.Immutable; @@ -22,6 +23,8 @@ namespace Avalonia.Rendering { private readonly IDispatcher? _dispatcher; private readonly IRenderLoop? _renderLoop; + private readonly Func? _renderTargetFactory; + private readonly PlatformRenderInterfaceContextManager? _renderInterface; private readonly Visual _root; private readonly ISceneBuilder _sceneBuilder; @@ -39,18 +42,23 @@ namespace Avalonia.Rendering private readonly object _startStopLock = new object(); private readonly object _renderLoopIsRenderingLock = new object(); private readonly Action _updateSceneIfNeededDelegate; + private List? _pendingRenderThreadJobs; /// /// Initializes a new instance of the class. /// /// 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 public DeferredRenderer( IRenderRoot root, IRenderLoop renderLoop, + Func renderTargetFactory, + PlatformRenderInterfaceContextManager? renderInterface = null, ISceneBuilder? sceneBuilder = null, IDispatcher? dispatcher = null, IDeferredRendererLock? rendererLock = null) : base(true) @@ -60,6 +68,8 @@ namespace Avalonia.Rendering _sceneBuilder = sceneBuilder ?? new SceneBuilder(); Layers = new RenderLayers(); _renderLoop = renderLoop; + _renderTargetFactory = renderTargetFactory; + _renderInterface = renderInterface; _lock = rendererLock ?? new ManagedDeferredRendererLock(); _updateSceneIfNeededDelegate = UpdateSceneIfNeeded; } @@ -256,6 +266,30 @@ namespace Avalonia.Rendering } } + public ValueTask TryGetRenderInterfaceFeature(Type featureType) + { + if (_renderInterface == null) + return new((object?)null); + + var tcs = new TaskCompletionSource(); + _pendingRenderThreadJobs ??= new(); + _pendingRenderThreadJobs.Add(() => + { + try + { + using (_renderInterface.EnsureCurrent()) + { + tcs.TrySetResult(_renderInterface.Value.TryGetFeature(featureType)); + } + } + catch (Exception e) + { + tcs.TrySetException(e); + } + }); + return new ValueTask(tcs.Task); + } + bool NeedsUpdate => _dirty == null || _dirty.Count > 0; bool IRenderLoopTask.NeedsUpdate => NeedsUpdate; @@ -277,7 +311,7 @@ namespace Avalonia.Rendering /// Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) { - return TryGetChildScene(_currentDraw)?.Size ?? Size.Empty; + return TryGetChildScene(_currentDraw)?.Size ?? default; } /// @@ -337,7 +371,16 @@ namespace Avalonia.Rendering } finally { - scene.Item.MarkAsRendered(); + try + { + if(scene.Item.RenderThreadJobs!=null) + foreach (var job in scene.Item.RenderThreadJobs) + job(); + } + finally + { + scene.Item.MarkAsRendered(); + } } } } @@ -420,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; @@ -604,7 +647,7 @@ namespace Avalonia.Rendering return; } - if ((RenderTarget as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true) + if (RenderTarget?.IsCorrupted == true) { RenderTarget!.Dispose(); RenderTarget = null; @@ -612,7 +655,7 @@ namespace Avalonia.Rendering if (RenderTarget == null) { - RenderTarget = ((IRenderRoot)_root).CreateRenderTarget(); + RenderTarget = _renderTargetFactory!(); } context = RenderTarget.CreateDrawingContext(this); @@ -637,7 +680,11 @@ namespace Avalonia.Rendering } if (_root.IsVisible) { - var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root)); + var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root) + { + RenderThreadJobs = _pendingRenderThreadJobs + }); + _pendingRenderThreadJobs = null; var scene = sceneRef.Item; if (_dirty == null) 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/IRenderRoot.cs b/src/Avalonia.Base/Rendering/IRenderRoot.cs index aa06ce7bed..cabb1302cd 100644 --- a/src/Avalonia.Base/Rendering/IRenderRoot.cs +++ b/src/Avalonia.Base/Rendering/IRenderRoot.cs @@ -25,12 +25,6 @@ namespace Avalonia.Rendering /// double RenderScaling { get; } - /// - /// Creates a render target for the window. - /// - /// An . - IRenderTarget CreateRenderTarget(); - /// /// Adds a rectangle to the window's dirty region. /// diff --git a/src/Avalonia.Base/Rendering/IRenderer.cs b/src/Avalonia.Base/Rendering/IRenderer.cs index cb36aad7cc..f3f5b5e99b 100644 --- a/src/Avalonia.Base/Rendering/IRenderer.cs +++ b/src/Avalonia.Base/Rendering/IRenderer.cs @@ -1,6 +1,7 @@ using System; using Avalonia.VisualTree; using System.Collections.Generic; +using System.Threading.Tasks; using Avalonia.Rendering.Composition; namespace Avalonia.Rendering @@ -87,6 +88,11 @@ namespace Avalonia.Rendering /// Stops the renderer. /// void Stop(); + + /// + /// Attempts to query for a feature from the platform render interface + /// + public ValueTask TryGetRenderInterfaceFeature(Type featureType); } public interface IRendererWithCompositor : IRenderer diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index 9e1582ed43..60b144e806 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Avalonia.Logging; using Avalonia.Media; using Avalonia.Platform; @@ -19,6 +20,8 @@ namespace Avalonia.Rendering public class ImmediateRenderer : RendererBase, IRenderer, IVisualBrushRenderer { private readonly Visual _root; + private readonly Func _renderTargetFactory; + private readonly PlatformRenderInterfaceContextManager? _renderContext; private readonly IRenderRoot? _renderRoot; private bool _updateTransformedBounds = true; private IRenderTarget? _renderTarget; @@ -27,15 +30,21 @@ namespace Avalonia.Rendering /// Initializes a new instance of the class. /// /// The control to render. - public ImmediateRenderer(Visual root) + /// The target render factory. + /// The render contex. + public ImmediateRenderer(Visual root, Func renderTargetFactory, + PlatformRenderInterfaceContextManager? renderContext = null) { _root = root ?? throw new ArgumentNullException(nameof(root)); + _renderTargetFactory = renderTargetFactory; + _renderContext = renderContext; _renderRoot = root as IRenderRoot; } - private ImmediateRenderer(Visual root, bool updateTransformedBounds) + private ImmediateRenderer(Visual root, Func renderTargetFactory, bool updateTransformedBounds) { _root = root ?? throw new ArgumentNullException(nameof(root)); + _renderTargetFactory = renderTargetFactory; _renderRoot = root as IRenderRoot; _updateTransformedBounds = updateTransformedBounds; } @@ -54,7 +63,7 @@ namespace Avalonia.Rendering { if (_renderTarget == null) { - _renderTarget = ((IRenderRoot)_root).CreateRenderTarget(); + _renderTarget = _renderTargetFactory(); } try @@ -104,7 +113,7 @@ namespace Avalonia.Rendering /// The render target. public static void Render(Visual visual, IRenderTarget target) { - using (var renderer = new ImmediateRenderer(visual, updateTransformedBounds: false)) + using (var renderer = new ImmediateRenderer(visual, () => target, updateTransformedBounds: false)) using (var context = new DrawingContext(target.CreateDrawingContext(renderer))) { renderer.Render(context, visual, visual.Bounds); @@ -118,7 +127,9 @@ namespace Avalonia.Rendering /// The drawing context. public static void Render(Visual visual, DrawingContext context) { - using (var renderer = new ImmediateRenderer(visual, updateTransformedBounds: false)) + using (var renderer = new ImmediateRenderer(visual, + () => throw new InvalidOperationException("This is not supposed to be called"), + updateTransformedBounds: false)) { renderer.Render(context, visual, visual.Bounds); } @@ -127,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); @@ -185,11 +196,14 @@ namespace Avalonia.Rendering { } + public ValueTask TryGetRenderInterfaceFeature(Type featureType) => + new(_renderContext?.Value?.TryGetFeature(featureType)); + /// Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) { (brush.Visual as IVisualBrushInitialize)?.EnsureInitialized(); - return brush.Visual?.Bounds.Size ?? Size.Empty; + return brush.Visual?.Bounds.Size ?? default; } /// @@ -201,7 +215,9 @@ namespace Avalonia.Rendering internal static void Render(Visual visual, DrawingContext context, bool updateTransformedBounds) { - using var renderer = new ImmediateRenderer(visual, updateTransformedBounds); + using var renderer = new ImmediateRenderer(visual, + () => throw new InvalidOperationException("This is not supposed to be called"), + updateTransformedBounds); renderer.Render(context, visual, visual.Bounds); } @@ -314,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/OwnedDisposable.cs b/src/Avalonia.Base/Rendering/OwnedDisposable.cs new file mode 100644 index 0000000000..27122751aa --- /dev/null +++ b/src/Avalonia.Base/Rendering/OwnedDisposable.cs @@ -0,0 +1,24 @@ +using System; + +namespace Avalonia.Rendering; + +struct OwnedDisposable :IDisposable where T : class, IDisposable +{ + private readonly bool _owns; + private T? _value; + + public T Value => _value ?? throw new ObjectDisposedException("OwnedDisposable"); + + public OwnedDisposable(T value, bool owns) + { + _owns = owns; + _value = value; + } + + public void Dispose() + { + if(_owns) + _value?.Dispose(); + _value = null; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs new file mode 100644 index 0000000000..261a39fa09 --- /dev/null +++ b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using Avalonia.Metadata; +using Avalonia.Platform; +using Avalonia.Reactive; + +namespace Avalonia.Rendering; + +[Unstable] +// TODO: Make it internal once legacy renderers are removed +public class PlatformRenderInterfaceContextManager +{ + private readonly IPlatformGraphics? _graphics; + private IPlatformRenderInterfaceContext? _backend; + private OwnedDisposable? _gpuContext; + + public PlatformRenderInterfaceContextManager(IPlatformGraphics? graphics) + { + _graphics = graphics; + } + + public void EnsureValidBackendContext() + { + if (_backend == null || _gpuContext?.Value.IsLost == true) + { + _backend?.Dispose(); + _backend = null; + _gpuContext?.Dispose(); + _gpuContext = null; + + if (_graphics != null) + { + if (_graphics.UsesSharedContext) + _gpuContext = new OwnedDisposable(_graphics.GetSharedContext(), false); + else + _gpuContext = new OwnedDisposable(_graphics.CreateContext(), true); + } + + _backend = AvaloniaLocator.Current.GetRequiredService() + .CreateBackendContext(_gpuContext?.Value); + } + } + + public IPlatformRenderInterfaceContext Value + { + get + { + EnsureValidBackendContext(); + return _backend!; + } + } + + public IDisposable EnsureCurrent() + { + EnsureValidBackendContext(); + if (_gpuContext.HasValue) + return _gpuContext.Value.Value.EnsureCurrent(); + return Disposable.Empty; + } + + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) + { + EnsureValidBackendContext(); + return _backend!.CreateRenderTarget(surfaces); + } +} diff --git a/src/Avalonia.Base/Rendering/RenderLoop.cs b/src/Avalonia.Base/Rendering/RenderLoop.cs index c66fec92aa..1f58ca3827 100644 --- a/src/Avalonia.Base/Rendering/RenderLoop.cs +++ b/src/Avalonia.Base/Rendering/RenderLoop.cs @@ -19,6 +19,7 @@ namespace Avalonia.Rendering private readonly IDispatcher _dispatcher; private List _items = new List(); private List _itemsCopy = new List(); + private List _updateItemsCopy = new List(); private IRenderTimer? _timer; private int _inTick; private int _inUpdate; @@ -49,8 +50,7 @@ namespace Avalonia.Rendering { get { - return _timer ??= AvaloniaLocator.Current.GetService() ?? - throw new InvalidOperationException("Cannot locate IRenderTimer."); + return _timer ??= AvaloniaLocator.Current.GetRequiredService(); } } @@ -97,7 +97,13 @@ namespace Avalonia.Rendering { bool needsUpdate = false; - foreach (IRenderLoopTask item in _items) + lock (_items) + { + _itemsCopy.Clear(); + _itemsCopy.AddRange(_items); + } + + foreach (IRenderLoopTask item in _itemsCopy) { if (item.NeedsUpdate) { @@ -112,10 +118,13 @@ namespace Avalonia.Rendering { _dispatcher.Post(() => { - for (var i = 0; i < _items.Count; ++i) + lock (_items) + { + _updateItemsCopy.Clear(); + _updateItemsCopy.AddRange(_items); + } + foreach (var item in _updateItemsCopy) { - var item = _items[i]; - if (item.NeedsUpdate) { try @@ -128,18 +137,12 @@ namespace Avalonia.Rendering } } } + _updateItemsCopy.Clear(); Interlocked.Exchange(ref _inUpdate, 0); }, DispatcherPriority.Render); } - lock (_items) - { - _itemsCopy.Clear(); - foreach (var i in _items) - _itemsCopy.Add(i); - } - for (int i = 0; i < _itemsCopy.Count; i++) { _itemsCopy[i].Render(); 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/Scene.cs b/src/Avalonia.Base/Rendering/SceneGraph/Scene.cs index 2da8923497..735eb3bb3f 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/Scene.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/Scene.cs @@ -346,5 +346,7 @@ namespace Avalonia.Rendering.SceneGraph } public void MarkAsRendered() => _rendered.TrySetResult(true); + + public List? RenderThreadJobs { get; set; } } } 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/SelectorMatch.cs b/src/Avalonia.Base/Styling/SelectorMatch.cs index 26b525347e..2eac04301a 100644 --- a/src/Avalonia.Base/Styling/SelectorMatch.cs +++ b/src/Avalonia.Base/Styling/SelectorMatch.cs @@ -43,7 +43,7 @@ namespace Avalonia.Styling /// A selector match describes whether and how a matches a control, and /// in addition whether the selector can ever match a control of the same type. /// - public readonly struct SelectorMatch + public readonly record struct SelectorMatch { /// /// A selector match with the result of . 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/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/SmallDictionary.cs b/src/Avalonia.Base/Utilities/SmallDictionary.cs index 7d6a21c136..8aaf2200df 100644 --- a/src/Avalonia.Base/Utilities/SmallDictionary.cs +++ b/src/Avalonia.Base/Utilities/SmallDictionary.cs @@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis; namespace Avalonia.Utilities; -public struct InlineDictionary : IEnumerable> where TKey : class where TValue : class +public record struct InlineDictionary : IEnumerable> where TKey : class where TValue : class { object? _data; TValue? _value; diff --git a/src/Avalonia.Base/Utilities/StringTokenizer.cs b/src/Avalonia.Base/Utilities/StringTokenizer.cs index 748eb09209..aad742e02b 100644 --- a/src/Avalonia.Base/Utilities/StringTokenizer.cs +++ b/src/Avalonia.Base/Utilities/StringTokenizer.cs @@ -8,7 +8,7 @@ namespace Avalonia.Utilities #if !BUILDTASK public #endif - struct StringTokenizer : IDisposable + record struct StringTokenizer : IDisposable { private const char DefaultSeparatorChar = ','; @@ -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/SynchronousCompletionAsyncResult.cs b/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs index fb8d3ed224..e574462e2c 100644 --- a/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs +++ b/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs @@ -8,7 +8,7 @@ namespace Avalonia.Utilities /// A task-like operation that is guaranteed to finish continuations synchronously, /// can be used for parametrized one-shot events /// - public struct SynchronousCompletionAsyncResult : INotifyCompletion + public record struct SynchronousCompletionAsyncResult : INotifyCompletion { private readonly SynchronousCompletionAsyncResultSource? _source; private readonly T? _result; diff --git a/src/Avalonia.Base/Utilities/ValueSpan.cs b/src/Avalonia.Base/Utilities/ValueSpan.cs index 7a10d865ef..4c2de2776d 100644 --- a/src/Avalonia.Base/Utilities/ValueSpan.cs +++ b/src/Avalonia.Base/Utilities/ValueSpan.cs @@ -3,7 +3,7 @@ /// /// Pairing of value and positions sharing that value. /// - public readonly struct ValueSpan + public readonly record struct ValueSpan { public ValueSpan(int start, int length, T value) { 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/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/GenerateAvaloniaResourcesTask.cs b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs index fad6ad397b..65681a2d28 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,8 +70,7 @@ 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(); @@ -93,15 +97,15 @@ namespace Avalonia.Build.Tasks ms.CopyTo(output); foreach (var s in sources) { - using(var input = s.Open()) + using (var input = s.Open()) input.CopyTo(output); } } 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 +115,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 +125,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.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.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index 9d2a994e5b..2ccf20d460 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -3,10 +3,6 @@ xmlns:controls="using:Avalonia.Controls" xmlns:primitives="using:Avalonia.Controls.Primitives"> - - - - diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml index 85f4e417e6..2cc8a1d38a 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml @@ -50,13 +50,13 @@ - - - + + + - - + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml index 626ddd4b43..b82d36a288 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml @@ -3,10 +3,6 @@ xmlns:controls="using:Avalonia.Controls" xmlns:primitives="using:Avalonia.Controls.Primitives"> - - - - diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml index 7aefa23706..8c4dfa9c87 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml @@ -50,13 +50,13 @@ - - - + + + - - + + 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/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index fb4335f4de..dd8575c989 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -6,8 +6,8 @@ M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z - M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z - M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z + M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z + M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z @@ -379,7 +379,7 @@ HorizontalAlignment="Center" VerticalAlignment="Center"> @@ -387,7 +387,7 @@ 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/AcrylicPlatformCompensationLevels.cs b/src/Avalonia.Controls/AcrylicPlatformCompensationLevels.cs index e1f1c4029e..2402de56ee 100644 --- a/src/Avalonia.Controls/AcrylicPlatformCompensationLevels.cs +++ b/src/Avalonia.Controls/AcrylicPlatformCompensationLevels.cs @@ -5,7 +5,7 @@ /// It controls the base opacity level of the 'tracing paper' layer that compensates /// for low blur radius. /// - public struct AcrylicPlatformCompensationLevels + public record struct AcrylicPlatformCompensationLevels { public AcrylicPlatformCompensationLevels(double transparent, double blurred, double acrylic) { 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 604f2369e7..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(); - WindowingSubsystemInitializer(); - RenderingSubsystemInitializer(); - 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..8da99b5f25 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) @@ -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/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs index 5ff0fd1feb..b79fef55b9 100644 --- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs +++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs @@ -4,7 +4,6 @@ using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Platform; using Avalonia.Styling; -using JetBrains.Annotations; namespace Avalonia.Controls.Embedding { @@ -12,7 +11,6 @@ namespace Avalonia.Controls.Embedding { public EmbeddableControlRoot(ITopLevelImpl impl) : base(impl) { - } public EmbeddableControlRoot() : base(PlatformManager.CreateEmbeddableWindow()) diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index 4f80892857..4a227a0c00 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -13,6 +13,7 @@ namespace Avalonia.Controls.Embedding.Offscreen { private double _scaling = 1; private Size _clientSize; + private PlatformRenderInterfaceContextManager _renderInterface = new(null); public IInputRoot? InputRoot { get; private set; } public bool IsDisposed { get; private set; } @@ -22,7 +23,8 @@ namespace Avalonia.Controls.Embedding.Offscreen IsDisposed = true; } - public IRenderer CreateRenderer(IRenderRoot root) => new ImmediateRenderer((Visual)root); + public IRenderer CreateRenderer(IRenderRoot root) => + new ImmediateRenderer((Visual)root, () => _renderInterface.CreateRenderTarget(Surfaces), _renderInterface); public abstract void Invalidate(Rect rect); public abstract IEnumerable Surfaces { get; } 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..5a691ce665 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); } @@ -500,7 +499,7 @@ namespace Avalonia.Controls.Presenters if (e.OldValue != null) { - Offset = default(Vector); + Offset = default; } } @@ -542,7 +541,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..480a8ee7a7 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() 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..e0f72cae54 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; diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 3c329a9a3e..bf79198939 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; diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index 17dfec118f..2e70947457 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -64,7 +64,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning /// surface. /// [Unstable] - public struct PopupPositionerParameters + public record struct PopupPositionerParameters { private PopupGravity _gravity; private PopupAnchor _anchor; 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/ISelectionModel.cs b/src/Avalonia.Controls/Selection/ISelectionModel.cs index 3f4ae48263..4c2a355bb5 100644 --- a/src/Avalonia.Controls/Selection/ISelectionModel.cs +++ b/src/Avalonia.Controls/Selection/ISelectionModel.cs @@ -39,7 +39,7 @@ namespace Avalonia.Controls.Selection return new BatchUpdateOperation(model); } - public struct BatchUpdateOperation : IDisposable + public record struct BatchUpdateOperation : IDisposable { private readonly ISelectionModel _owner; private bool _isDisposed; 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/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index 154f1868f3..9bbddfcbb2 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -738,7 +738,7 @@ namespace Avalonia.Controls.Selection } } - public struct BatchUpdateOperation : IDisposable + public record struct BatchUpdateOperation : IDisposable { private readonly SelectionModel _owner; private bool _isDisposed; 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 f39064435d..1d2bab8d27 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 08156ae00f..7a0da1ecc5 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -827,7 +827,7 @@ namespace Avalonia.Controls InvalidateTextLayout(); } - protected readonly struct SimpleTextSource : ITextSource + protected readonly record struct SimpleTextSource : ITextSource { private readonly CharacterBufferRange _text; private readonly TextRunProperties _defaultProperties; 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 d2d5a8309c..1f5b8f80d2 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; @@ -323,17 +323,7 @@ namespace Avalonia.Controls ??= AvaloniaLocator.Current.GetService()?.CreateProvider(this) ?? (PlatformImpl as ITopLevelImplWithStorageProvider)?.StorageProvider ?? throw new InvalidOperationException("StorageProvider platform implementation is not available."); - - IRenderTarget IRenderRoot.CreateRenderTarget() => CreateRenderTarget(); - - /// - protected virtual IRenderTarget CreateRenderTarget() - { - if(PlatformImpl == null) - throw new InvalidOperationException("Can't create render target, PlatformImpl is null (might be already disposed)"); - return _renderInterface!.CreateRenderTarget(PlatformImpl.Surfaces); - } - + /// void IRenderRoot.Invalidate(Rect rect) { 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 71389be1a6..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(); @@ -61,7 +61,10 @@ namespace Avalonia.DesignerSupport.Remote })); } - public IRenderer CreateRenderer(IRenderRoot root) => new ImmediateRenderer((Visual)root); + public IRenderer CreateRenderer(IRenderRoot root) => new ImmediateRenderer((Visual)root, () => + new PlatformRenderInterfaceContextManager(null) + .CreateRenderTarget(Surfaces)); + public void Dispose() { } 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..560d6fc252 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; 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/IX11InputMethod.cs b/src/Avalonia.FreeDesktop/IX11InputMethod.cs index 5d91118978..9fa9c1809e 100644 --- a/src/Avalonia.FreeDesktop/IX11InputMethod.cs +++ b/src/Avalonia.FreeDesktop/IX11InputMethod.cs @@ -11,7 +11,9 @@ namespace Avalonia.FreeDesktop (ITextInputMethodImpl method, IX11InputMethodControl control) CreateClient(IntPtr xid); } +#pragma warning disable CA1815 // Override equals and operator equals on value types public struct X11InputMethodForwardedKey +#pragma warning restore CA1815 // Override equals and operator equals on value types { public int KeyVal { get; set; } public KeyModifiers Modifiers { get; set; } 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 7abc0ca131..9bb4b34976 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -12,7 +12,7 @@ using Avalonia.Media.Imaging; namespace Avalonia.Headless { - internal class HeadlessPlatformRenderInterface : IPlatformRenderInterface + internal class HeadlessPlatformRenderInterface : IPlatformRenderInterface, IPlatformRenderInterfaceContext { public static void Initialize() { @@ -22,6 +22,8 @@ namespace Avalonia.Headless public IEnumerable InstalledFontNames { get; } = new[] { "Tahoma" }; + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => this; + public bool SupportsIndividualRoundRects => false; public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul; @@ -47,6 +49,7 @@ namespace Avalonia.Headless public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => throw new NotImplementedException(); public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); + public object TryGetFeature(Type featureType) => null; public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) { @@ -212,7 +215,7 @@ namespace Avalonia.Headless class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl { - public HeadlessStreamingGeometryStub() : base(Rect.Empty) + public HeadlessStreamingGeometryStub() : base(default) { } @@ -313,6 +316,8 @@ namespace Avalonia.Headless return new HeadlessDrawingContextStub(); } + public bool IsCorrupted => false; + public void Blit(IDrawingContextImpl context) { @@ -474,6 +479,13 @@ namespace Avalonia.Headless { return new HeadlessDrawingContextStub(); } + + public bool IsCorrupted => false; + } + + public void Dispose() + { + } } } 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 742df3324b..8eafce208b 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -56,8 +56,9 @@ namespace Avalonia.Headless public IRenderer CreateRenderer(IRenderRoot root) => AvaloniaHeadlessPlatform.Compositor != null - ? new CompositingRenderer(root, AvaloniaHeadlessPlatform.Compositor) - : new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService()); + ? new CompositingRenderer(root, AvaloniaHeadlessPlatform.Compositor, () => Surfaces) + : new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService(), + () => new PlatformRenderInterfaceContextManager(null).CreateRenderTarget(Surfaces), null); public void Invalidate(Rect rect) { @@ -174,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/AvaloniaNativePlatformOpenGlInterface.cs b/src/Avalonia.Native/AvaloniaNativeGlPlatformGraphics.cs similarity index 75% rename from src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs rename to src/Avalonia.Native/AvaloniaNativeGlPlatformGraphics.cs index 14d27a90e9..8e40960af6 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs +++ b/src/Avalonia.Native/AvaloniaNativeGlPlatformGraphics.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia.OpenGL; using Avalonia.Native.Interop; using System.Drawing; @@ -8,18 +9,18 @@ using Avalonia.Threading; namespace Avalonia.Native { - class AvaloniaNativePlatformOpenGlInterface : IPlatformOpenGlInterface + class AvaloniaNativeGlPlatformGraphics : IPlatformGraphics { private readonly IAvnGlDisplay _display; - public AvaloniaNativePlatformOpenGlInterface(IAvnGlDisplay display) + public AvaloniaNativeGlPlatformGraphics(IAvnGlDisplay display) { _display = display; - var immediate = display.CreateContext(null); + var context = display.CreateContext(null); int major, minor; GlInterface glInterface; - using (immediate.MakeCurrent()) + using (context.MakeCurrent()) { var basic = new GlBasicInfoInterface(display.GetProcAddress); basic.GetIntegerv(GlConsts.GL_MAJOR_VERSION, out major); @@ -32,24 +33,23 @@ namespace Avalonia.Native }); } - GlDisplay = new GlDisplay(display, glInterface, immediate.SampleCount, immediate.StencilSize); - MainContext = new GlContext(GlDisplay, null, immediate, _version); + GlDisplay = new GlDisplay(display, glInterface, context.SampleCount, context.StencilSize); + SharedContext =(GlContext)CreateContext(); } - internal GlContext MainContext { get; } - public IGlContext PrimaryContext => MainContext; - IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; + + public bool UsesSharedContext => true; + public IPlatformGraphicsContext CreateContext() => new GlContext(GlDisplay, + null, _display.CreateContext(null), _version); + + public IPlatformGraphicsContext GetSharedContext() => SharedContext; + public bool CanShareContexts => true; public bool CanCreateContexts => true; internal GlDisplay GlDisplay; private readonly GlVersion _version; - - public IGlContext CreateSharedContext() => new GlContext(GlDisplay, - MainContext, _display.CreateContext(MainContext.Context), _version); - - public IGlContext CreateContext() => new GlContext(GlDisplay, - null, _display.CreateContext(null), _version); + internal GlContext SharedContext { get; } } class GlDisplay @@ -71,6 +71,10 @@ namespace Avalonia.Native public int StencilSize { get; } public void ClearContext() => _display.LegacyClearCurrentContext(); + + public GlContext CreateSharedContext(GlContext share) => + new GlContext(this, share, _display.CreateContext(share.Context), share.Version); + } class GlContext : IGlContext @@ -91,7 +95,14 @@ namespace Avalonia.Native public GlInterface GlInterface => _display.GlInterface; public int SampleCount => _display.SampleCount; public int StencilSize => _display.StencilSize; - public IDisposable MakeCurrent() => Context.MakeCurrent(); + public IDisposable MakeCurrent() + { + if (IsLost) + throw new PlatformGraphicsContextLostException(); + return Context.MakeCurrent(); + } + + public bool IsLost => Context == null; public IDisposable EnsureCurrent() => MakeCurrent(); public bool IsSharedWith(IGlContext context) @@ -103,12 +114,18 @@ namespace Avalonia.Native || _sharedWith != null && _sharedWith == c._sharedWith; } + public bool CanCreateSharedContext => true; + + public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) => + _display.CreateSharedContext(_sharedWith ?? this); public void Dispose() { Context.Dispose(); Context = null; } + + public object TryGetFeature(Type featureType) => null; } @@ -125,7 +142,6 @@ namespace Avalonia.Native public IGlPlatformSurfaceRenderingSession BeginDraw() { - var feature = (AvaloniaNativePlatformOpenGlInterface)AvaloniaLocator.Current.GetService(); return new GlPlatformSurfaceRenderingSession(_context, _target.BeginDrawing()); } @@ -172,16 +188,14 @@ namespace Avalonia.Native class GlPlatformSurface : IGlPlatformSurface { private readonly IAvnWindowBase _window; - private readonly IGlContext _context; - - public GlPlatformSurface(IAvnWindowBase window, IGlContext context) + public GlPlatformSurface(IAvnWindowBase window) { _window = window; - _context = context; } - public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { - return new GlPlatformSurfaceRenderTarget(_window.CreateGlRenderTarget(), _context); + var avnContext = (GlContext)context; + return new GlPlatformSurfaceRenderTarget(_window.CreateGlRenderTarget(), avnContext); } } diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index db6b6f57cd..6d5925c0ae 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -9,22 +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 AvaloniaNativePlatformOpenGlInterface _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; } + internal static Compositor? Compositor { get; private set; } + internal static PlatformRenderInterfaceContextManager? RenderInterface { get; private set; } public static AvaloniaNativePlatform Initialize(IntPtr factory, AvaloniaNativePlatformOptions options) { @@ -62,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); } @@ -117,19 +118,21 @@ 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) { try { - _platformGl = new AvaloniaNativePlatformOpenGlInterface(_factory.ObtainGlDisplay()); + _platformGl = new AvaloniaNativeGlPlatformGraphics(_factory.ObtainGlDisplay()); AvaloniaLocator.CurrentMutable - .Bind().ToConstant(_platformGl) - .Bind().ToConstant(_platformGl); + .Bind().ToConstant(_platformGl); } catch (Exception) @@ -137,12 +140,14 @@ namespace Avalonia.Native // ignored } } - + if (_options.UseDeferredRendering && _options.UseCompositor) { Compositor = new Compositor(renderLoop, _platformGl); } + else + RenderInterface = new PlatformRenderInterfaceContextManager(_platformGl); } public ITrayIconImpl CreateTrayIcon() 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/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index 76d3905b47..0953527284 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -8,12 +8,12 @@ namespace Avalonia.Native class PopupImpl : WindowBaseImpl, IPopupImpl { private readonly AvaloniaNativePlatformOptions _opts; - private readonly AvaloniaNativePlatformOpenGlInterface _glFeature; + private readonly AvaloniaNativeGlPlatformGraphics _glFeature; private readonly IWindowBaseImpl _parent; public PopupImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, - AvaloniaNativePlatformOpenGlInterface glFeature, + AvaloniaNativeGlPlatformGraphics glFeature, IWindowBaseImpl parent) : base(factory, opts, glFeature) { _opts = opts; @@ -21,8 +21,7 @@ namespace Avalonia.Native _parent = parent; using (var e = new PopupEvents(this)) { - var context = _opts.UseGpu ? glFeature?.MainContext : null; - Init(factory.CreatePopup(e, context?.Context), factory.CreateScreens(), context); + Init(factory.CreatePopup(e, _glFeature.SharedContext.Context), factory.CreateScreens()); } PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize)); } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 9de794ed53..880a385744 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -14,14 +14,14 @@ namespace Avalonia.Native internal class WindowImpl : WindowBaseImpl, IWindowImpl, ITopLevelImplWithNativeMenuExporter { private readonly AvaloniaNativePlatformOptions _opts; - private readonly AvaloniaNativePlatformOpenGlInterface _glFeature; + private readonly AvaloniaNativeGlPlatformGraphics _glFeature; IAvnWindow _native; private double _extendTitleBarHeight = -1; private DoubleClickHelper _doubleClickHelper; internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, - AvaloniaNativePlatformOpenGlInterface glFeature) : base(factory, opts, glFeature) + AvaloniaNativeGlPlatformGraphics glFeature) : base(factory, opts, glFeature) { _opts = opts; _glFeature = glFeature; @@ -29,8 +29,7 @@ namespace Avalonia.Native using (var e = new WindowEvents(this)) { - var context = _opts.UseGpu ? glFeature?.MainContext : null; - Init(_native = factory.CreateWindow(e, context?.Context), factory.CreateScreens(), context); + Init(_native = factory.CreateWindow(e, glFeature.SharedContext.Context), factory.CreateScreens()); } NativeMenuExporter = new AvaloniaNativeMenuExporter(_native, factory); @@ -49,7 +48,7 @@ namespace Avalonia.Native { if (_parent.Closing != null) { - return _parent.Closing().AsComBool(); + return _parent.Closing(WindowCloseReason.WindowClosing).AsComBool(); } return true.AsComBool(); @@ -208,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 4744a20107..ca57e30b1c 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -63,10 +63,9 @@ namespace Avalonia.Native private double _savedScaling; private GlPlatformSurface _glSurface; private NativeControlHostImpl _nativeControlHost; - private IGlContext _glContext; internal WindowBaseImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, - AvaloniaNativePlatformOpenGlInterface glFeature) + AvaloniaNativeGlPlatformGraphics glFeature) { _factory = factory; _gpu = opts.UseGpu && glFeature != null; @@ -78,14 +77,13 @@ namespace Avalonia.Native StorageProvider = new SystemDialogs(this, _factory.CreateSystemDialogs()); } - protected void Init(IAvnWindowBase window, IAvnScreens screens, IGlContext glContext) + protected void Init(IAvnWindowBase window, IAvnScreens screens) { _native = window; - _glContext = glContext; Handle = new MacOSTopLevelWindowHandle(window); if (_gpu) - _glSurface = new GlPlatformSurface(window, _glContext); + _glSurface = new GlPlatformSurface(window); Screen = new ScreenImpl(screens); _savedLogicalSize = ClientSize; @@ -375,14 +373,17 @@ namespace Avalonia.Native if (_deferredRendering) { if (AvaloniaNativePlatform.Compositor != null) - return new CompositingRenderer(root, AvaloniaNativePlatform.Compositor) + return new CompositingRenderer(root, AvaloniaNativePlatform.Compositor, () => Surfaces) { RenderOnlyOnRenderThread = false }; - return new DeferredRenderer(root, loop); + return new DeferredRenderer(root, loop, + () => AvaloniaNativePlatform.RenderInterface!.CreateRenderTarget(Surfaces), + AvaloniaNativePlatform.RenderInterface); } - return new ImmediateRenderer((Visual)root); + return new ImmediateRenderer((Visual)root, + () => AvaloniaNativePlatform.RenderInterface!.CreateRenderTarget(Surfaces), AvaloniaNativePlatform.RenderInterface); } public virtual void Dispose() diff --git a/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs b/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs deleted file mode 100644 index 3a8fdb8491..0000000000 --- a/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using Avalonia.OpenGL.Egl; -using static Avalonia.OpenGL.Egl.EglConsts; - -namespace Avalonia.OpenGL.Angle -{ - public class AngleWin32EglDisplay : EglDisplay - { - struct AngleInfo - { - public IntPtr Display { get; set; } - public AngleOptions.PlatformApi PlatformApi { get; set; } - } - - static AngleInfo CreateAngleDisplay(EglInterface _egl) - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - throw new PlatformNotSupportedException(); - var display = IntPtr.Zero; - AngleOptions.PlatformApi angleApi = default; - { - if (!_egl.IsGetPlatformDisplayExtAvailable) - throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); - - var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis - ?? new [] { AngleOptions.PlatformApi.DirectX11, AngleOptions.PlatformApi.DirectX9 }; - - foreach (var platformApi in allowedApis) - { - int dapi; - if (platformApi == AngleOptions.PlatformApi.DirectX9) - dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE; - else if (platformApi == AngleOptions.PlatformApi.DirectX11) - dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; - else - continue; - - display = _egl.GetPlatformDisplayExt(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, - new[] { EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE }); - if (display != IntPtr.Zero) - { - angleApi = platformApi; - break; - } - } - - if (display == IntPtr.Zero) - throw new OpenGlException("Unable to create ANGLE display"); - return new AngleInfo { Display = display, PlatformApi = angleApi }; - } - } - - private AngleWin32EglDisplay(EglInterface egl, AngleInfo info) : base(egl, false, info.Display) - { - PlatformApi = info.PlatformApi; - } - - public AngleWin32EglDisplay(EglInterface egl) : this(egl, CreateAngleDisplay(egl)) - { - - } - - public AngleWin32EglDisplay() : this(new AngleEglInterface()) - { - - } - - public AngleOptions.PlatformApi PlatformApi { get; } - - public IntPtr GetDirect3DDevice() - { - if (!EglInterface.QueryDisplayAttribExt(Handle, EglConsts.EGL_DEVICE_EXT, out var eglDevice)) - throw new OpenGlException("Unable to get EGL_DEVICE_EXT"); - if (!EglInterface.QueryDeviceAttribExt(eglDevice, PlatformApi == AngleOptions.PlatformApi.DirectX9 ? EGL_D3D9_DEVICE_ANGLE : EGL_D3D11_DEVICE_ANGLE, out var d3dDeviceHandle)) - throw new OpenGlException("Unable to get EGL_D3D9_DEVICE_ANGLE"); - return d3dDeviceHandle; - } - - public EglSurface WrapDirect3D11Texture(EglPlatformOpenGlInterface egl, IntPtr handle) - { - if (PlatformApi != AngleOptions.PlatformApi.DirectX11) - throw new InvalidOperationException("Current platform API is " + PlatformApi); - return egl.CreatePBufferFromClientBuffer(EGL_D3D_TEXTURE_ANGLE, handle, new[] { EGL_NONE, EGL_NONE }); - } - - public EglSurface WrapDirect3D11Texture(EglPlatformOpenGlInterface egl, IntPtr handle, int offsetX, int offsetY, int width, int height) - { - if (PlatformApi != AngleOptions.PlatformApi.DirectX11) - throw new InvalidOperationException("Current platform API is " + PlatformApi); - return egl.CreatePBufferFromClientBuffer(EGL_D3D_TEXTURE_ANGLE, handle, new[] { EGL_WIDTH, width, EGL_HEIGHT, height, EGL_FLEXIBLE_SURFACE_COMPATIBILITY_SUPPORTED_ANGLE, EGL_TRUE, EGL_TEXTURE_OFFSET_X_ANGLE, offsetX, EGL_TEXTURE_OFFSET_Y_ANGLE, offsetY, EGL_NONE }); - } - } -} 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/Controls/OpenGlControlBase.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs index e13ee80864..c62a3d8d2f 100644 --- a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs @@ -1,8 +1,10 @@ using System; +using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Logging; using Avalonia.Media; using Avalonia.OpenGL.Imaging; +using Avalonia.VisualTree; using static Avalonia.OpenGL.GlConsts; namespace Avalonia.OpenGL.Controls @@ -14,8 +16,10 @@ namespace Avalonia.OpenGL.Controls private OpenGlBitmap _bitmap; private IOpenGlBitmapAttachment _attachment; private PixelSize _depthBufferSize; - private bool _glFailed; - private bool _initialized; + + private Task _initialization; + private IOpenGlTextureSharingRenderInterfaceContextFeature _feature; + protected GlVersion GlVersion { get; private set; } public sealed override void Render(DrawingContext context) { @@ -38,13 +42,6 @@ namespace Avalonia.OpenGL.Controls base.Render(context); } - private static void CheckError(GlInterface gl) - { - int err; - while ((err = gl.GetError()) != GL_NO_ERROR) - Console.WriteLine(err); - } - void EnsureTextureAttachment() { _context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, _fb); @@ -54,7 +51,7 @@ namespace Avalonia.OpenGL.Controls _attachment = null; _bitmap?.Dispose(); _bitmap = null; - _bitmap = new OpenGlBitmap(GetPixelSize(), new Vector(96, 96)); + _bitmap = new OpenGlBitmap(_feature, GetPixelSize(), new Vector(96, 96)); _attachment = _bitmap.CreateFramebufferAttachment(_context); } } @@ -87,9 +84,11 @@ namespace Avalonia.OpenGL.Controls gl.ActiveTexture(GL_TEXTURE0); gl.BindTexture(GL_TEXTURE_2D, 0); gl.BindFramebuffer(GL_FRAMEBUFFER, 0); - gl.DeleteFramebuffer(_fb); + if (_fb != 0) + gl.DeleteFramebuffer(_fb); _fb = 0; - gl.DeleteRenderbuffer(_depthBuffer); + if (_depthBuffer != 0) + gl.DeleteRenderbuffer(_depthBuffer); _depthBuffer = 0; _attachment?.Dispose(); _attachment = null; @@ -98,10 +97,10 @@ namespace Avalonia.OpenGL.Controls try { - if (_initialized) + if (_initialization is { Status: TaskStatus.RanToCompletion, Result: true }) { - _initialized = false; OnOpenGlDeinit(_context.GlInterface, _fb); + _initialization = null; } } finally @@ -111,6 +110,11 @@ namespace Avalonia.OpenGL.Controls } } } + + _fb = _depthBuffer = 0; + _attachment = null; + _bitmap = null; + _feature = null; } protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) @@ -119,26 +123,12 @@ namespace Avalonia.OpenGL.Controls base.OnDetachedFromVisualTree(e); } - private bool EnsureInitializedCore() + private bool EnsureInitializedCore(IOpenGlTextureSharingRenderInterfaceContextFeature feature) { - if (_context != null) - return true; - - if (_glFailed) - return false; - - var feature = AvaloniaLocator.Current.GetService(); - if (feature == null) - return false; - if (!feature.CanShareContexts) - { - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: current platform does not support multithreaded context sharing"); - return false; - } try { _context = feature.CreateSharedContext(); + _feature = feature; } catch (Exception e) { @@ -157,7 +147,7 @@ namespace Avalonia.OpenGL.Controls GlVersion = _context.Version; try { - _bitmap = new OpenGlBitmap(GetPixelSize(), new Vector(96, 96)); + _bitmap = new OpenGlBitmap(_feature, GetPixelSize(), new Vector(96, 96)); if (!_bitmap.SupportsContext(_context)) { Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", @@ -212,15 +202,68 @@ namespace Avalonia.OpenGL.Controls return true; } + void ContextLost() + { + _context = null; + _feature = null; + _initialization = null; + _attachment = null; + _bitmap = null; + _fb = 0; + _depthBuffer = 0; + _depthBufferSize = default; + OnOpenGlLost(); + } + private bool EnsureInitialized() { - if (_initialized) - return true; - _glFailed = !(_initialized = EnsureInitializedCore()); - if (_glFailed) + if (_initialization != null) + { + // Check if we've previously failed to initialize OpenGL on this platform + if (_initialization is { IsCompleted: true, Result: false } || + _initialization?.IsFaulted == true) + return false; + + // Check if we are still waiting for init to complete + if (_initialization is { IsCompleted: false }) + return false; + + if (_context.IsLost) + ContextLost(); + else + return true; + } + + _initialization = InitializeAsync(); + return false; + + } + + private async Task InitializeAsync() + { + var contextSharingFeature = + (IOpenGlTextureSharingRenderInterfaceContextFeature) + await this.GetVisualRoot()!.Renderer.TryGetRenderInterfaceFeature( + typeof(IOpenGlTextureSharingRenderInterfaceContextFeature)); + + if (contextSharingFeature == null || !contextSharingFeature.CanCreateSharedContext) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL: current platform does not support multithreaded context sharing"); return false; + } + + if (!EnsureInitializedCore(contextSharingFeature)) + { + DoCleanup(); + return false; + } + using (_context.MakeCurrent()) OnOpenGlInit(_context.GlInterface, _fb); + + InvalidateVisual(); + return true; } @@ -242,6 +285,11 @@ namespace Avalonia.OpenGL.Controls } + protected virtual void OnOpenGlLost() + { + + } + protected abstract void OnOpenGlRender(GlInterface gl, int fb); } } diff --git a/src/Avalonia.OpenGL/Egl/EglConsts.cs b/src/Avalonia.OpenGL/Egl/EglConsts.cs index 1268845f0f..428d11857f 100644 --- a/src/Avalonia.OpenGL/Egl/EglConsts.cs +++ b/src/Avalonia.OpenGL/Egl/EglConsts.cs @@ -196,6 +196,11 @@ namespace Avalonia.OpenGL.Egl // public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_D3D_WARP_ANGLE = 0x320B; // public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_D3D_REFERENCE_ANGLE = 0x320C; +// + // EXT_platform_device + public const int EGL_PLATFORM_DEVICE_EXT = 0x313F; + + //EXT_device_query public const int EGL_DEVICE_EXT = 0x322C; diff --git a/src/Avalonia.OpenGL/Egl/EglContext.cs b/src/Avalonia.OpenGL/Egl/EglContext.cs index e7b5dc7c83..0b4b7d26a1 100644 --- a/src/Avalonia.OpenGL/Egl/EglContext.cs +++ b/src/Avalonia.OpenGL/Egl/EglContext.cs @@ -1,6 +1,8 @@ using System; -using System.Reactive.Disposables; +using System.Collections.Generic; using System.Threading; +using Avalonia.Platform; +using Avalonia.Reactive; using static Avalonia.OpenGL.Egl.EglConsts; namespace Avalonia.OpenGL.Egl @@ -10,24 +12,32 @@ namespace Avalonia.OpenGL.Egl private readonly EglDisplay _disp; private readonly EglInterface _egl; private readonly EglContext _sharedWith; - private readonly object _lock = new object(); + private bool _isLost; + private IntPtr _context; + private readonly Action _disposeCallback; + private readonly Dictionary _features; + private readonly object _lock; - public EglContext(EglDisplay display, EglInterface egl, EglContext sharedWith, IntPtr ctx, Func offscreenSurface, - GlVersion version, int sampleCount, int stencilSize) + internal EglContext(EglDisplay display, EglInterface egl, EglContext sharedWith, IntPtr ctx, EglSurface offscreenSurface, + GlVersion version, int sampleCount, int stencilSize, Action disposeCallback, Dictionary features) { _disp = display; _egl = egl; _sharedWith = sharedWith; - Context = ctx; - OffscreenSurface = offscreenSurface(this); + _context = ctx; + _disposeCallback = disposeCallback; + _features = features; + OffscreenSurface = offscreenSurface; Version = version; SampleCount = sampleCount; StencilSize = stencilSize; + _lock = display.ContextSharedSyncRoot ?? new object(); using (MakeCurrent()) GlInterface = GlInterface.FromNativeUtf8GetProcAddress(version, _egl.GetProcAddress); } - public IntPtr Context { get; } + public IntPtr Context => + _context == IntPtr.Zero ? throw new ObjectDisposedException(nameof(EglContext)) : _context; public EglSurface OffscreenSurface { get; } public GlVersion Version { get; } public GlInterface GlInterface { get; } @@ -40,7 +50,7 @@ namespace Avalonia.OpenGL.Egl private readonly EglInterface _egl; private readonly object _l; private readonly IntPtr _display; - private IntPtr _context, _read, _draw; + public IntPtr _context, _read, _draw; public RestoreContext(EglInterface egl, IntPtr defDisplay, object l) { @@ -66,6 +76,9 @@ namespace Avalonia.OpenGL.Egl public IDisposable MakeCurrent(EglSurface surface) { + if (IsLost) + throw new PlatformGraphicsContextLostException(); + Monitor.Enter(_lock); var success = false; try @@ -74,8 +87,18 @@ namespace Avalonia.OpenGL.Egl var surf = surface ?? OffscreenSurface; _egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); if (!_egl.MakeCurrent(_disp.Handle, surf?.DangerousGetHandle() ?? IntPtr.Zero, - surf?.DangerousGetHandle() ?? IntPtr.Zero, Context)) - throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl); + surf?.DangerousGetHandle() ?? IntPtr.Zero, Context)) + { + var error = _egl.GetError(); + if (error == EGL_CONTEXT_LOST) + { + NotifyContextLost(); + throw new PlatformGraphicsContextLostException(); + } + + throw OpenGlException.GetFormattedEglException("eglMakeCurrent", error); + } + success = true; return old; } @@ -85,7 +108,15 @@ namespace Avalonia.OpenGL.Egl Monitor.Exit(_lock); } } + + public void NotifyContextLost() + { + _isLost = true; + _disp.OnContextLost(this); + } + public bool IsLost => _isLost || _disp.IsLost || Context == IntPtr.Zero; + public IDisposable EnsureCurrent() { if(IsCurrent) @@ -110,12 +141,32 @@ namespace Avalonia.OpenGL.Egl || _sharedWith != null && _sharedWith == c._sharedWith; } + public bool CanCreateSharedContext => _disp.SupportsSharing; + + public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) => + _disp.CreateContext(new EglContextOptions + { + ShareWith = _sharedWith ?? this + }); + public bool IsCurrent => _egl.GetCurrentDisplay() == _disp.Handle && _egl.GetCurrentContext() == Context; public void Dispose() { + if(_context == IntPtr.Zero) + return; _egl.DestroyContext(_disp.Handle, Context); OffscreenSurface?.Dispose(); + _context = IntPtr.Zero; + _disp.OnContextDisposed(this); + _disposeCallback?.Invoke(); + } + + public object TryGetFeature(Type featureType) + { + if (_features?.TryGetValue(featureType, out var feature) == true) + return feature; + return null; } } } diff --git a/src/Avalonia.OpenGL/Egl/EglDisplay.cs b/src/Avalonia.OpenGL/Egl/EglDisplay.cs index d3d85e8c83..c67d7674a8 100644 --- a/src/Avalonia.OpenGL/Egl/EglDisplay.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplay.cs @@ -1,193 +1,179 @@ using System; +using System.Collections.Generic; using System.Linq; +using System.Threading; +using Avalonia.Reactive; using static Avalonia.OpenGL.Egl.EglConsts; namespace Avalonia.OpenGL.Egl { - public class EglDisplay + public class EglDisplay : IDisposable { private readonly EglInterface _egl; + private IntPtr _display; + private readonly EglDisplayOptions _options; + private EglConfigInfo _config; + private bool _isLost; + private object _lock = new(); + public bool SupportsSharing { get; } - private readonly IntPtr _display; - private readonly IntPtr _config; - private readonly int[] _contextAttributes; - private readonly int _surfaceType; public IntPtr Handle => _display; - public IntPtr Config => _config; - private int _sampleCount; - private int _stencilSize; - private GlVersion _version; - - public EglDisplay(EglInterface egl, bool supportsSharing) : this(egl, supportsSharing, -1, IntPtr.Zero, null) + public IntPtr Config => _config.Config; + internal bool SingleContext => !_options.SupportsMultipleContexts; + private List _contexts = new(); + + public EglDisplay() : this(new EglDisplayCreationOptions { - - } - - static IntPtr CreateDisplay(EglInterface egl, int platformType, IntPtr platformDisplay, int[] attrs) + Egl = new EglInterface() + }) { - var display = IntPtr.Zero; - if (platformType == -1 && platformDisplay == IntPtr.Zero) - { - if (display == IntPtr.Zero) - display = egl.GetDisplay(IntPtr.Zero); - } - else - { - if (!egl.IsGetPlatformDisplayExtAvailable) - throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl"); - display = egl.GetPlatformDisplayExt(platformType, platformDisplay, attrs); - } - - if (display == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglGetDisplay", egl); - return display; + } - public EglDisplay(EglInterface egl, bool supportsSharing, int platformType, IntPtr platformDisplay, int[] attrs) - : this(egl, supportsSharing, CreateDisplay(egl, platformType, platformDisplay, attrs)) + public EglDisplay(EglDisplayCreationOptions options) : this(EglDisplayUtils.CreateDisplay(options), options) { - + } - - public EglDisplay(EglInterface egl, bool supportsSharing, IntPtr display) + + public EglDisplay(IntPtr display, EglDisplayOptions options) { - _egl = egl; - SupportsSharing = supportsSharing; + _egl = options.Egl; + SupportsSharing = options.SupportsContextSharing; _display = display; + _options = options; if(_display == IntPtr.Zero) throw new ArgumentException(); - - - if (!_egl.Initialize(_display, out var major, out var minor)) - throw OpenGlException.GetFormattedException("eglInitialize", _egl); - - var glProfiles = AvaloniaLocator.Current.GetService()?.GlProfiles - ?? new[] - { - new GlVersion(GlProfileType.OpenGLES, 3, 0), - new GlVersion(GlProfileType.OpenGLES, 2, 0) - }; - var cfgs = glProfiles.Select(x => + _config = EglDisplayUtils.InitializeAndGetConfig(_egl, display, options.GlVersions); + } + + public EglInterface EglInterface => _egl; + public EglContext CreateContext(EglContextOptions options) + { + if (SingleContext && _contexts.Any()) + throw new OpenGlException("This EGLDisplay can only have one active context"); + + options ??= new EglContextOptions(); + lock (_lock) { - var typeBit = EGL_OPENGL_ES3_BIT; + var share = options.ShareWith; + if (share != null && !SupportsSharing) + throw new NotSupportedException("Context sharing is not supported by this display"); - switch (x.Major) - { - case 2: - typeBit = EGL_OPENGL_ES2_BIT; - break; + var offscreenSurface = options.OffscreenSurface; - case 1: - typeBit = EGL_OPENGL_ES_BIT; - break; - } - - return new + if (offscreenSurface == null) { - Attributes = new[] + // Check if eglMakeCurrent can work with EGL_NONE as read-write surfaces + var extensions = _egl.QueryString(Handle, EGL_EXTENSIONS); + if (extensions?.Contains("EGL_KHR_surfaceless_context") != true) { - EGL_CONTEXT_MAJOR_VERSION, x.Major, - EGL_CONTEXT_MINOR_VERSION, x.Minor, - EGL_NONE - }, - Api = EGL_OPENGL_ES_API, - RenderableTypeBit = typeBit, - Version = x - }; - }); - - foreach (var cfg in cfgs) - { - if (!_egl.BindApi(cfg.Api)) - continue; - foreach(var surfaceType in new[]{EGL_PBUFFER_BIT|EGL_WINDOW_BIT, EGL_WINDOW_BIT}) - foreach(var stencilSize in new[]{8, 1, 0}) - foreach (var depthSize in new []{8, 1, 0}) + // Attempt to create a PBuffer as a surface for offscreen rendering + if ((_config.SurfaceType | EGL_PBUFFER_BIT) == 0) + throw new InvalidOperationException( + "Platform doesn't support EGL_KHR_surfaceless_context and PBUFFER surfaces"); + + var pBufferSurface = _egl.CreatePBufferSurface(_display, Config, + new[] { EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE }); + if (pBufferSurface == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreatePBufferSurface", _egl); + + offscreenSurface = new EglSurface(this, pBufferSurface); + } + } + + var ctx = _egl.CreateContext(_display, Config, share?.Context ?? IntPtr.Zero, _config.Attributes); + if (ctx == IntPtr.Zero) { - var attribs = new[] - { - EGL_SURFACE_TYPE, surfaceType, - EGL_RENDERABLE_TYPE, cfg.RenderableTypeBit, - EGL_RED_SIZE, 8, - EGL_GREEN_SIZE, 8, - EGL_BLUE_SIZE, 8, - EGL_ALPHA_SIZE, 8, - EGL_STENCIL_SIZE, stencilSize, - EGL_DEPTH_SIZE, depthSize, - EGL_NONE - }; - if (!_egl.ChooseConfig(_display, attribs, out _config, 1, out int numConfigs)) - continue; - if (numConfigs == 0) - continue; - _contextAttributes = cfg.Attributes; - _surfaceType = surfaceType; - _version = cfg.Version; - _egl.GetConfigAttrib(_display, _config, EGL_SAMPLES, out _sampleCount); - _egl.GetConfigAttrib(_display, _config, EGL_STENCIL_SIZE, out _stencilSize); - goto Found; + var ex = OpenGlException.GetFormattedException("eglCreateContext", _egl); + offscreenSurface?.Dispose(); + throw ex; } + var rv = new EglContext(this, _egl, share, ctx, offscreenSurface, + _config.Version, _config.SampleCount, _config.StencilSize, + options.DisposeCallback, options.ExtraFeatures); + _contexts.Add(rv); + return rv; } - Found: - if (_contextAttributes == null) - throw new OpenGlException("No suitable EGL config was found"); } - public EglDisplay() : this(false) + public EglSurface CreateWindowSurface(IntPtr window) { - + if (window == IntPtr.Zero) + throw new OpenGlException($"Window {window} is invalid."); + + using (Lock()) + { + var s = EglInterface.CreateWindowSurface(Handle, Config, window, + new[] { EGL_NONE, EGL_NONE }); + if (s == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreateWindowSurface", EglInterface); + return new EglSurface(this, s); + } } - public EglDisplay(bool supportsSharing) : this(new EglInterface(), supportsSharing) + public EglSurface CreatePBufferFromClientBuffer (int bufferType, IntPtr handle, int[] attribs) { - + using (Lock()) + { + var s = EglInterface.CreatePbufferFromClientBuffer(Handle, bufferType, handle, + Config, attribs); + + if (s == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreatePbufferFromClientBuffer", EglInterface); + return new EglSurface(this, s); + } } + + protected virtual bool DisplayLockIsSharedWithContexts => false; - public EglInterface EglInterface => _egl; - public EglContext CreateContext(IGlContext share) + internal object ContextSharedSyncRoot => DisplayLockIsSharedWithContexts ? _lock : null; + + internal void OnContextLost(EglContext context) { - if (share != null && !SupportsSharing) - throw new NotSupportedException("Context sharing is not supported by this display"); - - if((_surfaceType|EGL_PBUFFER_BIT) == 0) - throw new InvalidOperationException("Platform doesn't support PBUFFER surfaces"); - var shareCtx = (EglContext)share; - var ctx = _egl.CreateContext(_display, _config, shareCtx?.Context ?? IntPtr.Zero, _contextAttributes); - if (ctx == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreateContext", _egl); - - var extensions = _egl.QueryString(Handle, EGL_EXTENSIONS); - - IntPtr surf = IntPtr.Zero; - if (extensions?.Contains("EGL_KHR_surfaceless_context") != true) + if (_options.ContextLossIsDisplayLoss) + _isLost = true; + } + + internal void OnContextDisposed(EglContext context) + { + lock (_lock) + _contexts.Remove(context); + } + + public bool IsLost + { + get { - surf = _egl.CreatePBufferSurface(_display, _config, - new[] { EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE }); - if (surf == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreatePBufferSurface", _egl); + if (_isLost || _display == IntPtr.Zero) + return true; + if (_options.DeviceLostCheckCallback?.Invoke() == true) + return _isLost = true; + return false; } - - var rv = new EglContext(this, _egl, shareCtx, ctx, - context => - surf == IntPtr.Zero ? null : new EglSurface(this, context, surf), - _version, _sampleCount, _stencilSize); - return rv; + } + + public IDisposable Lock() + { + Monitor.Enter(_lock); + return Disposable.Create(() => { Monitor.Exit(_lock); }); } - public EglContext CreateContext(EglContext share, EglSurface offscreenSurface) + public void Dispose() { - if (share != null && !SupportsSharing) - throw new NotSupportedException("Context sharing is not supported by this display"); - - var ctx = _egl.CreateContext(_display, _config, share?.Context ?? IntPtr.Zero, _contextAttributes); - if (ctx == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreateContext", _egl); - var rv = new EglContext(this, _egl, share, ctx, _ => offscreenSurface, _version, _sampleCount, _stencilSize); - rv.MakeCurrent(null); - return rv; + lock (_lock) + { + foreach(var ctx in _contexts) + ctx.Dispose(); + _contexts.Clear(); + if (_display != IntPtr.Zero) + _egl.Terminate(_display); + _display = IntPtr.Zero; + _config = null; + _options.DisposeCallback?.Invoke(); + } } } } diff --git a/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs b/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs new file mode 100644 index 0000000000..5648645c54 --- /dev/null +++ b/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; + +namespace Avalonia.OpenGL.Egl; + +public class EglDisplayOptions +{ + public EglInterface Egl { get; set; } + public bool SupportsContextSharing { get; set; } + public bool SupportsMultipleContexts { get; set; } + public bool ContextLossIsDisplayLoss { get; set; } + public Func DeviceLostCheckCallback { get; set; } + public Action DisposeCallback { get; set; } + public IEnumerable GlVersions { get; set; } +} + +public class EglContextOptions +{ + public EglContext ShareWith { get; set; } + public EglSurface OffscreenSurface { get; set; } + public Action DisposeCallback { get; set; } + public Dictionary ExtraFeatures { get; set; } +} + +public class EglDisplayCreationOptions : EglDisplayOptions +{ + public int? PlatformType { get; set; } + public IntPtr PlatformDisplay { get; set; } + public int[] PlatformDisplayAttrs { get; set; } +} diff --git a/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs b/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs new file mode 100644 index 0000000000..5573eb39fa --- /dev/null +++ b/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using static Avalonia.OpenGL.Egl.EglConsts; +namespace Avalonia.OpenGL.Egl; + +static class EglDisplayUtils +{ + public static IntPtr CreateDisplay(EglDisplayCreationOptions options) + { + var egl = options.Egl; + var display = IntPtr.Zero; + if (options.PlatformType == null) + { + if (display == IntPtr.Zero) + display = egl.GetDisplay(IntPtr.Zero); + } + else + { + if (!egl.IsGetPlatformDisplayExtAvailable) + throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl"); + + display = egl.GetPlatformDisplayExt(options.PlatformType.Value, options.PlatformDisplay, + options.PlatformDisplayAttrs); + } + + if (display == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglGetDisplay", egl); + return display; + } + + public static EglConfigInfo InitializeAndGetConfig(EglInterface egl, IntPtr display, IEnumerable versions) + { + if (!egl.Initialize(display, out var major, out var minor)) + throw OpenGlException.GetFormattedException("eglInitialize", egl); + + // TODO: AvaloniaLocator.Current.GetService()?.GlProfiles + versions ??= new[] + { + new GlVersion(GlProfileType.OpenGLES, 3, 0), + new GlVersion(GlProfileType.OpenGLES, 2, 0) + }; + + var cfgs = versions + .Where(x => x.Type == GlProfileType.OpenGLES) + .Select(x => + { + var typeBit = EGL_OPENGL_ES3_BIT; + + switch (x.Major) + { + case 2: + typeBit = EGL_OPENGL_ES2_BIT; + break; + + case 1: + typeBit = EGL_OPENGL_ES_BIT; + break; + } + + return new + { + Attributes = new[] + { + EGL_CONTEXT_MAJOR_VERSION, x.Major, + EGL_CONTEXT_MINOR_VERSION, x.Minor, + EGL_NONE + }, + Api = EGL_OPENGL_ES_API, + RenderableTypeBit = typeBit, + Version = x + }; + }); + + foreach (var cfg in cfgs) + { + if (!egl.BindApi(cfg.Api)) + continue; + foreach (var surfaceType in new[] { EGL_PBUFFER_BIT | EGL_WINDOW_BIT, EGL_WINDOW_BIT }) + foreach (var stencilSize in new[] { 8, 1, 0 }) + foreach (var depthSize in new[] { 8, 1, 0 }) + { + var attribs = new[] + { + EGL_SURFACE_TYPE, surfaceType, + EGL_RENDERABLE_TYPE, cfg.RenderableTypeBit, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_STENCIL_SIZE, stencilSize, + EGL_DEPTH_SIZE, depthSize, + EGL_NONE + }; + if (!egl.ChooseConfig(display, attribs, out var config, 1, out int numConfigs)) + continue; + if (numConfigs == 0) + continue; + + + egl.GetConfigAttrib(display, config, EGL_SAMPLES, out var sampleCount); + egl.GetConfigAttrib(display, config, EGL_STENCIL_SIZE, out var returnedStencilSize); + return new EglConfigInfo(config, cfg.Version, surfaceType, cfg.Attributes, sampleCount, + returnedStencilSize); + } + } + + throw new OpenGlException("No suitable EGL config was found"); + } + + +} + +class EglConfigInfo +{ + public IntPtr Config { get; } + public GlVersion Version { get; } + public int SurfaceType { get; } + public int[] Attributes { get; } + public int SampleCount { get; } + public int StencilSize { get; } + + public EglConfigInfo(IntPtr config, GlVersion version, int surfaceType, int[] attributes, int sampleCount, + int stencilSize) + { + Config = config; + Version = version; + SurfaceType = surfaceType; + Attributes = attributes; + SampleCount = sampleCount; + StencilSize = stencilSize; + } +} diff --git a/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs index 3d58660d47..348028bd08 100644 --- a/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs +++ b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs @@ -1,52 +1,61 @@ +using System; using Avalonia.OpenGL.Surfaces; namespace Avalonia.OpenGL.Egl { public class EglGlPlatformSurface : EglGlPlatformSurfaceBase { - private readonly EglPlatformOpenGlInterface _egl; + public interface IEglWindowGlPlatformSurfaceInfo + { + IntPtr Handle { get; } + PixelSize Size { get; } + double Scaling { get; } + } + private readonly IEglWindowGlPlatformSurfaceInfo _info; - public EglGlPlatformSurface(EglPlatformOpenGlInterface egl, IEglWindowGlPlatformSurfaceInfo info) : base() + public EglGlPlatformSurface(IEglWindowGlPlatformSurfaceInfo info) { - _egl = egl; _info = info; } - public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { - var glSurface = _egl.CreateWindowSurface(_info.Handle); - return new RenderTarget(_egl, glSurface, _info); + var eglContext = (EglContext)context; + + var glSurface = eglContext.Display.CreateWindowSurface(_info.Handle); + return new RenderTarget(glSurface, eglContext, _info); } class RenderTarget : EglPlatformSurfaceRenderTargetBase { - private readonly EglPlatformOpenGlInterface _egl; private EglSurface _glSurface; private readonly IEglWindowGlPlatformSurfaceInfo _info; private PixelSize _currentSize; + private readonly IntPtr _handle; - public RenderTarget(EglPlatformOpenGlInterface egl, - EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) : base(egl) + public RenderTarget(EglSurface glSurface, EglContext context, IEglWindowGlPlatformSurfaceInfo info) : base(context) { - _egl = egl; _glSurface = glSurface; _info = info; _currentSize = info.Size; + _handle = _info.Handle; } public override void Dispose() => _glSurface.Dispose(); - - public override IGlPlatformSurfaceRenderingSession BeginDraw() + + public override IGlPlatformSurfaceRenderingSession BeginDrawCore() { - if (_info.Size != _currentSize || _glSurface == null) + if (_info.Size != _currentSize + || _handle != _info.Handle + || _glSurface == null) { _glSurface?.Dispose(); _glSurface = null; - _glSurface = _egl.CreateWindowSurface(_info.Handle); + _glSurface = Context.Display.CreateWindowSurface(_info.Handle); _currentSize = _info.Size; } - return base.BeginDraw(_glSurface, _info); + return base.BeginDraw(_glSurface, _info.Size, _info.Scaling); } } } diff --git a/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs index 4ea6766de2..f66f630556 100644 --- a/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs +++ b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs @@ -5,23 +5,16 @@ namespace Avalonia.OpenGL.Egl { public abstract class EglGlPlatformSurfaceBase : IGlPlatformSurface { - public interface IEglWindowGlPlatformSurfaceInfo - { - IntPtr Handle { get; } - PixelSize Size { get; } - double Scaling { get; } - } - - public abstract IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(); + public abstract IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context); } - public abstract class EglPlatformSurfaceRenderTargetBase : IGlPlatformSurfaceRenderTarget + public abstract class EglPlatformSurfaceRenderTargetBase : IGlPlatformSurfaceRenderTargetWithCorruptionInfo { - private readonly EglPlatformOpenGlInterface _egl; + protected EglContext Context { get; } - protected EglPlatformSurfaceRenderTargetBase(EglPlatformOpenGlInterface egl) + protected EglPlatformSurfaceRenderTargetBase(EglContext context) { - _egl = egl; + Context = context; } public virtual void Dispose() @@ -29,25 +22,33 @@ namespace Avalonia.OpenGL.Egl } - public abstract IGlPlatformSurfaceRenderingSession BeginDraw(); + public IGlPlatformSurfaceRenderingSession BeginDraw() + { + if (Context.IsLost) + throw new RenderTargetCorruptedException(); + + return BeginDrawCore(); + } + + public abstract IGlPlatformSurfaceRenderingSession BeginDrawCore(); protected IGlPlatformSurfaceRenderingSession BeginDraw(EglSurface surface, - EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, Action onFinish = null, bool isYFlipped = false) + PixelSize size, double scaling, Action onFinish = null, bool isYFlipped = false) { - var restoreContext = _egl.PrimaryEglContext.MakeCurrent(surface); + var restoreContext = Context.MakeCurrent(surface); var success = false; try { - var egli = _egl.Display.EglInterface; + var egli = Context.Display.EglInterface; egli.WaitClient(); egli.WaitGL(); egli.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE); - _egl.PrimaryContext.GlInterface.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, 0); + Context.GlInterface.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, 0); success = true; - return new Session(_egl.Display, _egl.PrimaryEglContext, surface, info, restoreContext, onFinish, isYFlipped); + return new Session(Context.Display, Context, surface, size, scaling, restoreContext, onFinish, isYFlipped); } finally { @@ -60,21 +61,21 @@ namespace Avalonia.OpenGL.Egl { private readonly EglContext _context; private readonly EglSurface _glSurface; - private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info; private readonly EglDisplay _display; private readonly IDisposable _restoreContext; private readonly Action _onFinish; public Session(EglDisplay display, EglContext context, - EglSurface glSurface, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, + EglSurface glSurface, PixelSize size, double scaling, IDisposable restoreContext, Action onFinish, bool isYFlipped) { + Size = size; + Scaling = scaling; IsYFlipped = isYFlipped; _context = context; _display = display; _glSurface = glSurface; - _info = info; _restoreContext = restoreContext; _onFinish = onFinish; } @@ -92,9 +93,11 @@ namespace Avalonia.OpenGL.Egl } public IGlContext Context => _context; - public PixelSize Size => _info.Size; - public double Scaling => _info.Scaling; + public PixelSize Size { get; } + public double Scaling { get; } public bool IsYFlipped { get; } } + + public virtual bool IsCorrupted => Context.IsLost; } } diff --git a/src/Avalonia.OpenGL/Egl/EglInterface.cs b/src/Avalonia.OpenGL/Egl/EglInterface.cs index 4fec8e5356..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); } @@ -53,6 +53,9 @@ namespace Avalonia.OpenGL.Egl [GetProcAddress("eglInitialize")] public partial bool Initialize(IntPtr display, out int major, out int minor); + + [GetProcAddress("eglTerminate")] + public partial void Terminate(IntPtr display); [GetProcAddress("eglGetProcAddress")] public partial IntPtr GetProcAddress(IntPtr proc); diff --git a/src/Avalonia.OpenGL/Egl/EglPlatformGraphics.cs b/src/Avalonia.OpenGL/Egl/EglPlatformGraphics.cs new file mode 100644 index 0000000000..faa9f279a6 --- /dev/null +++ b/src/Avalonia.OpenGL/Egl/EglPlatformGraphics.cs @@ -0,0 +1,48 @@ +using System; +using Avalonia.Logging; +using Avalonia.Platform; +using static Avalonia.OpenGL.Egl.EglConsts; + +namespace Avalonia.OpenGL.Egl +{ + public sealed class EglPlatformGraphics : IPlatformGraphics + { + private readonly EglDisplay _display; + public bool UsesSharedContext => false; + public IPlatformGraphicsContext CreateContext() => _display.CreateContext(null); + public IPlatformGraphicsContext GetSharedContext() => throw new NotSupportedException(); + + public EglPlatformGraphics(EglDisplay display) + { + _display = display; + } + + public static void TryInitialize() + { + var feature = TryCreate(); + if (feature != null) + AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); + } + + public static EglPlatformGraphics TryCreate() => TryCreate(() => new EglDisplay(new EglDisplayCreationOptions + { + Egl = new EglInterface(), + // Those are expected to be supported by most EGL implementations + SupportsMultipleContexts = true, + SupportsContextSharing = true + })); + + public static EglPlatformGraphics TryCreate(Func displayFactory) + { + try + { + return new EglPlatformGraphics(displayFactory()); + } + catch(Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log(null, "Unable to initialize EGL-based rendering: {0}", e); + return null; + } + } + } +} diff --git a/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs b/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs deleted file mode 100644 index a1ac2a9d37..0000000000 --- a/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using Avalonia.Logging; -using Avalonia.Platform; -using static Avalonia.OpenGL.Egl.EglConsts; - -namespace Avalonia.OpenGL.Egl -{ - public class EglPlatformOpenGlInterface : IPlatformOpenGlInterface - { - public EglDisplay Display { get; private set; } - public bool CanCreateContexts => true; - public bool CanShareContexts => Display.SupportsSharing; - - public EglContext PrimaryEglContext { get; } - public IGlContext PrimaryContext => PrimaryEglContext; - IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; - - public EglPlatformOpenGlInterface(EglDisplay display) - { - Display = display; - PrimaryEglContext = display.CreateContext(null); - } - - public static void TryInitialize() - { - var feature = TryCreate(); - if (feature != null) - AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); - } - - public static EglPlatformOpenGlInterface TryCreate() => TryCreate(() => new EglDisplay()); - public static EglPlatformOpenGlInterface TryCreate(Func displayFactory) - { - try - { - return new EglPlatformOpenGlInterface(displayFactory()); - } - catch(Exception e) - { - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log(null, "Unable to initialize EGL-based rendering: {0}", e); - return null; - } - } - - public IGlContext CreateContext() => Display.CreateContext(null); - public IGlContext CreateSharedContext() => Display.CreateContext(PrimaryEglContext); - - - public EglSurface CreateWindowSurface(IntPtr window) - { - if (window == IntPtr.Zero) - throw new OpenGlException($"Window {window} is invalid."); - - using (PrimaryContext.MakeCurrent()) - { - var s = Display.EglInterface.CreateWindowSurface(Display.Handle, Display.Config, window, - new[] { EGL_NONE, EGL_NONE }); - if (s == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreateWindowSurface", Display.EglInterface); - return new EglSurface(Display, PrimaryEglContext, s); - } - } - - public EglSurface CreatePBufferFromClientBuffer (int bufferType, IntPtr handle, int[] attribs) - { - using (PrimaryContext.MakeCurrent()) - { - var s = Display.EglInterface.CreatePbufferFromClientBuffer(Display.Handle, bufferType, handle, - Display.Config, attribs); - - if (s == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreatePbufferFromClientBuffer", Display.EglInterface); - return new EglSurface(Display, PrimaryEglContext, s); - } - } - } -} diff --git a/src/Avalonia.OpenGL/Egl/EglSurface.cs b/src/Avalonia.OpenGL/Egl/EglSurface.cs index a93751ca9e..f3ee85b3f6 100644 --- a/src/Avalonia.OpenGL/Egl/EglSurface.cs +++ b/src/Avalonia.OpenGL/Egl/EglSurface.cs @@ -6,19 +6,17 @@ namespace Avalonia.OpenGL.Egl public class EglSurface : SafeHandle { private readonly EglDisplay _display; - private readonly EglContext _context; private readonly EglInterface _egl; - public EglSurface(EglDisplay display, EglContext context, IntPtr surface) : base(surface, true) + public EglSurface(EglDisplay display, IntPtr surface) : base(surface, true) { _display = display; - _context = context; _egl = display.EglInterface; } protected override bool ReleaseHandle() { - using (_context.MakeCurrent()) + using (_display.Lock()) _egl.DestroySurface(_display.Handle, handle); return true; } diff --git a/src/Avalonia.OpenGL/GlVersion.cs b/src/Avalonia.OpenGL/GlVersion.cs index 042ff4c2f0..16ed18e45a 100644 --- a/src/Avalonia.OpenGL/GlVersion.cs +++ b/src/Avalonia.OpenGL/GlVersion.cs @@ -6,7 +6,7 @@ namespace Avalonia.OpenGL OpenGLES } - public struct GlVersion + public record struct GlVersion { public GlProfileType Type { get; } public int Major { get; } diff --git a/src/Avalonia.OpenGL/IGlContext.cs b/src/Avalonia.OpenGL/IGlContext.cs index a52a6535da..d45ea931d0 100644 --- a/src/Avalonia.OpenGL/IGlContext.cs +++ b/src/Avalonia.OpenGL/IGlContext.cs @@ -1,9 +1,11 @@ using System; +using System.Collections.Generic; +using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; namespace Avalonia.OpenGL { - public interface IGlContext : IPlatformGpuContext + public interface IGlContext : IPlatformGraphicsContext { GlVersion Version { get; } GlInterface GlInterface { get; } @@ -12,5 +14,13 @@ namespace Avalonia.OpenGL IDisposable MakeCurrent(); IDisposable EnsureCurrent(); bool IsSharedWith(IGlContext context); + bool CanCreateSharedContext { get; } + IGlContext CreateSharedContext(IEnumerable preferredVersions = null); + } + + public interface IGlPlatformSurfaceRenderTargetFactory + { + bool CanRenderToSurface(IGlContext context, object surface); + IGlPlatformSurfaceRenderTarget CreateRenderTarget(IGlContext context, object surface); } } diff --git a/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs b/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs deleted file mode 100644 index fdb9162164..0000000000 --- a/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Avalonia.OpenGL.Imaging; - -namespace Avalonia.OpenGL -{ - public interface IOpenGlAwarePlatformRenderInterface - { - IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi); - } -} diff --git a/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs b/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs new file mode 100644 index 0000000000..9c22d446ef --- /dev/null +++ b/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Avalonia.OpenGL.Imaging; +using Avalonia.Platform; + +namespace Avalonia.OpenGL +{ + public interface IOpenGlTextureSharingRenderInterfaceContextFeature + { + bool CanCreateSharedContext { get; } + IGlContext CreateSharedContext(IEnumerable preferredVersions = null); + IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi); + } +} diff --git a/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs b/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs deleted file mode 100644 index 4ff7997b03..0000000000 --- a/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Avalonia.Platform; - -namespace Avalonia.OpenGL -{ - public interface IPlatformOpenGlInterface : IPlatformGpu - { - new IGlContext PrimaryContext { get; } - IGlContext CreateSharedContext(); - bool CanShareContexts { get; } - bool CanCreateContexts { get; } - IGlContext CreateContext(); - /*IGlContext TryCreateContext(GlVersion version); - */ - } -} diff --git a/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs b/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs index 7af44cd624..53013ae5a3 100644 --- a/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs +++ b/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs @@ -10,19 +10,15 @@ namespace Avalonia.OpenGL.Imaging { private IOpenGlBitmapImpl _impl; - public OpenGlBitmap(PixelSize size, Vector dpi) - : base(CreateOrThrow(size, dpi)) + public OpenGlBitmap(IOpenGlTextureSharingRenderInterfaceContextFeature feature, + PixelSize size, Vector dpi) + : base(CreateOrThrow(feature, size, dpi)) { _impl = (IOpenGlBitmapImpl)PlatformImpl.Item; } - - static IOpenGlBitmapImpl CreateOrThrow(PixelSize size, Vector dpi) - { - if (!(AvaloniaLocator.Current.GetService() is IOpenGlAwarePlatformRenderInterface - glAware)) - throw new PlatformNotSupportedException("Rendering platform does not support OpenGL integration"); - return glAware.CreateOpenGlBitmap(size, dpi); - } + + static IOpenGlBitmapImpl CreateOrThrow(IOpenGlTextureSharingRenderInterfaceContextFeature feature, + PixelSize size, Vector dpi) => feature.CreateOpenGlBitmap(size, dpi); public IOpenGlBitmapAttachment CreateFramebufferAttachment(IGlContext context) => _impl.CreateFramebufferAttachment(context, SetIsDirty); diff --git a/src/Avalonia.OpenGL/OpenGlException.cs b/src/Avalonia.OpenGL/OpenGlException.cs index 196f507ad8..efe305dba5 100644 --- a/src/Avalonia.OpenGL/OpenGlException.cs +++ b/src/Avalonia.OpenGL/OpenGlException.cs @@ -18,25 +18,33 @@ namespace Avalonia.OpenGL public static OpenGlException GetFormattedException(string funcName, EglInterface egl) { - return GetFormattedException(typeof(EglErrors), funcName, egl.GetError()); + return GetFormattedEglException(funcName, egl.GetError()); } public static OpenGlException GetFormattedException(string funcName, GlInterface gl) { - return GetFormattedException(typeof(GlErrors), funcName, gl.GetError()); + var err = gl.GetError(); + return GetFormattedException(funcName, (GlErrors)err, err); } - private static OpenGlException GetFormattedException(Type consts, string funcName, int errorCode) + public static OpenGlException GetFormattedEglException(string funcName, int errorCode) => + GetFormattedException(funcName, (EglErrors)errorCode,errorCode); + + private static OpenGlException GetFormattedException(string funcName, T errorCode, int intErrorCode) where T : struct, Enum { try { - string errorName = Enum.GetName(consts, errorCode); +#if NET6_0_OR_GREATER + var errorName = Enum.GetName(errorCode); +#else + var errorName = Enum.GetName(typeof(T), errorCode); +#endif return new OpenGlException( - $"{funcName} failed with error {errorName} (0x{errorCode.ToString("X")})", errorCode); + $"{funcName} failed with error {errorName} (0x{errorCode.ToString("X")})", intErrorCode); } catch (ArgumentException) { - return new OpenGlException($"{funcName} failed with error 0x{errorCode.ToString("X")}", errorCode); + return new OpenGlException($"{funcName} failed with error 0x{errorCode.ToString("X")}", intErrorCode); } } } diff --git a/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurface.cs b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurface.cs index 875c215336..d80e72e4e7 100644 --- a/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurface.cs +++ b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurface.cs @@ -2,6 +2,6 @@ namespace Avalonia.OpenGL.Surfaces { public interface IGlPlatformSurface { - IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(); + IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context); } } 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/Base.xaml b/src/Avalonia.Themes.Fluent/Accents/Base.xaml index 259d107b5c..479bcd8531 100644 --- a/src/Avalonia.Themes.Fluent/Accents/Base.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/Base.xaml @@ -22,6 +22,7 @@ 10,6,6,5 20 20 + 8,5,8,6 3 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/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml index 7828fd52ed..126f2c22e0 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml @@ -8,8 +8,6 @@ - - 8,5,8,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/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml index f8b4854553..67e70d154f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml @@ -1,6 +1,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 9156d11a96..2c3550a72f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -4,72 +4,71 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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.Fluent/Controls/RepeatButton.xaml b/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml index a54187104b..fd04c85fed 100644 --- a/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml @@ -10,8 +10,6 @@ - 8,5,8,6 - diff --git a/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml b/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml index 7a46f21534..da2021790a 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml @@ -10,8 +10,6 @@ - 8,5,8,6 - diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml index d8f8267fe5..44ca60e2fa 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -4,8 +4,8 @@ - - + + diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs index eea3d3ad08..a8297953a8 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs @@ -99,15 +99,15 @@ namespace Avalonia.Themes.Fluent var themeVariantResource1 = Mode == FluentThemeMode.Dark ? _baseDark : _baseLight; var themeVariantResource2 = Mode == FluentThemeMode.Dark ? _fluentDark : _fluentLight; var dict = Resources.MergedDictionaries; - if (dict.Count == 2) + if (dict.Count == 0) { - dict.Insert(1, themeVariantResource1); + dict.Add(themeVariantResource1); dict.Add(themeVariantResource2); } else { - dict[1] = themeVariantResource1; - dict[3] = themeVariantResource2; + dict[0] = themeVariantResource1; + dict[1] = themeVariantResource2; } } 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/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Simple/Controls/EmbeddableControlRoot.xaml index 79d6c6d917..3d52464858 100644 --- a/src/Avalonia.Themes.Simple/Controls/EmbeddableControlRoot.xaml +++ b/src/Avalonia.Themes.Simple/Controls/EmbeddableControlRoot.xaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> + 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.Themes.Simple/Controls/SimpleControls.xaml b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml index 4bad556338..093adaeab2 100644 --- a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml @@ -3,70 +3,70 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/SimpleTheme.xaml b/src/Avalonia.Themes.Simple/SimpleTheme.xaml index fe296bd288..5b0cae7fd2 100644 --- a/src/Avalonia.Themes.Simple/SimpleTheme.xaml +++ b/src/Avalonia.Themes.Simple/SimpleTheme.xaml @@ -4,7 +4,7 @@ - + diff --git a/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs b/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs index 56c4cbeac6..42dfafd7e0 100644 --- a/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs +++ b/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs @@ -58,13 +58,13 @@ namespace Avalonia.Themes.Simple { var themeVariantResource = Mode == SimpleThemeMode.Dark ? _simpleDark : _simpleLight; var dict = Resources.MergedDictionaries; - if (dict.Count == 1) + if (dict.Count == 0) { dict.Add(themeVariantResource); } else { - dict[1] = themeVariantResource; + dict[0] = themeVariantResource; } } } 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 e9cb88cb8f..88006f0b47 100644 --- a/src/Avalonia.X11/Glx/GlxContext.cs +++ b/src/Avalonia.X11/Glx/GlxContext.cs @@ -1,7 +1,8 @@ using System; -using System.Reactive.Disposables; +using System.Collections.Generic; using System.Threading; using Avalonia.OpenGL; +using Avalonia.Reactive; namespace Avalonia.X11.Glx { class GlxContext : IGlContext @@ -70,6 +71,8 @@ namespace Avalonia.X11.Glx } public IDisposable MakeCurrent() => MakeCurrent(_defaultXid); + public bool IsLost => false; + public IDisposable EnsureCurrent() { if(IsCurrent) @@ -86,6 +89,11 @@ namespace Avalonia.X11.Glx || _sharedWith != null && _sharedWith == c._sharedWith; } + public bool CanCreateSharedContext => true; + + public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) => + Display.CreateContext(_sharedWith ?? this); + public IDisposable MakeCurrent(IntPtr xid) { Monitor.Enter(_lock); @@ -114,5 +122,7 @@ namespace Avalonia.X11.Glx if (_ownsPBuffer) Glx.DestroyPbuffer(_x11.Display, _defaultXid); } + + public object TryGetFeature(Type featureType) => null; } } diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs index 1e70608168..9b8a8f0b5b 100644 --- a/src/Avalonia.X11/Glx/GlxDisplay.cs +++ b/src/Avalonia.X11/Glx/GlxDisplay.cs @@ -111,8 +111,8 @@ namespace Avalonia.X11.Glx return Glx.CreatePbuffer(_x11.Display, _fbconfig, new[] { GLX_PBUFFER_WIDTH, 1, GLX_PBUFFER_HEIGHT, 1, 0 }); } - - public GlxContext CreateContext() => CreateContext(); + public GlxContext CreateContext() => CreateContext(CreatePBuffer(), null, DeferredContext.SampleCount, + DeferredContext.StencilSize, true); public GlxContext CreateContext(IGlContext share) => CreateContext(CreatePBuffer(), share, share.SampleCount, share.StencilSize, true); diff --git a/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs b/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs index cb4ab4aca0..ebb1e18723 100644 --- a/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs +++ b/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs @@ -9,20 +9,16 @@ namespace Avalonia.X11.Glx class GlxGlPlatformSurface: IGlPlatformSurface { - private readonly GlxDisplay _display; - private readonly GlxContext _context; private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info; - public GlxGlPlatformSurface(GlxDisplay display, GlxContext context, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) + public GlxGlPlatformSurface(EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) { - _display = display; - _context = context; _info = info; } - public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { - return new RenderTarget(_context, _info); + return new RenderTarget((GlxContext)context, _info); } class RenderTarget : IGlPlatformSurfaceRenderTarget diff --git a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs index 0968adc799..bc50fa61aa 100644 --- a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs +++ b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs @@ -6,38 +6,36 @@ using Avalonia.Platform; namespace Avalonia.X11.Glx { - class GlxPlatformOpenGlInterface : IPlatformOpenGlInterface + class GlxPlatformGraphics : IPlatformGraphics { public GlxDisplay Display { get; private set; } public bool CanCreateContexts => true; public bool CanShareContexts => true; - public IGlContext CreateContext() => Display.CreateContext(); - public IGlContext CreateSharedContext() => Display.CreateContext(PrimaryContext); - public GlxContext DeferredContext { get; private set; } - public IGlContext PrimaryContext => DeferredContext; - IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; + public bool UsesSharedContext => false; + IPlatformGraphicsContext IPlatformGraphics.CreateContext() => Display.CreateContext(); + + public IPlatformGraphicsContext GetSharedContext() => throw new NotSupportedException(); public static bool TryInitialize(X11Info x11, IList glProfiles) { var feature = TryCreate(x11, glProfiles); if (feature != null) { - AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); return true; } return false; } - public static GlxPlatformOpenGlInterface TryCreate(X11Info x11, IList glProfiles) + public static GlxPlatformGraphics TryCreate(X11Info x11, IList glProfiles) { try { var disp = new GlxDisplay(x11, glProfiles); - return new GlxPlatformOpenGlInterface + return new GlxPlatformGraphics { - Display = disp, - DeferredContext = disp.DeferredContext + Display = disp }; } catch(Exception e) 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/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 6041b53a62..16de10e163 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -113,7 +113,8 @@ namespace Avalonia.X11 image->yhot = hotSpot.Y; image->pixels = (IntPtr)(image + 1); - using (var renderTarget = platformRenderInterface.CreateRenderTarget(new[] { this })) + using (var cpuContext = platformRenderInterface.CreateBackendContext(null)) + using (var renderTarget = cpuContext.CreateRenderTarget(new[] { this })) using (var ctx = renderTarget.CreateDrawingContext(null)) { var r = new Rect(_pixelSize.ToSize(1)); 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/X11IconLoader.cs b/src/Avalonia.X11/X11IconLoader.cs index 4ae1c1599f..bf59d72c0f 100644 --- a/src/Avalonia.X11/X11IconLoader.cs +++ b/src/Avalonia.X11/X11IconLoader.cs @@ -41,7 +41,8 @@ namespace Avalonia.X11 _width = Math.Min(bitmap.PixelSize.Width, 128); _height = Math.Min(bitmap.PixelSize.Height, 128); _bdata = new uint[_width * _height]; - using(var rt = AvaloniaLocator.Current.GetService().CreateRenderTarget(new[]{this})) + using(var cpuContext = AvaloniaLocator.Current.GetRequiredService().CreateBackendContext(null)) + using(var rt = cpuContext.CreateRenderTarget(new[]{this})) using (var ctx = rt.CreateDrawingContext(null)) ctx.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), new Rect(0, 0, _width, _height)); diff --git a/src/Avalonia.X11/X11ImmediateRendererProxy.cs b/src/Avalonia.X11/X11ImmediateRendererProxy.cs index 293e5110f7..ef061cbe5c 100644 --- a/src/Avalonia.X11/X11ImmediateRendererProxy.cs +++ b/src/Avalonia.X11/X11ImmediateRendererProxy.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; using Avalonia.VisualTree; @@ -14,11 +16,11 @@ namespace Avalonia.X11 private bool _running; private object _lock = new object(); - public X11ImmediateRendererProxy(Visual root, IRenderLoop loop) + public X11ImmediateRendererProxy(Visual root, IRenderLoop loop, Func renderTargetFactory, + PlatformRenderInterfaceContextManager renderContext) { _loop = loop; - _renderer = new ImmediateRenderer(root); - + _renderer = new ImmediateRenderer(root, renderTargetFactory, renderContext); } public void Dispose() @@ -92,6 +94,9 @@ namespace Avalonia.X11 _renderer.Stop(); } + public ValueTask TryGetRenderInterfaceFeature(Type featureType) => + _renderer.TryGetRenderInterfaceFeature(featureType); + public bool NeedsUpdate => false; public void Update(TimeSpan time) { 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 cbb782edd0..0f9ef72a3f 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -30,6 +30,7 @@ namespace Avalonia.X11 public X11Info Info { get; private set; } public IX11Screens X11Screens { get; private set; } public Compositor Compositor { get; private set; } + public PlatformRenderInterfaceContextManager RenderInterface { get; private set; } public IScreenImpl Screens { get; private set; } public X11PlatformOptions Options { get; private set; } public IntPtr OrphanedWindow { get; private set; } @@ -96,17 +97,17 @@ namespace Avalonia.X11 if (options.UseGpu) { if (options.UseEGL) - EglPlatformOpenGlInterface.TryInitialize(); + EglPlatformGraphics.TryInitialize(); else - GlxPlatformOpenGlInterface.TryInitialize(Info, Options.GlProfiles); + GlxPlatformGraphics.TryInitialize(Info, Options.GlProfiles); } - var gl = AvaloniaLocator.Current.GetService(); - if (gl != null) - AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); - + 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); } @@ -302,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/X11Structs.cs b/src/Avalonia.X11/X11Structs.cs index 7e46606c36..26515762b4 100644 --- a/src/Avalonia.X11/X11Structs.cs +++ b/src/Avalonia.X11/X11Structs.cs @@ -1768,7 +1768,9 @@ namespace Avalonia.X11 { } [StructLayout(LayoutKind.Sequential)] +#pragma warning disable CA1815 // Override equals and operator equals on value types public unsafe struct XImage +#pragma warning restore CA1815 // Override equals and operator equals on value types { public int width, height; /* size of image */ public int xoffset; /* number of pixels offset in X direction */ @@ -1799,7 +1801,7 @@ namespace Avalonia.X11 { internal IntPtr green_mask; internal IntPtr blue_mask; internal int colormap_size; - internal int bits_per_rgb; + internal int bits_per_rgb; } internal enum XIMFeedback 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 2f84e15b32..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 { @@ -74,7 +76,7 @@ namespace Avalonia.X11 _touch = new TouchDevice(); _keyboard = platform.KeyboardDevice; - var glfeature = AvaloniaLocator.Current.GetService(); + var glfeature = AvaloniaLocator.Current.GetService(); XSetWindowAttributes attr = new XSetWindowAttributes(); var valueMask = default(SetWindowValuemask); @@ -96,13 +98,13 @@ namespace Avalonia.X11 // OpenGL seems to be do weird things to it's current window which breaks resize sometimes _useRenderWindow = glfeature != null; - var glx = glfeature as GlxPlatformOpenGlInterface; + var glx = glfeature as GlxPlatformGraphics; if (glx != null) visualInfo = *glx.Display.VisualInfo; else if (glfeature == null) visualInfo = _x11.TransparentVisualInfo; - var egl = glfeature as EglPlatformOpenGlInterface; + var egl = glfeature as EglPlatformGraphics; var visual = IntPtr.Zero; var depth = 24; @@ -176,11 +178,9 @@ namespace Avalonia.X11 if (egl != null) surfaces.Insert(0, - new EglGlPlatformSurface(egl, - new SurfaceInfo(this, _x11.DeferredDisplay, _handle, _renderHandle))); + new EglGlPlatformSurface(new SurfaceInfo(this, _x11.DeferredDisplay, _handle, _renderHandle))); if (glx != null) - surfaces.Insert(0, new GlxGlPlatformSurface(glx.Display, glx.DeferredContext, - new SurfaceInfo(this, _x11.Display, _handle, _renderHandle))); + surfaces.Insert(0, new GlxGlPlatformSurface(new SurfaceInfo(this, _x11.Display, _handle, _renderHandle))); surfaces.Add(Handle); @@ -300,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) { @@ -356,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 @@ -391,12 +394,14 @@ namespace Avalonia.X11 return _platform.Options.UseDeferredRendering ? _platform.Options.UseCompositor - ? new CompositingRenderer(root, this._platform.Compositor) - : new DeferredRenderer(root, loop) + ? new CompositingRenderer(root, this._platform.Compositor, () => Surfaces) + : new DeferredRenderer(root, loop, () => _platform.RenderInterface.CreateRenderTarget(Surfaces), _platform.RenderInterface) { RenderOnlyOnRenderThread = true } - : (IRenderer)new X11ImmediateRendererProxy((Visual)root, loop); + : new X11ImmediateRendererProxy((Visual)root, loop, + () => _platform.RenderInterface.CreateRenderTarget(Surfaces), + _platform.RenderInterface); } void OnEvent(ref XEvent ev) @@ -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 a407e1e4d8..294216ee03 100644 --- a/src/Browser/Avalonia.Browser/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser/AvaloniaView.cs @@ -11,13 +11,13 @@ using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; +using Avalonia.Platform; using Avalonia.Rendering.Composition; using Avalonia.Threading; 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); @@ -106,7 +106,7 @@ namespace Avalonia.Browser _dpi = DomHelper.ObserveDpi(OnDpiChanged); - _useGL = skiaOptions?.CustomGpuFactory != 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 ee4f6eca9b..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,13 +37,11 @@ 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) - .UseSkia() - .With(new SkiaOptions { CustomGpuFactory = () => new BrowserSkiaGpu() }); + .UseSkia(); } } diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs index 69e2d27181..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; @@ -166,7 +168,8 @@ namespace Avalonia.Browser public IRenderer CreateRenderer(IRenderRoot root) { var loop = AvaloniaLocator.Current.GetRequiredService(); - return new CompositingRenderer(root, new Compositor(loop, null)); + return new CompositingRenderer(root, + new Compositor(loop, AvaloniaLocator.Current.GetRequiredService()), () => Surfaces); } public void Invalidate(Rect rect) @@ -200,7 +203,11 @@ namespace Avalonia.Browser public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { - + if (transparencyLevel == WindowTransparencyLevel.None + || transparencyLevel == WindowTransparencyLevel.Transparent) + { + TransparencyLevel = transparencyLevel; + } } public Size ClientSize => _clientSize; @@ -220,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 a96ead93cb..4d3d8dcd97 100644 --- a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs +++ b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs @@ -1,5 +1,8 @@ +using System; using System.Collections.Generic; +using Avalonia.Platform; using Avalonia.Skia; +using Avalonia.Reactive; namespace Avalonia.Browser.Skia { @@ -22,5 +25,28 @@ namespace Avalonia.Browser.Skia { return null; } + + public void Dispose() + { + + } + + public object? TryGetFeature(Type t) => null; + + public bool IsLost => false; + + public IDisposable EnsureCurrent() + { + return Disposable.Empty; + } + } + + class BrowserSkiaGraphics : IPlatformGraphics + { + private BrowserSkiaGpu _skia = new(); + public bool UsesSharedContext => true; + public IPlatformGraphicsContext CreateContext() => throw new NotSupportedException(); + + public IPlatformGraphicsContext GetSharedContext() => _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..dc580f0fcb 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"; 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/WindowingPlatform.cs b/src/Browser/Avalonia.Browser/WindowingPlatform.cs index 6535e9534c..6493374a50 100644 --- a/src/Browser/Avalonia.Browser/WindowingPlatform.cs +++ b/src/Browser/Avalonia.Browser/WindowingPlatform.cs @@ -1,5 +1,6 @@ using System; using System.Threading; +using Avalonia.Browser.Skia; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Platform; @@ -43,6 +44,7 @@ namespace Avalonia.Browser .Bind().ToConstant(new RenderLoop()) .Bind().ToConstant(ManualTriggerRenderTimer.Instance) .Bind().ToConstant(instance) + .Bind().ToConstant(new BrowserSkiaGraphics()) .Bind().ToSingleton() .Bind().ToSingleton(); } 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 b64423ec10..6032f3a92c 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -1,4 +1,4 @@ -using System; + using System; using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Input; @@ -25,7 +25,7 @@ namespace Avalonia.LinuxFramebuffer Surfaces = new object[] { _outputBackend }; - Invalidate(default(Rect)); + Invalidate(default); _inputBackend.Initialize(this, e => Input?.Invoke(e)); } @@ -33,7 +33,7 @@ namespace Avalonia.LinuxFramebuffer { var factory = AvaloniaLocator.Current.GetService(); var renderLoop = AvaloniaLocator.Current.GetService(); - return factory?.Create(root, renderLoop) ?? new CompositingRenderer(root, LinuxFramebufferPlatform.Compositor); + return factory?.Create(root, renderLoop) ?? new CompositingRenderer(root, LinuxFramebufferPlatform.Compositor, () => Surfaces); } public void Dispose() 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 d881a97af2..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) @@ -41,7 +40,7 @@ namespace Avalonia.LinuxFramebuffer { Threading = new InternalPlatformThreadingInterface(); if (_fb is IGlOutputBackend gl) - AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl.PlatformOpenGlInterface); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl.PlatformGraphics); var opts = AvaloniaLocator.Current.GetService() ?? new LinuxFramebufferPlatformOptions(); @@ -56,11 +55,11 @@ namespace Avalonia.LinuxFramebuffer Compositor = new Compositor( AvaloniaLocator.Current.GetRequiredService(), - AvaloniaLocator.Current.GetService()); + AvaloniaLocator.Current.GetService()); } - 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 a4d7d8b1eb..d61dcd4f91 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -6,8 +6,8 @@ using System.Runtime.InteropServices; using Avalonia.OpenGL; 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; @@ -25,10 +25,22 @@ namespace Avalonia.LinuxFramebuffer.Output get => _outputOptions.Scaling; set => _outputOptions.Scaling = value; } - public IGlContext PrimaryContext => _deferredContext; - private EglPlatformOpenGlInterface _platformGl; - public IPlatformOpenGlInterface PlatformOpenGlInterface => _platformGl; + class SharedContextGraphics : IPlatformGraphics + { + private readonly IPlatformGraphicsContext _context; + + public SharedContextGraphics(IPlatformGraphicsContext context) + { + _context = context; + } + public bool UsesSharedContext => true; + public IPlatformGraphicsContext CreateContext() => throw new NotSupportedException(); + + public IPlatformGraphicsContext GetSharedContext() => _context; + } + + public IPlatformGraphics PlatformGraphics { get; private set; } public DrmOutput(DrmCard card, DrmResources resources, DrmConnector connector, DrmModeInfo modeInfo, DrmOutputOptions? options = null) @@ -37,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; @@ -161,11 +173,22 @@ namespace Avalonia.LinuxFramebuffer.Output if(_gbmTargetSurface == IntPtr.Zero) throw new InvalidOperationException("Unable to create GBM surface"); - _eglDisplay = new EglDisplay(new EglInterface(eglGetProcAddress), false, 0x31D7, device, null); - _platformGl = new EglPlatformOpenGlInterface(_eglDisplay); - _eglSurface = _platformGl.CreateWindowSurface(_gbmTargetSurface); + _eglDisplay = new EglDisplay( + new EglDisplayCreationOptions + { + Egl = new EglInterface(eglGetProcAddress), + PlatformType = 0x31D7, + PlatformDisplay = device, + SupportsMultipleContexts = true, + SupportsContextSharing = true + }); + + var surface = _eglDisplay.EglInterface.CreateWindowSurface(_eglDisplay.Handle, _eglDisplay.Config, _gbmTargetSurface, new[] { EglConsts.EGL_NONE, EglConsts.EGL_NONE }); + + _eglSurface = new EglSurface(_eglDisplay, surface); - _deferredContext = _platformGl.PrimaryEglContext; + _deferredContext = _eglDisplay.CreateContext(null); + PlatformGraphics = new SharedContextGraphics(_deferredContext); var initialBufferSwappingColorR = _outputOptions.InitialBufferSwappingColor.R / 255.0f; var initialBufferSwappingColorG = _outputOptions.InitialBufferSwappingColor.G / 255.0f; @@ -206,11 +229,17 @@ namespace Avalonia.LinuxFramebuffer.Output } - public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() => new RenderTarget(this); + + + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { - return new RenderTarget(this); + if (context != _deferredContext) + throw new InvalidOperationException( + "This platform backend can only create render targets for its primary context"); + return CreateGlRenderTarget(); } - + class RenderTarget : IGlPlatformSurfaceRenderTarget { private readonly DrmOutput _parent; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/IGlOutputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/IGlOutputBackend.cs index 7bc73d590c..e415b1782e 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/IGlOutputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/IGlOutputBackend.cs @@ -1,9 +1,10 @@ using Avalonia.OpenGL; +using Avalonia.Platform; namespace Avalonia.LinuxFramebuffer.Output { public interface IGlOutputBackend : IOutputBackend { - public IPlatformOpenGlInterface PlatformOpenGlInterface { get; } + public IPlatformGraphics PlatformGraphics { get; } } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs index b4c951fc5e..6f6420f66d 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs @@ -56,8 +56,8 @@ namespace Avalonia.Markup.Xaml /// /// Collection of documents. /// Xaml loader configuration. - /// The loaded objects per each input document. - public static IReadOnlyList LoadGroup(IReadOnlyCollection documents, RuntimeXamlLoaderConfiguration? configuration = null) + /// The loaded objects per each input document. If document was removed, the element by index is null. + public static IReadOnlyList LoadGroup(IReadOnlyCollection documents, RuntimeXamlLoaderConfiguration? configuration = null) => AvaloniaXamlIlRuntimeCompiler.LoadGroup(documents, configuration ?? new RuntimeXamlLoaderConfiguration()); /// 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/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index b8350c3f11..aaaee39b0d 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -85,6 +85,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions GroupTransformers = new() { + new XamlMergeResourceGroupTransformer(), new AvaloniaXamlIncludeTransformer() }; } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/IXamlAstGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/IXamlAstGroupTransformer.cs index 32bf37431f..eeb5293325 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/IXamlAstGroupTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/IXamlAstGroupTransformer.cs @@ -18,7 +18,7 @@ internal class AstGroupTransformationContext : AstTransformationContext public IXamlDocumentResource CurrentDocument { get; set; } public IReadOnlyCollection Documents { get; } - + public new IXamlAstNode ParseError(string message, IXamlAstNode node) => Error(node, new XamlDocumentParseException(CurrentDocument?.FileSource?.FilePath, message, node)); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs index 1bb05e238c..afc7b569a8 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs @@ -15,6 +15,7 @@ using XamlX.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers; +#nullable enable internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer { public IXamlAstNode Transform(AstGroupTransformationContext context, IXamlAstNode node) @@ -34,40 +35,26 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer { throw new InvalidOperationException($"\"{nodeTypeName}\".Loaded property is expected to be defined"); } - + if (valueNode.Manipulation is not XamlObjectInitializationNode { Manipulation: XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty }) { - return context.ParseError($"Source property must be set on the \"{nodeTypeName}\" node.", node); + throw new XamlDocumentParseException(context.CurrentDocument, + $"Source property must be set on the \"{nodeTypeName}\" node.", valueNode); } - // We expect that AvaloniaXamlIlLanguageParseIntrinsics has already parsed the Uri and created node like: `new Uri(assetPath, uriKind)`. - if (sourceProperty.Values.OfType().FirstOrDefault() is not { } sourceUriNode - || sourceUriNode.Type.GetClrType() != context.GetAvaloniaTypes().Uri - || sourceUriNode.Arguments.FirstOrDefault() is not XamlConstantNode { Constant: string originalAssetPath } - || sourceUriNode.Arguments.Skip(1).FirstOrDefault() is not XamlConstantNode { Constant: int uriKind }) + var (assetPathUri, sourceUriNode) = ResolveSourceFromXamlInclude(context, nodeTypeName, sourceProperty, false); + if (assetPathUri is null) { - // TODO: make it a compiler warning - // Source value can be set with markup extension instead of the Uri object node, we don't support it here yet. return node; } - - var uriPath = new Uri(originalAssetPath, (UriKind)uriKind); - if (!uriPath.IsAbsoluteUri) + else { - var baseUrl = context.CurrentDocument.Uri ?? throw new InvalidOperationException("CurrentDocument URI is null."); - uriPath = new Uri(new Uri(baseUrl, UriKind.Absolute), uriPath); - } - else if (!uriPath.Scheme.Equals("avares", StringComparison.CurrentCultureIgnoreCase)) - { - return context.ParseError( - $"\"{nodeTypeName}.Source\" supports only \"avares://\" absolute or relative uri.", - sourceUriNode, node); + sourceUriNode ??= valueNode; } - var assetPathUri = Uri.UnescapeDataString(uriPath.AbsoluteUri); var assetPath = assetPathUri.Replace("avares://", ""); var assemblyNameSeparator = assetPath.IndexOf('/'); var assembly = assetPath.Substring(0, assemblyNameSeparator); @@ -119,7 +106,7 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer $"Unable to resolve XAML resource \"{assetPathUri}\" in the \"{assembly}\" assembly.", sourceUriNode, node); } - + private static IXamlAstNode FromType(AstTransformationContext context, IXamlType type, IXamlAstNode li, IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly) { @@ -151,7 +138,49 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer new[] { new NewServiceProviderNode(sp, li) }); } - internal class NewServiceProviderNode : XamlAstNode, IXamlAstValueNode,IXamlAstNodeNeedsParentStack, + internal static (string?, IXamlAstNode?) ResolveSourceFromXamlInclude( + AstGroupTransformationContext context, string nodeTypeName, XamlPropertyAssignmentNode sourceProperty, + bool strictSourceValueType) + { + // We expect that AvaloniaXamlIlLanguageParseIntrinsics has already parsed the Uri and created node like: `new Uri(assetPath, uriKind)`. + if (sourceProperty.Values.OfType().FirstOrDefault() is not { } sourceUriNode + || sourceUriNode.Type.GetClrType() != context.GetAvaloniaTypes().Uri + || sourceUriNode.Arguments.FirstOrDefault() is not XamlConstantNode { Constant: string originalAssetPath } + || sourceUriNode.Arguments.Skip(1).FirstOrDefault() is not XamlConstantNode { Constant: int uriKind }) + { + // Source value can be set with markup extension instead of the Uri object node, we don't support it here yet. + var anyPropValue = sourceProperty.Values.FirstOrDefault(); + if (strictSourceValueType) + { + context.Error(anyPropValue, + new XamlDocumentParseException(context.CurrentDocument, + $"\"{nodeTypeName}.Source\" supports only \"avares://\" absolute or relative uri.", anyPropValue)); + } + else + { + // TODO: make it a compiler warning + } + return (null, anyPropValue); + } + + var uriPath = new Uri(originalAssetPath, (UriKind)uriKind); + if (!uriPath.IsAbsoluteUri) + { + var baseUrl = context.CurrentDocument.Uri ?? throw new InvalidOperationException("CurrentDocument URI is null."); + uriPath = new Uri(new Uri(baseUrl, UriKind.Absolute), uriPath); + } + else if (!uriPath.Scheme.Equals("avares", StringComparison.CurrentCultureIgnoreCase)) + { + context.Error(sourceUriNode, + new XamlDocumentParseException(context.CurrentDocument, + $"\"{nodeTypeName}.Source\" supports only \"avares://\" absolute or relative uri.", sourceUriNode)); + return (null, sourceUriNode); + } + + return (Uri.UnescapeDataString(uriPath.AbsoluteUri), sourceUriNode); + } + + private class NewServiceProviderNode : XamlAstNode, IXamlAstValueNode,IXamlAstNodeNeedsParentStack, IXamlAstEmitableNode { public NewServiceProviderNode(IXamlType type, IXamlLineInfo lineInfo) : base(lineInfo) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs new file mode 100644 index 0000000000..8c83c74248 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using XamlX.Ast; +using XamlX.IL.Emitters; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers; +#nullable enable + +internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer +{ + public IXamlAstNode Transform(AstGroupTransformationContext context, IXamlAstNode node) + { + var resourceDictionaryType = context.GetAvaloniaTypes().ResourceDictionary; + if (node is not XamlObjectInitializationNode resourceDictionaryNode + || resourceDictionaryNode.Type != resourceDictionaryType + || resourceDictionaryNode.Manipulation is not XamlManipulationGroupNode resourceDictionaryManipulation) + { + return node; + } + + var mergeResourceIncludeType = context.GetAvaloniaTypes().MergeResourceInclude; + var mergeSourceNodes = new List(); + var hasAnyNonMergedResource = false; + foreach (var manipulationNode in resourceDictionaryManipulation.Children.ToArray()) + { + void ProcessXamlPropertyAssignmentNode(XamlManipulationGroupNode parent, XamlPropertyAssignmentNode assignmentNode) + { + if (assignmentNode.Property.Name == "MergedDictionaries" + && assignmentNode.Values.FirstOrDefault() is XamlValueWithManipulationNode valueNode) + { + if (valueNode.Type.GetClrType() == mergeResourceIncludeType) + { + if (valueNode.Manipulation is XamlObjectInitializationNode objectInitialization + && objectInitialization.Manipulation is XamlPropertyAssignmentNode sourceAssignmentNode) + { + parent.Children.Remove(assignmentNode); + mergeSourceNodes.Add(sourceAssignmentNode); + } + else + { + throw new XamlDocumentParseException(context.CurrentDocument, + "Invalid MergeResourceInclude node found. Make sure that Source property is set.", + valueNode); + } + } + else + { + hasAnyNonMergedResource = true; + } + + if (hasAnyNonMergedResource && mergeSourceNodes.Any()) + { + throw new XamlDocumentParseException(context.CurrentDocument, + "Mix of MergeResourceInclude and other dictionaries inside of the ResourceDictionary.MergedDictionaries is not allowed", + valueNode); + } + } + } + + if (manipulationNode is XamlPropertyAssignmentNode singleValueAssignment) + { + ProcessXamlPropertyAssignmentNode(resourceDictionaryManipulation, singleValueAssignment); + } + else if (manipulationNode is XamlManipulationGroupNode groupNodeValues) + { + foreach (var groupNodeValue in groupNodeValues.Children.OfType().ToArray()) + { + ProcessXamlPropertyAssignmentNode(groupNodeValues, groupNodeValue); + } + } + } + + var manipulationGroup = new XamlManipulationGroupNode(node, new List()); + foreach (var sourceNode in mergeSourceNodes) + { + var (originalAssetPath, propertyNode) = + AvaloniaXamlIncludeTransformer.ResolveSourceFromXamlInclude(context, "MergeResourceInclude", sourceNode, true); + if (originalAssetPath is null) + { + return node; + } + + var targetDocument = context.Documents.FirstOrDefault(d => + string.Equals(d.Uri, originalAssetPath, StringComparison.InvariantCultureIgnoreCase)) + ?.XamlDocument.Root as XamlValueWithManipulationNode; + if (targetDocument is null) + { + return context.ParseError( + $"Node MergeResourceInclude is unable to resolve \"{originalAssetPath}\" path.", propertyNode, node); + } + + var singleRootObject = ((XamlManipulationGroupNode)targetDocument.Manipulation) + .Children.OfType().Single(); + if (singleRootObject.Type != resourceDictionaryType) + { + return context.ParseError( + $"MergeResourceInclude can only include another ResourceDictionary", propertyNode, node); + } + + manipulationGroup.Children.Add(singleRootObject.Manipulation); + } + + if (manipulationGroup.Children.Any()) + { + // MergedDictionaries are read first, so we need ot inject our merged values in the beginning. + resourceDictionaryManipulation.Children.Insert(0, manipulationGroup); + } + + return node; + } +} 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/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 87a037c16a..5753a1008c 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -104,6 +104,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType IStyle { get; } public IXamlType StyleInclude { get; } public IXamlType ResourceInclude { get; } + public IXamlType MergeResourceInclude { get; } public IXamlType IResourceDictionary { get; } public IXamlType ResourceDictionary { get; } public IXamlMethod ResourceDictionaryDeferredAdd { get; } @@ -236,6 +237,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers IStyle = cfg.TypeSystem.GetType("Avalonia.Styling.IStyle"); StyleInclude = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Styling.StyleInclude"); ResourceInclude = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Styling.ResourceInclude"); + MergeResourceInclude = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Styling.MergeResourceInclude"); IResourceDictionary = cfg.TypeSystem.GetType("Avalonia.Controls.IResourceDictionary"); ResourceDictionary = cfg.TypeSystem.GetType("Avalonia.Controls.ResourceDictionary"); ResourceDictionaryDeferredAdd = ResourceDictionary.FindMethod("AddDeferred", XamlIlTypes.Void, true, XamlIlTypes.Object, diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentParseException.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentParseException.cs index d031a6086b..0532287a67 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentParseException.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentParseException.cs @@ -18,4 +18,9 @@ internal class XamlDocumentParseException : XamlParseException { FilePath = path; } + + public XamlDocumentParseException(IXamlDocumentResource document, string message, IXamlLineInfo lineInfo) + : this(document.FileSource?.FilePath, message, lineInfo) + { + } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index c59542c0f3..da739c754c 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -43,6 +43,7 @@ + @@ -65,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/Styling/MergeResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs new file mode 100644 index 0000000000..d231a1d221 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs @@ -0,0 +1,26 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Controls; + +#nullable enable + +namespace Avalonia.Markup.Xaml.Styling; + +/// +/// Loads a resource dictionary from a specified URL. +/// +/// +/// If used from the XAML code, it is merged into the parent dictionary in the compile time. +/// When used in runtime, this type behaves like . +/// +[RequiresUnreferencedCode(TrimmingMessages.StyleResourceIncludeRequiresUnreferenceCodeMessage)] +public class MergeResourceInclude : ResourceInclude +{ + public MergeResourceInclude(Uri? baseUri) : base(baseUri) + { + } + + public MergeResourceInclude(IServiceProvider serviceProvider) : base(serviceProvider) + { + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs index 3d8a61364a..595b37f7d1 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs @@ -9,6 +9,10 @@ namespace Avalonia.Markup.Xaml.Styling /// /// Loads a resource dictionary from a specified URL. /// + /// + /// If used from the XAML code, it is replaced with direct resource dictionary reference. + /// When used in runtime, this type might be unsafe with trimming and AOT. + /// [RequiresUnreferencedCode(TrimmingMessages.StyleResourceIncludeRequiresUnreferenceCodeMessage)] public class ResourceInclude : IResourceProvider { diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index bdde6e725c..b87aa64297 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -11,6 +11,10 @@ namespace Avalonia.Markup.Xaml.Styling /// /// Includes a style from a URL. /// + /// + /// If used from the XAML code, it is replaced with direct style reference. + /// When used in runtime, this type might be unsafe with trimming and AOT. + /// [RequiresUnreferencedCode(TrimmingMessages.StyleResourceIncludeRequiresUnreferenceCodeMessage)] public class StyleInclude : IStyle, IResourceProvider { 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 9dbfd0f8d9..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; @@ -61,6 +61,8 @@ namespace Avalonia.Skia return new DrawingContextImpl(createInfo, _preFramebufferCopyHandler, canvas, framebuffer); } + public bool IsCorrupted => false; + /// /// Check if two images info are compatible. /// 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..484fd9f219 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -1,8 +1,8 @@ using System; using Avalonia.Metadata; using Avalonia.Platform; -using JetBrains.Annotations; using SkiaSharp; +#nullable enable namespace Avalonia.Skia { @@ -10,7 +10,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)); } diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs index 4a7d4d4ac8..b064445a0b 100644 --- a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using Avalonia.OpenGL; using Avalonia.OpenGL.Imaging; +using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia @@ -8,7 +10,7 @@ namespace Avalonia.Skia /// /// Custom Skia gpu instance. /// - public interface ISkiaGpu + public interface ISkiaGpu : IPlatformGraphicsContext { /// /// Attempts to create custom render target from given surfaces. diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs index 5cc338a469..e19379df09 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs @@ -1,11 +1,13 @@ using System; using Avalonia.OpenGL; +using Avalonia.Platform; using SkiaSharp; using static Avalonia.OpenGL.GlConsts; namespace Avalonia.Skia { - public class FboSkiaSurface : ISkiaSurface + internal class FboSkiaSurface : ISkiaSurface { + private readonly GlSkiaGpu _gpu; private readonly GRContext _grContext; private readonly IGlContext _glContext; private readonly PixelSize _pixelSize; @@ -14,8 +16,9 @@ namespace Avalonia.Skia private int _texture; private static readonly bool[] TrueFalse = new[] { true, false }; - public FboSkiaSurface(GRContext grContext, IGlContext glContext, PixelSize pixelSize, GRSurfaceOrigin surfaceOrigin) + public FboSkiaSurface(GlSkiaGpu gpu, GRContext grContext, IGlContext glContext, PixelSize pixelSize, GRSurfaceOrigin surfaceOrigin) { + _gpu = gpu; _grContext = grContext; _glContext = glContext; _pixelSize = pixelSize; @@ -93,19 +96,33 @@ namespace Avalonia.Skia public void Dispose() { - using (_glContext.EnsureCurrent()) + try { - Surface?.Dispose(); - Surface = null; - var gl = _glContext.GlInterface; - if (_fbo != 0) + using (_glContext.EnsureCurrent()) { - gl.DeleteFramebuffer(_fbo); - gl.DeleteTexture(_texture); - gl.DeleteRenderbuffer(_depthStencil); - _fbo = _texture = _depthStencil = 0; + Surface?.Dispose(); + Surface = null; + var gl = _glContext.GlInterface; + if (_fbo != 0) + { + gl.DeleteFramebuffer(_fbo); + gl.DeleteTexture(_texture); + gl.DeleteRenderbuffer(_depthStencil); + } } } + catch (PlatformGraphicsContextLostException) + { + if (Surface != null) + // We need to dispose SKSurface _after_ GRContext.Abandon was called, + // otherwise it will try to do OpenGL calls without a proper context + _gpu.AddPostDispose(Surface.Dispose); + Surface = null; + } + finally + { + _fbo = _texture = _depthStencil = 0; + } } public SKSurface Surface { get; private set; } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index 476ecc4a39..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; @@ -14,10 +14,11 @@ namespace Avalonia.Skia private readonly GRContext _grContext; private IGlPlatformSurfaceRenderTarget _surface; - public GlRenderTarget(GRContext grContext, IGlPlatformSurface glSurface) + public GlRenderTarget(GRContext grContext, IGlContext glContext, IGlPlatformSurface glSurface) { _grContext = grContext; - _surface = glSurface.CreateGlRenderTarget(); + using (glContext.EnsureCurrent()) + _surface = glSurface.CreateGlRenderTarget(glContext); } public void Dispose() => _surface.Dispose(); diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs index 08e0fbd808..cdba3b9ea2 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs @@ -5,21 +5,22 @@ using Avalonia.Logging; using Avalonia.OpenGL; using Avalonia.OpenGL.Imaging; using Avalonia.OpenGL.Surfaces; +using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia { - class GlSkiaGpu : IOpenGlAwareSkiaGpu + class GlSkiaGpu : IOpenGlAwareSkiaGpu, IOpenGlTextureSharingRenderInterfaceContextFeature { private GRContext _grContext; private IGlContext _glContext; + private List _postDisposeCallbacks = new(); private bool? _canCreateSurfaces; - public GlSkiaGpu(IPlatformOpenGlInterface openGl, long? maxResourceBytes) + public GlSkiaGpu(IGlContext context, long? maxResourceBytes) { - var context = openGl.PrimaryContext; _glContext = context; - using (context.MakeCurrent()) + using (_glContext.EnsureCurrent()) { using (var iface = context.Version.Type == GlProfileType.OpenGL ? GRGlInterface.CreateOpenGl(proc => context.GlInterface.GetProcAddress(proc)) : @@ -34,13 +35,34 @@ namespace Avalonia.Skia } } + class SurfaceWrapper : IGlPlatformSurface + { + private readonly object _surface; + + public SurfaceWrapper( object surface) + { + _surface = surface; + } + + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) + { + var feature = context.TryGetFeature()!; + return feature.CreateRenderTarget(context, _surface); + } + } + public ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable surfaces) { + var customRenderTargetFactory = _glContext.TryGetFeature(); foreach (var surface in surfaces) { + if (customRenderTargetFactory?.CanRenderToSurface(_glContext, surface) == true) + { + return new GlRenderTarget(_grContext, _glContext, new SurfaceWrapper(surface)); + } if (surface is IGlPlatformSurface glSurface) { - return new GlRenderTarget(_grContext, glSurface); + return new GlRenderTarget(_grContext, _glContext, glSurface); } } @@ -62,7 +84,8 @@ namespace Avalonia.Skia return null; try { - var surface = new FboSkiaSurface(_grContext, _glContext, size, session?.SurfaceOrigin ?? GRSurfaceOrigin.TopLeft); + var surface = new FboSkiaSurface(this, _grContext, _glContext, size, + session?.SurfaceOrigin ?? GRSurfaceOrigin.TopLeft); _canCreateSurfaces = true; return surface; } @@ -75,6 +98,40 @@ namespace Avalonia.Skia } } + public bool CanCreateSharedContext => _glContext.CanCreateSharedContext; + + public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) => + _glContext.CreateSharedContext(preferredVersions); + public IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi) => new GlOpenGlBitmapImpl(_glContext, size, dpi); + + public void Dispose() + { + if (_glContext.IsLost) + _grContext.AbandonContext(); + else + _grContext.AbandonContext(true); + _grContext.Dispose(); + + lock(_postDisposeCallbacks) + foreach (var cb in _postDisposeCallbacks) + cb(); + } + + public bool IsLost => _glContext.IsLost; + public IDisposable EnsureCurrent() => _glContext.EnsureCurrent(); + + public object TryGetFeature(Type featureType) + { + if (featureType == typeof(IOpenGlTextureSharingRenderInterfaceContextFeature)) + return this; + return null; + } + + public void AddPostDispose(Action dispose) + { + lock (_postDisposeCallbacks) + _postDisposeCallbacks.Add(dispose); + } } } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs index 5e02500bf0..d8bff7cfc8 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs @@ -109,7 +109,9 @@ namespace Avalonia.Skia using (_context.EnsureCurrent()) { var glVersion = _context.Version; - InternalFormat = glVersion.Type == GlProfileType.OpenGLES ? GL_RGBA : GL_RGBA8; + InternalFormat = glVersion.Type == GlProfileType.OpenGLES && glVersion.Major == 2 + ? GL_RGBA + : GL_RGBA8; _context.GlInterface.GetIntegerv(GL_FRAMEBUFFER_BINDING, out _fbo); if (_fbo == 0) @@ -145,7 +147,7 @@ namespace Avalonia.Skia public void Present() { - using (_context.MakeCurrent()) + using (_context.EnsureCurrent()) { if (_disposed) throw new ObjectDisposedException(nameof(SharedOpenGlBitmapAttachment)); diff --git a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs index 6626546c0c..6b4a7a3409 100644 --- a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs @@ -6,7 +6,7 @@ namespace Avalonia.Skia /// /// Adapts to be used within our rendering pipeline. /// - internal class SkiaGpuRenderTarget : IRenderTargetWithCorruptionInfo + internal class SkiaGpuRenderTarget : IRenderTarget { private readonly ISkiaGpu _skiaGpu; private readonly ISkiaGpuRenderTarget _renderTarget; 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/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index f34e25299c..b202b60cdf 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -18,26 +18,28 @@ namespace Avalonia.Skia /// /// Skia platform render interface. /// - internal class PlatformRenderInterface : IPlatformRenderInterface, IOpenGlAwarePlatformRenderInterface + internal class PlatformRenderInterface : IPlatformRenderInterface { - private readonly ISkiaGpu _skiaGpu; + private readonly long? _maxResourceBytes; - public PlatformRenderInterface(ISkiaGpu skiaGpu, long? maxResourceBytes = null) + public PlatformRenderInterface(long? maxResourceBytes = null) { + _maxResourceBytes = maxResourceBytes; DefaultPixelFormat = SKImageInfo.PlatformColorType.ToPixelFormat(); + } - if (skiaGpu != null) - { - _skiaGpu = skiaGpu; - return; - } - var gl = AvaloniaLocator.Current.GetService(); - if (gl != null) - _skiaGpu = new GlSkiaGpu(gl, maxResourceBytes); + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) + { + if (graphicsContext == null) + return new SkiaContext(null); + if (graphicsContext is ISkiaGpu skiaGpu) + return new SkiaContext(skiaGpu); + if (graphicsContext is IGlContext gl) + return new SkiaContext(new GlSkiaGpu(gl, _maxResourceBytes)); + throw new ArgumentException("Graphics context of type is not supported"); } - public bool SupportsIndividualRoundRects => true; public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul; @@ -205,43 +207,12 @@ namespace Avalonia.Skia return new SurfaceRenderTarget(createInfo); } - /// - public IRenderTarget CreateRenderTarget(IEnumerable surfaces) - { - if (!(surfaces is IList)) - surfaces = surfaces.ToList(); - var gpuRenderTarget = _skiaGpu?.TryCreateRenderTarget(surfaces); - if (gpuRenderTarget != null) - { - return new SkiaGpuRenderTarget(_skiaGpu, gpuRenderTarget); - } - - foreach (var surface in surfaces) - { - if (surface is IFramebufferPlatformSurface framebufferSurface) - return new FramebufferRenderTarget(framebufferSurface); - } - - throw new NotSupportedException( - "Don't know how to create a Skia render target from any of provided surfaces"); - } - /// public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat format, AlphaFormat alphaFormat) { return new WriteableBitmapImpl(size, dpi, format, alphaFormat); } - public IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi) - { - if (_skiaGpu is IOpenGlAwareSkiaGpu glAware) - return glAware.CreateOpenGlBitmap(size, dpi); - if (_skiaGpu == null) - throw new PlatformNotSupportedException("GPU acceleration is not available"); - throw new PlatformNotSupportedException( - "Current GPU acceleration backend does not support OpenGL integration"); - } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) { 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/SkiaBackendContext.cs b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs new file mode 100644 index 0000000000..4949f4a50d --- /dev/null +++ b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Platform; + +namespace Avalonia.Skia; + +internal class SkiaContext : IPlatformRenderInterfaceContext +{ + private ISkiaGpu _gpu; + + public SkiaContext(ISkiaGpu gpu) + { + _gpu = gpu; + } + + public void Dispose() + { + _gpu?.Dispose(); + _gpu = null; + } + + /// + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) + { + if (!(surfaces is IList)) + surfaces = surfaces.ToList(); + var gpuRenderTarget = _gpu?.TryCreateRenderTarget(surfaces); + if (gpuRenderTarget != null) + { + return new SkiaGpuRenderTarget(_gpu, gpuRenderTarget); + } + + foreach (var surface in surfaces) + { + if (surface is IFramebufferPlatformSurface framebufferSurface) + return new FramebufferRenderTarget(framebufferSurface); + } + + throw new NotSupportedException( + "Don't know how to create a Skia render target from any of provided surfaces"); + } + + public object TryGetFeature(Type featureType) => _gpu?.TryGetFeature(featureType); +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/SkiaOptions.cs b/src/Skia/Avalonia.Skia/SkiaOptions.cs index 493263677d..b3c3056a58 100644 --- a/src/Skia/Avalonia.Skia/SkiaOptions.cs +++ b/src/Skia/Avalonia.Skia/SkiaOptions.cs @@ -8,11 +8,6 @@ namespace Avalonia /// public class SkiaOptions { - /// - /// Custom gpu factory to use. Can be used to customize behavior of Skia renderer. - /// - public Func CustomGpuFactory { get; set; } - /// /// The maximum number of bytes for video memory to store textures and resources. /// diff --git a/src/Skia/Avalonia.Skia/SkiaPlatform.cs b/src/Skia/Avalonia.Skia/SkiaPlatform.cs index 9a5725e06f..27f2631db8 100644 --- a/src/Skia/Avalonia.Skia/SkiaPlatform.cs +++ b/src/Skia/Avalonia.Skia/SkiaPlatform.cs @@ -17,8 +17,7 @@ namespace Avalonia.Skia public static void Initialize(SkiaOptions options) { - var customGpu = options.CustomGpuFactory?.Invoke(); - var renderInterface = new PlatformRenderInterface(customGpu, options.MaxGpuResourceSizeBytes); + var renderInterface = new PlatformRenderInterface(options.MaxGpuResourceSizeBytes); AvaloniaLocator.CurrentMutable .Bind().ToConstant(renderInterface) 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 74e3ebc51d..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; @@ -107,6 +107,8 @@ namespace Avalonia.Skia return new DrawingContextImpl(createInfo, Disposable.Create(() => Version++)); } + public bool IsCorrupted => _gpu?.IsLost == true; + /// public Vector Dpi { get; } diff --git a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs index d437f514bb..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(); @@ -217,8 +217,7 @@ namespace Avalonia.Skia public int RowBytes => _bitmap.RowBytes; /// - public Vector Dpi { get; } = SkiaPlatform.DefaultDpi; - + public Vector Dpi => _parent.Dpi; /// public PixelFormat Format => _bitmap.ColorType.ToPixelFormat(); } 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 4d307c9762..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; @@ -113,7 +113,7 @@ namespace Avalonia.Direct2D1 SharpDX.Configuration.EnableReleaseOnFinalizer = true; } - public IRenderTarget CreateRenderTarget(IEnumerable surfaces) + private IRenderTarget CreateRenderTarget(IEnumerable surfaces) { foreach (var s in surfaces) { @@ -223,6 +223,26 @@ namespace Avalonia.Direct2D1 return new GlyphRunImpl(run); } + class D2DApi : IPlatformRenderInterfaceContext + { + private readonly Direct2D1Platform _platform; + + public D2DApi(Direct2D1Platform platform) + { + _platform = platform; + } + public object TryGetFeature(Type featureType) => null; + + public void Dispose() + { + } + + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => _platform.CreateRenderTarget(surfaces); + } + + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => + new D2DApi(this); + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { if (glyphRun.GlyphTypeface is not GlyphTypefaceImpl glyphTypeface) diff --git a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs index 2c0adcac32..02932c52da 100644 --- a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs @@ -38,6 +38,8 @@ namespace Avalonia.Direct2D1 }); } + public bool IsCorrupted => false; + public IDrawingContextLayerImpl CreateLayer(Size size) { var renderTarget = _externalRenderTargetProvider.GetOrCreateRenderTarget(); diff --git a/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs index f7e7ed3dc2..984a24fb30 100644 --- a/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs @@ -35,6 +35,8 @@ namespace Avalonia.Direct2D1 .CreateDrawingContext(visualBrushRenderer); } + public bool IsCorrupted => false; + class FramebufferShim : WicRenderTargetBitmapImpl { private readonly ILockedFramebuffer _target; 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/Imaging/D2DRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs index c5f8e837ce..84f11acdd7 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs @@ -37,6 +37,8 @@ namespace Avalonia.Direct2D1.Media.Imaging return new DrawingContextImpl(visualBrushRenderer, this, _renderTarget, null, () => Version++); } + public bool IsCorrupted => false; + public void Blit(IDrawingContextImpl context) => throw new NotSupportedException(); public bool CanBlit => false; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs index 8c9d01f37d..9f0d48dbc7 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs @@ -39,6 +39,8 @@ namespace Avalonia.Direct2D1.Media public virtual IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) => CreateDrawingContext(visualBrushRenderer, null); + public bool IsCorrupted => false; + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer, Action finishedCallback) { return new DrawingContextImpl(visualBrushRenderer, null, _renderTarget, finishedCallback: () => diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs index 885be132a4..2e40bdd9d1 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs @@ -52,7 +52,7 @@ namespace Avalonia.Direct2D1.Media.Imaging public IntPtr Address => _lock.Data.DataPointer; public PixelSize Size => _lock.Size.ToAvalonia(); public int RowBytes => _lock.Stride; - public Vector Dpi { get; } = new Vector(96, 96); + public Vector Dpi => _parent.Dpi; public PixelFormat Format => _format; } diff --git a/src/Windows/Avalonia.Direct2D1/OptionalDispose.cs b/src/Windows/Avalonia.Direct2D1/OptionalDispose.cs index e302e71102..1cdf7661df 100644 --- a/src/Windows/Avalonia.Direct2D1/OptionalDispose.cs +++ b/src/Windows/Avalonia.Direct2D1/OptionalDispose.cs @@ -2,7 +2,7 @@ namespace Avalonia.Direct2D1 { - public readonly struct OptionalDispose : IDisposable where T : IDisposable + public readonly record struct OptionalDispose : IDisposable where T : IDisposable { private readonly bool _dispose; diff --git a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs index d04c616bd9..1a749c1a7f 100644 --- a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs @@ -30,6 +30,8 @@ namespace Avalonia.Direct2D1 return new DrawingContextImpl(visualBrushRenderer, this, _renderTarget); } + public bool IsCorrupted => false; + public IDrawingContextLayerImpl CreateLayer(Size size) { return D2DRenderTargetBitmapImpl.CreateCompatible(_renderTarget, size); diff --git a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs index f319cfae03..4935e3db48 100644 --- a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs @@ -35,6 +35,8 @@ namespace Avalonia.Direct2D1 return new DrawingContextImpl(visualBrushRenderer, this, _deviceContext, _swapChain); } + public bool IsCorrupted => false; + public IDrawingContextLayerImpl CreateLayer(Size size) { if (_deviceContext == 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.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index 1dcf45aa4e..16a20c855f 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -87,9 +87,11 @@ namespace Avalonia.Win32.Interop.Wpf _ttl.ScalingChanged?.Invoke(_ttl.RenderScaling); } + public IRenderer CreateRenderer(IRenderRoot root) { - return new ImmediateRenderer((Visual)root); + var mgr = new PlatformRenderInterfaceContextManager(null); + return new ImmediateRenderer((Visual)root, () => mgr.CreateRenderTarget(_surfaces), mgr); } public void Dispose() diff --git a/src/Avalonia.OpenGL/AngleOptions.cs b/src/Windows/Avalonia.Win32/AngleOptions.cs similarity index 90% rename from src/Avalonia.OpenGL/AngleOptions.cs rename to src/Windows/Avalonia.Win32/AngleOptions.cs index 0807eb7ab4..076ddd2a5d 100644 --- a/src/Avalonia.OpenGL/AngleOptions.cs +++ b/src/Windows/Avalonia.Win32/AngleOptions.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; +using Avalonia.OpenGL; -namespace Avalonia.OpenGL +namespace Avalonia.Win32 { public class AngleOptions { diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index e69e0ee219..eb51b7fd07 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -16,11 +16,13 @@ + + + + - - $(NoWarn);CA1416 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/DirectX/DirectXEnums.cs b/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs index 3a67da530f..a749ed1b45 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Avalonia.Win32.DxgiSwapchain +namespace Avalonia.Win32.DirectX { internal enum D3D_FEATURE_LEVEL { @@ -136,4 +136,53 @@ namespace Avalonia.Win32.DxgiSwapchain DXGI_ALPHA_MODE_FORCE_DWORD = (unchecked((int)0xffffffff)), } + + internal enum D3D_DRIVER_TYPE + { + D3D_DRIVER_TYPE_UNKNOWN = 0, + D3D_DRIVER_TYPE_HARDWARE, + D3D_DRIVER_TYPE_REFERENCE, + D3D_DRIVER_TYPE_NULL, + D3D_DRIVER_TYPE_SOFTWARE, + D3D_DRIVER_TYPE_WARP + } + + internal enum DXGI_ERROR : uint + { + DXGI_ERROR_ACCESS_DENIED = 0x887A002B, + DXGI_ERROR_ACCESS_LOST = 0x887A0026, + DXGI_ERROR_ALREADY_EXISTS = 0x887A0036, + DXGI_ERROR_CANNOT_PROTECT_CONTENT = 0x887A002A, + DXGI_ERROR_DEVICE_HUNG = 0x887A0006, + DXGI_ERROR_DEVICE_REMOVED = 0x887A0005, + DXGI_ERROR_DEVICE_RESET = 0x887A0007, + DXGI_ERROR_DRIVER_INTERNAL_ERROR = 0x887A0020, + DXGI_ERROR_FRAME_STATISTICS_DISJOINT = 0x887A000B, + DXGI_ERROR_GRAPHICS_VIDPN_SOURCE_IN_USE = 0x887A000C, + DXGI_ERROR_INVALID_CALL = 0x887A0001, + DXGI_ERROR_MORE_DATA = 0x887A0003, + DXGI_ERROR_NAME_ALREADY_EXISTS = 0x887A002C, + DXGI_ERROR_NONEXCLUSIVE = 0x887A0021, + DXGI_ERROR_NOT_CURRENTLY_AVAILABLE = 0x887A0022, + DXGI_ERROR_NOT_FOUND = 0x887A0002, + DXGI_ERROR_REMOTE_CLIENT_DISCONNECTED = 0x887A0023, + DXGI_ERROR_REMOTE_OUTOFMEMORY = 0x887A0024, + DXGI_ERROR_RESTRICT_TO_OUTPUT_STALE = 0x887A0029, + DXGI_ERROR_SDK_COMPONENT_MISSING = 0x887A002D, + DXGI_ERROR_SESSION_DISCONNECTED = 0x887A0028, + DXGI_ERROR_UNSUPPORTED = 0x887A0004, + DXGI_ERROR_WAIT_TIMEOUT = 0x887A0027, + DXGI_ERROR_WAS_STILL_DRAWING = 0x887A000A + } + + internal static class DxgiErrorExtensions + { + public static bool IsDeviceLostError(this DXGI_ERROR error) + { + return error is DXGI_ERROR.DXGI_ERROR_DEVICE_REMOVED + or DXGI_ERROR.DXGI_ERROR_DEVICE_HUNG + or DXGI_ERROR.DXGI_ERROR_DEVICE_RESET + or DXGI_ERROR.DXGI_ERROR_NOT_CURRENTLY_AVAILABLE; + } + } } diff --git a/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs b/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs index d606b00109..47451831a6 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs @@ -6,8 +6,10 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using static Avalonia.Win32.Interop.UnmanagedMethods; +// ReSharper disable InconsistentNaming +#pragma warning disable CS0649 -namespace Avalonia.Win32.DxgiSwapchain +namespace Avalonia.Win32.DirectX { #nullable enable public unsafe struct HANDLE @@ -36,1092 +38,13 @@ namespace Avalonia.Win32.DxgiSwapchain public override string ToString() => ((IntPtr)Value).ToString(); } - internal unsafe partial struct MONITORINFOEXW + internal unsafe struct MONITORINFOEXW { internal MONITORINFO Base; internal fixed ushort szDevice[32]; } - - internal unsafe struct DXGI_GAMMA_CONTROL - { - public DXGI_RGB Scale; - - public DXGI_RGB Offset; - - public _GammaCurve_e__FixedBuffer GammaCurve; - - public partial struct _GammaCurve_e__FixedBuffer - { - public DXGI_RGB e0; - public DXGI_RGB e1; - public DXGI_RGB e2; - public DXGI_RGB e3; - public DXGI_RGB e4; - public DXGI_RGB e5; - public DXGI_RGB e6; - public DXGI_RGB e7; - public DXGI_RGB e8; - public DXGI_RGB e9; - public DXGI_RGB e10; - public DXGI_RGB e11; - public DXGI_RGB e12; - public DXGI_RGB e13; - public DXGI_RGB e14; - public DXGI_RGB e15; - public DXGI_RGB e16; - public DXGI_RGB e17; - public DXGI_RGB e18; - public DXGI_RGB e19; - public DXGI_RGB e20; - public DXGI_RGB e21; - public DXGI_RGB e22; - public DXGI_RGB e23; - public DXGI_RGB e24; - public DXGI_RGB e25; - public DXGI_RGB e26; - public DXGI_RGB e27; - public DXGI_RGB e28; - public DXGI_RGB e29; - public DXGI_RGB e30; - public DXGI_RGB e31; - public DXGI_RGB e32; - public DXGI_RGB e33; - public DXGI_RGB e34; - public DXGI_RGB e35; - public DXGI_RGB e36; - public DXGI_RGB e37; - public DXGI_RGB e38; - public DXGI_RGB e39; - public DXGI_RGB e40; - public DXGI_RGB e41; - public DXGI_RGB e42; - public DXGI_RGB e43; - public DXGI_RGB e44; - public DXGI_RGB e45; - public DXGI_RGB e46; - public DXGI_RGB e47; - public DXGI_RGB e48; - public DXGI_RGB e49; - public DXGI_RGB e50; - public DXGI_RGB e51; - public DXGI_RGB e52; - public DXGI_RGB e53; - public DXGI_RGB e54; - public DXGI_RGB e55; - public DXGI_RGB e56; - public DXGI_RGB e57; - public DXGI_RGB e58; - public DXGI_RGB e59; - public DXGI_RGB e60; - public DXGI_RGB e61; - public DXGI_RGB e62; - public DXGI_RGB e63; - public DXGI_RGB e64; - public DXGI_RGB e65; - public DXGI_RGB e66; - public DXGI_RGB e67; - public DXGI_RGB e68; - public DXGI_RGB e69; - public DXGI_RGB e70; - public DXGI_RGB e71; - public DXGI_RGB e72; - public DXGI_RGB e73; - public DXGI_RGB e74; - public DXGI_RGB e75; - public DXGI_RGB e76; - public DXGI_RGB e77; - public DXGI_RGB e78; - public DXGI_RGB e79; - public DXGI_RGB e80; - public DXGI_RGB e81; - public DXGI_RGB e82; - public DXGI_RGB e83; - public DXGI_RGB e84; - public DXGI_RGB e85; - public DXGI_RGB e86; - public DXGI_RGB e87; - public DXGI_RGB e88; - public DXGI_RGB e89; - public DXGI_RGB e90; - public DXGI_RGB e91; - public DXGI_RGB e92; - public DXGI_RGB e93; - public DXGI_RGB e94; - public DXGI_RGB e95; - public DXGI_RGB e96; - public DXGI_RGB e97; - public DXGI_RGB e98; - public DXGI_RGB e99; - public DXGI_RGB e100; - public DXGI_RGB e101; - public DXGI_RGB e102; - public DXGI_RGB e103; - public DXGI_RGB e104; - public DXGI_RGB e105; - public DXGI_RGB e106; - public DXGI_RGB e107; - public DXGI_RGB e108; - public DXGI_RGB e109; - public DXGI_RGB e110; - public DXGI_RGB e111; - public DXGI_RGB e112; - public DXGI_RGB e113; - public DXGI_RGB e114; - public DXGI_RGB e115; - public DXGI_RGB e116; - public DXGI_RGB e117; - public DXGI_RGB e118; - public DXGI_RGB e119; - public DXGI_RGB e120; - public DXGI_RGB e121; - public DXGI_RGB e122; - public DXGI_RGB e123; - public DXGI_RGB e124; - public DXGI_RGB e125; - public DXGI_RGB e126; - public DXGI_RGB e127; - public DXGI_RGB e128; - public DXGI_RGB e129; - public DXGI_RGB e130; - public DXGI_RGB e131; - public DXGI_RGB e132; - public DXGI_RGB e133; - public DXGI_RGB e134; - public DXGI_RGB e135; - public DXGI_RGB e136; - public DXGI_RGB e137; - public DXGI_RGB e138; - public DXGI_RGB e139; - public DXGI_RGB e140; - public DXGI_RGB e141; - public DXGI_RGB e142; - public DXGI_RGB e143; - public DXGI_RGB e144; - public DXGI_RGB e145; - public DXGI_RGB e146; - public DXGI_RGB e147; - public DXGI_RGB e148; - public DXGI_RGB e149; - public DXGI_RGB e150; - public DXGI_RGB e151; - public DXGI_RGB e152; - public DXGI_RGB e153; - public DXGI_RGB e154; - public DXGI_RGB e155; - public DXGI_RGB e156; - public DXGI_RGB e157; - public DXGI_RGB e158; - public DXGI_RGB e159; - public DXGI_RGB e160; - public DXGI_RGB e161; - public DXGI_RGB e162; - public DXGI_RGB e163; - public DXGI_RGB e164; - public DXGI_RGB e165; - public DXGI_RGB e166; - public DXGI_RGB e167; - public DXGI_RGB e168; - public DXGI_RGB e169; - public DXGI_RGB e170; - public DXGI_RGB e171; - public DXGI_RGB e172; - public DXGI_RGB e173; - public DXGI_RGB e174; - public DXGI_RGB e175; - public DXGI_RGB e176; - public DXGI_RGB e177; - public DXGI_RGB e178; - public DXGI_RGB e179; - public DXGI_RGB e180; - public DXGI_RGB e181; - public DXGI_RGB e182; - public DXGI_RGB e183; - public DXGI_RGB e184; - public DXGI_RGB e185; - public DXGI_RGB e186; - public DXGI_RGB e187; - public DXGI_RGB e188; - public DXGI_RGB e189; - public DXGI_RGB e190; - public DXGI_RGB e191; - public DXGI_RGB e192; - public DXGI_RGB e193; - public DXGI_RGB e194; - public DXGI_RGB e195; - public DXGI_RGB e196; - public DXGI_RGB e197; - public DXGI_RGB e198; - public DXGI_RGB e199; - public DXGI_RGB e200; - public DXGI_RGB e201; - public DXGI_RGB e202; - public DXGI_RGB e203; - public DXGI_RGB e204; - public DXGI_RGB e205; - public DXGI_RGB e206; - public DXGI_RGB e207; - public DXGI_RGB e208; - public DXGI_RGB e209; - public DXGI_RGB e210; - public DXGI_RGB e211; - public DXGI_RGB e212; - public DXGI_RGB e213; - public DXGI_RGB e214; - public DXGI_RGB e215; - public DXGI_RGB e216; - public DXGI_RGB e217; - public DXGI_RGB e218; - public DXGI_RGB e219; - public DXGI_RGB e220; - public DXGI_RGB e221; - public DXGI_RGB e222; - public DXGI_RGB e223; - public DXGI_RGB e224; - public DXGI_RGB e225; - public DXGI_RGB e226; - public DXGI_RGB e227; - public DXGI_RGB e228; - public DXGI_RGB e229; - public DXGI_RGB e230; - public DXGI_RGB e231; - public DXGI_RGB e232; - public DXGI_RGB e233; - public DXGI_RGB e234; - public DXGI_RGB e235; - public DXGI_RGB e236; - public DXGI_RGB e237; - public DXGI_RGB e238; - public DXGI_RGB e239; - public DXGI_RGB e240; - public DXGI_RGB e241; - public DXGI_RGB e242; - public DXGI_RGB e243; - public DXGI_RGB e244; - public DXGI_RGB e245; - public DXGI_RGB e246; - public DXGI_RGB e247; - public DXGI_RGB e248; - public DXGI_RGB e249; - public DXGI_RGB e250; - public DXGI_RGB e251; - public DXGI_RGB e252; - public DXGI_RGB e253; - public DXGI_RGB e254; - public DXGI_RGB e255; - public DXGI_RGB e256; - public DXGI_RGB e257; - public DXGI_RGB e258; - public DXGI_RGB e259; - public DXGI_RGB e260; - public DXGI_RGB e261; - public DXGI_RGB e262; - public DXGI_RGB e263; - public DXGI_RGB e264; - public DXGI_RGB e265; - public DXGI_RGB e266; - public DXGI_RGB e267; - public DXGI_RGB e268; - public DXGI_RGB e269; - public DXGI_RGB e270; - public DXGI_RGB e271; - public DXGI_RGB e272; - public DXGI_RGB e273; - public DXGI_RGB e274; - public DXGI_RGB e275; - public DXGI_RGB e276; - public DXGI_RGB e277; - public DXGI_RGB e278; - public DXGI_RGB e279; - public DXGI_RGB e280; - public DXGI_RGB e281; - public DXGI_RGB e282; - public DXGI_RGB e283; - public DXGI_RGB e284; - public DXGI_RGB e285; - public DXGI_RGB e286; - public DXGI_RGB e287; - public DXGI_RGB e288; - public DXGI_RGB e289; - public DXGI_RGB e290; - public DXGI_RGB e291; - public DXGI_RGB e292; - public DXGI_RGB e293; - public DXGI_RGB e294; - public DXGI_RGB e295; - public DXGI_RGB e296; - public DXGI_RGB e297; - public DXGI_RGB e298; - public DXGI_RGB e299; - public DXGI_RGB e300; - public DXGI_RGB e301; - public DXGI_RGB e302; - public DXGI_RGB e303; - public DXGI_RGB e304; - public DXGI_RGB e305; - public DXGI_RGB e306; - public DXGI_RGB e307; - public DXGI_RGB e308; - public DXGI_RGB e309; - public DXGI_RGB e310; - public DXGI_RGB e311; - public DXGI_RGB e312; - public DXGI_RGB e313; - public DXGI_RGB e314; - public DXGI_RGB e315; - public DXGI_RGB e316; - public DXGI_RGB e317; - public DXGI_RGB e318; - public DXGI_RGB e319; - public DXGI_RGB e320; - public DXGI_RGB e321; - public DXGI_RGB e322; - public DXGI_RGB e323; - public DXGI_RGB e324; - public DXGI_RGB e325; - public DXGI_RGB e326; - public DXGI_RGB e327; - public DXGI_RGB e328; - public DXGI_RGB e329; - public DXGI_RGB e330; - public DXGI_RGB e331; - public DXGI_RGB e332; - public DXGI_RGB e333; - public DXGI_RGB e334; - public DXGI_RGB e335; - public DXGI_RGB e336; - public DXGI_RGB e337; - public DXGI_RGB e338; - public DXGI_RGB e339; - public DXGI_RGB e340; - public DXGI_RGB e341; - public DXGI_RGB e342; - public DXGI_RGB e343; - public DXGI_RGB e344; - public DXGI_RGB e345; - public DXGI_RGB e346; - public DXGI_RGB e347; - public DXGI_RGB e348; - public DXGI_RGB e349; - public DXGI_RGB e350; - public DXGI_RGB e351; - public DXGI_RGB e352; - public DXGI_RGB e353; - public DXGI_RGB e354; - public DXGI_RGB e355; - public DXGI_RGB e356; - public DXGI_RGB e357; - public DXGI_RGB e358; - public DXGI_RGB e359; - public DXGI_RGB e360; - public DXGI_RGB e361; - public DXGI_RGB e362; - public DXGI_RGB e363; - public DXGI_RGB e364; - public DXGI_RGB e365; - public DXGI_RGB e366; - public DXGI_RGB e367; - public DXGI_RGB e368; - public DXGI_RGB e369; - public DXGI_RGB e370; - public DXGI_RGB e371; - public DXGI_RGB e372; - public DXGI_RGB e373; - public DXGI_RGB e374; - public DXGI_RGB e375; - public DXGI_RGB e376; - public DXGI_RGB e377; - public DXGI_RGB e378; - public DXGI_RGB e379; - public DXGI_RGB e380; - public DXGI_RGB e381; - public DXGI_RGB e382; - public DXGI_RGB e383; - public DXGI_RGB e384; - public DXGI_RGB e385; - public DXGI_RGB e386; - public DXGI_RGB e387; - public DXGI_RGB e388; - public DXGI_RGB e389; - public DXGI_RGB e390; - public DXGI_RGB e391; - public DXGI_RGB e392; - public DXGI_RGB e393; - public DXGI_RGB e394; - public DXGI_RGB e395; - public DXGI_RGB e396; - public DXGI_RGB e397; - public DXGI_RGB e398; - public DXGI_RGB e399; - public DXGI_RGB e400; - public DXGI_RGB e401; - public DXGI_RGB e402; - public DXGI_RGB e403; - public DXGI_RGB e404; - public DXGI_RGB e405; - public DXGI_RGB e406; - public DXGI_RGB e407; - public DXGI_RGB e408; - public DXGI_RGB e409; - public DXGI_RGB e410; - public DXGI_RGB e411; - public DXGI_RGB e412; - public DXGI_RGB e413; - public DXGI_RGB e414; - public DXGI_RGB e415; - public DXGI_RGB e416; - public DXGI_RGB e417; - public DXGI_RGB e418; - public DXGI_RGB e419; - public DXGI_RGB e420; - public DXGI_RGB e421; - public DXGI_RGB e422; - public DXGI_RGB e423; - public DXGI_RGB e424; - public DXGI_RGB e425; - public DXGI_RGB e426; - public DXGI_RGB e427; - public DXGI_RGB e428; - public DXGI_RGB e429; - public DXGI_RGB e430; - public DXGI_RGB e431; - public DXGI_RGB e432; - public DXGI_RGB e433; - public DXGI_RGB e434; - public DXGI_RGB e435; - public DXGI_RGB e436; - public DXGI_RGB e437; - public DXGI_RGB e438; - public DXGI_RGB e439; - public DXGI_RGB e440; - public DXGI_RGB e441; - public DXGI_RGB e442; - public DXGI_RGB e443; - public DXGI_RGB e444; - public DXGI_RGB e445; - public DXGI_RGB e446; - public DXGI_RGB e447; - public DXGI_RGB e448; - public DXGI_RGB e449; - public DXGI_RGB e450; - public DXGI_RGB e451; - public DXGI_RGB e452; - public DXGI_RGB e453; - public DXGI_RGB e454; - public DXGI_RGB e455; - public DXGI_RGB e456; - public DXGI_RGB e457; - public DXGI_RGB e458; - public DXGI_RGB e459; - public DXGI_RGB e460; - public DXGI_RGB e461; - public DXGI_RGB e462; - public DXGI_RGB e463; - public DXGI_RGB e464; - public DXGI_RGB e465; - public DXGI_RGB e466; - public DXGI_RGB e467; - public DXGI_RGB e468; - public DXGI_RGB e469; - public DXGI_RGB e470; - public DXGI_RGB e471; - public DXGI_RGB e472; - public DXGI_RGB e473; - public DXGI_RGB e474; - public DXGI_RGB e475; - public DXGI_RGB e476; - public DXGI_RGB e477; - public DXGI_RGB e478; - public DXGI_RGB e479; - public DXGI_RGB e480; - public DXGI_RGB e481; - public DXGI_RGB e482; - public DXGI_RGB e483; - public DXGI_RGB e484; - public DXGI_RGB e485; - public DXGI_RGB e486; - public DXGI_RGB e487; - public DXGI_RGB e488; - public DXGI_RGB e489; - public DXGI_RGB e490; - public DXGI_RGB e491; - public DXGI_RGB e492; - public DXGI_RGB e493; - public DXGI_RGB e494; - public DXGI_RGB e495; - public DXGI_RGB e496; - public DXGI_RGB e497; - public DXGI_RGB e498; - public DXGI_RGB e499; - public DXGI_RGB e500; - public DXGI_RGB e501; - public DXGI_RGB e502; - public DXGI_RGB e503; - public DXGI_RGB e504; - public DXGI_RGB e505; - public DXGI_RGB e506; - public DXGI_RGB e507; - public DXGI_RGB e508; - public DXGI_RGB e509; - public DXGI_RGB e510; - public DXGI_RGB e511; - public DXGI_RGB e512; - public DXGI_RGB e513; - public DXGI_RGB e514; - public DXGI_RGB e515; - public DXGI_RGB e516; - public DXGI_RGB e517; - public DXGI_RGB e518; - public DXGI_RGB e519; - public DXGI_RGB e520; - public DXGI_RGB e521; - public DXGI_RGB e522; - public DXGI_RGB e523; - public DXGI_RGB e524; - public DXGI_RGB e525; - public DXGI_RGB e526; - public DXGI_RGB e527; - public DXGI_RGB e528; - public DXGI_RGB e529; - public DXGI_RGB e530; - public DXGI_RGB e531; - public DXGI_RGB e532; - public DXGI_RGB e533; - public DXGI_RGB e534; - public DXGI_RGB e535; - public DXGI_RGB e536; - public DXGI_RGB e537; - public DXGI_RGB e538; - public DXGI_RGB e539; - public DXGI_RGB e540; - public DXGI_RGB e541; - public DXGI_RGB e542; - public DXGI_RGB e543; - public DXGI_RGB e544; - public DXGI_RGB e545; - public DXGI_RGB e546; - public DXGI_RGB e547; - public DXGI_RGB e548; - public DXGI_RGB e549; - public DXGI_RGB e550; - public DXGI_RGB e551; - public DXGI_RGB e552; - public DXGI_RGB e553; - public DXGI_RGB e554; - public DXGI_RGB e555; - public DXGI_RGB e556; - public DXGI_RGB e557; - public DXGI_RGB e558; - public DXGI_RGB e559; - public DXGI_RGB e560; - public DXGI_RGB e561; - public DXGI_RGB e562; - public DXGI_RGB e563; - public DXGI_RGB e564; - public DXGI_RGB e565; - public DXGI_RGB e566; - public DXGI_RGB e567; - public DXGI_RGB e568; - public DXGI_RGB e569; - public DXGI_RGB e570; - public DXGI_RGB e571; - public DXGI_RGB e572; - public DXGI_RGB e573; - public DXGI_RGB e574; - public DXGI_RGB e575; - public DXGI_RGB e576; - public DXGI_RGB e577; - public DXGI_RGB e578; - public DXGI_RGB e579; - public DXGI_RGB e580; - public DXGI_RGB e581; - public DXGI_RGB e582; - public DXGI_RGB e583; - public DXGI_RGB e584; - public DXGI_RGB e585; - public DXGI_RGB e586; - public DXGI_RGB e587; - public DXGI_RGB e588; - public DXGI_RGB e589; - public DXGI_RGB e590; - public DXGI_RGB e591; - public DXGI_RGB e592; - public DXGI_RGB e593; - public DXGI_RGB e594; - public DXGI_RGB e595; - public DXGI_RGB e596; - public DXGI_RGB e597; - public DXGI_RGB e598; - public DXGI_RGB e599; - public DXGI_RGB e600; - public DXGI_RGB e601; - public DXGI_RGB e602; - public DXGI_RGB e603; - public DXGI_RGB e604; - public DXGI_RGB e605; - public DXGI_RGB e606; - public DXGI_RGB e607; - public DXGI_RGB e608; - public DXGI_RGB e609; - public DXGI_RGB e610; - public DXGI_RGB e611; - public DXGI_RGB e612; - public DXGI_RGB e613; - public DXGI_RGB e614; - public DXGI_RGB e615; - public DXGI_RGB e616; - public DXGI_RGB e617; - public DXGI_RGB e618; - public DXGI_RGB e619; - public DXGI_RGB e620; - public DXGI_RGB e621; - public DXGI_RGB e622; - public DXGI_RGB e623; - public DXGI_RGB e624; - public DXGI_RGB e625; - public DXGI_RGB e626; - public DXGI_RGB e627; - public DXGI_RGB e628; - public DXGI_RGB e629; - public DXGI_RGB e630; - public DXGI_RGB e631; - public DXGI_RGB e632; - public DXGI_RGB e633; - public DXGI_RGB e634; - public DXGI_RGB e635; - public DXGI_RGB e636; - public DXGI_RGB e637; - public DXGI_RGB e638; - public DXGI_RGB e639; - public DXGI_RGB e640; - public DXGI_RGB e641; - public DXGI_RGB e642; - public DXGI_RGB e643; - public DXGI_RGB e644; - public DXGI_RGB e645; - public DXGI_RGB e646; - public DXGI_RGB e647; - public DXGI_RGB e648; - public DXGI_RGB e649; - public DXGI_RGB e650; - public DXGI_RGB e651; - public DXGI_RGB e652; - public DXGI_RGB e653; - public DXGI_RGB e654; - public DXGI_RGB e655; - public DXGI_RGB e656; - public DXGI_RGB e657; - public DXGI_RGB e658; - public DXGI_RGB e659; - public DXGI_RGB e660; - public DXGI_RGB e661; - public DXGI_RGB e662; - public DXGI_RGB e663; - public DXGI_RGB e664; - public DXGI_RGB e665; - public DXGI_RGB e666; - public DXGI_RGB e667; - public DXGI_RGB e668; - public DXGI_RGB e669; - public DXGI_RGB e670; - public DXGI_RGB e671; - public DXGI_RGB e672; - public DXGI_RGB e673; - public DXGI_RGB e674; - public DXGI_RGB e675; - public DXGI_RGB e676; - public DXGI_RGB e677; - public DXGI_RGB e678; - public DXGI_RGB e679; - public DXGI_RGB e680; - public DXGI_RGB e681; - public DXGI_RGB e682; - public DXGI_RGB e683; - public DXGI_RGB e684; - public DXGI_RGB e685; - public DXGI_RGB e686; - public DXGI_RGB e687; - public DXGI_RGB e688; - public DXGI_RGB e689; - public DXGI_RGB e690; - public DXGI_RGB e691; - public DXGI_RGB e692; - public DXGI_RGB e693; - public DXGI_RGB e694; - public DXGI_RGB e695; - public DXGI_RGB e696; - public DXGI_RGB e697; - public DXGI_RGB e698; - public DXGI_RGB e699; - public DXGI_RGB e700; - public DXGI_RGB e701; - public DXGI_RGB e702; - public DXGI_RGB e703; - public DXGI_RGB e704; - public DXGI_RGB e705; - public DXGI_RGB e706; - public DXGI_RGB e707; - public DXGI_RGB e708; - public DXGI_RGB e709; - public DXGI_RGB e710; - public DXGI_RGB e711; - public DXGI_RGB e712; - public DXGI_RGB e713; - public DXGI_RGB e714; - public DXGI_RGB e715; - public DXGI_RGB e716; - public DXGI_RGB e717; - public DXGI_RGB e718; - public DXGI_RGB e719; - public DXGI_RGB e720; - public DXGI_RGB e721; - public DXGI_RGB e722; - public DXGI_RGB e723; - public DXGI_RGB e724; - public DXGI_RGB e725; - public DXGI_RGB e726; - public DXGI_RGB e727; - public DXGI_RGB e728; - public DXGI_RGB e729; - public DXGI_RGB e730; - public DXGI_RGB e731; - public DXGI_RGB e732; - public DXGI_RGB e733; - public DXGI_RGB e734; - public DXGI_RGB e735; - public DXGI_RGB e736; - public DXGI_RGB e737; - public DXGI_RGB e738; - public DXGI_RGB e739; - public DXGI_RGB e740; - public DXGI_RGB e741; - public DXGI_RGB e742; - public DXGI_RGB e743; - public DXGI_RGB e744; - public DXGI_RGB e745; - public DXGI_RGB e746; - public DXGI_RGB e747; - public DXGI_RGB e748; - public DXGI_RGB e749; - public DXGI_RGB e750; - public DXGI_RGB e751; - public DXGI_RGB e752; - public DXGI_RGB e753; - public DXGI_RGB e754; - public DXGI_RGB e755; - public DXGI_RGB e756; - public DXGI_RGB e757; - public DXGI_RGB e758; - public DXGI_RGB e759; - public DXGI_RGB e760; - public DXGI_RGB e761; - public DXGI_RGB e762; - public DXGI_RGB e763; - public DXGI_RGB e764; - public DXGI_RGB e765; - public DXGI_RGB e766; - public DXGI_RGB e767; - public DXGI_RGB e768; - public DXGI_RGB e769; - public DXGI_RGB e770; - public DXGI_RGB e771; - public DXGI_RGB e772; - public DXGI_RGB e773; - public DXGI_RGB e774; - public DXGI_RGB e775; - public DXGI_RGB e776; - public DXGI_RGB e777; - public DXGI_RGB e778; - public DXGI_RGB e779; - public DXGI_RGB e780; - public DXGI_RGB e781; - public DXGI_RGB e782; - public DXGI_RGB e783; - public DXGI_RGB e784; - public DXGI_RGB e785; - public DXGI_RGB e786; - public DXGI_RGB e787; - public DXGI_RGB e788; - public DXGI_RGB e789; - public DXGI_RGB e790; - public DXGI_RGB e791; - public DXGI_RGB e792; - public DXGI_RGB e793; - public DXGI_RGB e794; - public DXGI_RGB e795; - public DXGI_RGB e796; - public DXGI_RGB e797; - public DXGI_RGB e798; - public DXGI_RGB e799; - public DXGI_RGB e800; - public DXGI_RGB e801; - public DXGI_RGB e802; - public DXGI_RGB e803; - public DXGI_RGB e804; - public DXGI_RGB e805; - public DXGI_RGB e806; - public DXGI_RGB e807; - public DXGI_RGB e808; - public DXGI_RGB e809; - public DXGI_RGB e810; - public DXGI_RGB e811; - public DXGI_RGB e812; - public DXGI_RGB e813; - public DXGI_RGB e814; - public DXGI_RGB e815; - public DXGI_RGB e816; - public DXGI_RGB e817; - public DXGI_RGB e818; - public DXGI_RGB e819; - public DXGI_RGB e820; - public DXGI_RGB e821; - public DXGI_RGB e822; - public DXGI_RGB e823; - public DXGI_RGB e824; - public DXGI_RGB e825; - public DXGI_RGB e826; - public DXGI_RGB e827; - public DXGI_RGB e828; - public DXGI_RGB e829; - public DXGI_RGB e830; - public DXGI_RGB e831; - public DXGI_RGB e832; - public DXGI_RGB e833; - public DXGI_RGB e834; - public DXGI_RGB e835; - public DXGI_RGB e836; - public DXGI_RGB e837; - public DXGI_RGB e838; - public DXGI_RGB e839; - public DXGI_RGB e840; - public DXGI_RGB e841; - public DXGI_RGB e842; - public DXGI_RGB e843; - public DXGI_RGB e844; - public DXGI_RGB e845; - public DXGI_RGB e846; - public DXGI_RGB e847; - public DXGI_RGB e848; - public DXGI_RGB e849; - public DXGI_RGB e850; - public DXGI_RGB e851; - public DXGI_RGB e852; - public DXGI_RGB e853; - public DXGI_RGB e854; - public DXGI_RGB e855; - public DXGI_RGB e856; - public DXGI_RGB e857; - public DXGI_RGB e858; - public DXGI_RGB e859; - public DXGI_RGB e860; - public DXGI_RGB e861; - public DXGI_RGB e862; - public DXGI_RGB e863; - public DXGI_RGB e864; - public DXGI_RGB e865; - public DXGI_RGB e866; - public DXGI_RGB e867; - public DXGI_RGB e868; - public DXGI_RGB e869; - public DXGI_RGB e870; - public DXGI_RGB e871; - public DXGI_RGB e872; - public DXGI_RGB e873; - public DXGI_RGB e874; - public DXGI_RGB e875; - public DXGI_RGB e876; - public DXGI_RGB e877; - public DXGI_RGB e878; - public DXGI_RGB e879; - public DXGI_RGB e880; - public DXGI_RGB e881; - public DXGI_RGB e882; - public DXGI_RGB e883; - public DXGI_RGB e884; - public DXGI_RGB e885; - public DXGI_RGB e886; - public DXGI_RGB e887; - public DXGI_RGB e888; - public DXGI_RGB e889; - public DXGI_RGB e890; - public DXGI_RGB e891; - public DXGI_RGB e892; - public DXGI_RGB e893; - public DXGI_RGB e894; - public DXGI_RGB e895; - public DXGI_RGB e896; - public DXGI_RGB e897; - public DXGI_RGB e898; - public DXGI_RGB e899; - public DXGI_RGB e900; - public DXGI_RGB e901; - public DXGI_RGB e902; - public DXGI_RGB e903; - public DXGI_RGB e904; - public DXGI_RGB e905; - public DXGI_RGB e906; - public DXGI_RGB e907; - public DXGI_RGB e908; - public DXGI_RGB e909; - public DXGI_RGB e910; - public DXGI_RGB e911; - public DXGI_RGB e912; - public DXGI_RGB e913; - public DXGI_RGB e914; - public DXGI_RGB e915; - public DXGI_RGB e916; - public DXGI_RGB e917; - public DXGI_RGB e918; - public DXGI_RGB e919; - public DXGI_RGB e920; - public DXGI_RGB e921; - public DXGI_RGB e922; - public DXGI_RGB e923; - public DXGI_RGB e924; - public DXGI_RGB e925; - public DXGI_RGB e926; - public DXGI_RGB e927; - public DXGI_RGB e928; - public DXGI_RGB e929; - public DXGI_RGB e930; - public DXGI_RGB e931; - public DXGI_RGB e932; - public DXGI_RGB e933; - public DXGI_RGB e934; - public DXGI_RGB e935; - public DXGI_RGB e936; - public DXGI_RGB e937; - public DXGI_RGB e938; - public DXGI_RGB e939; - public DXGI_RGB e940; - public DXGI_RGB e941; - public DXGI_RGB e942; - public DXGI_RGB e943; - public DXGI_RGB e944; - public DXGI_RGB e945; - public DXGI_RGB e946; - public DXGI_RGB e947; - public DXGI_RGB e948; - public DXGI_RGB e949; - public DXGI_RGB e950; - public DXGI_RGB e951; - public DXGI_RGB e952; - public DXGI_RGB e953; - public DXGI_RGB e954; - public DXGI_RGB e955; - public DXGI_RGB e956; - public DXGI_RGB e957; - public DXGI_RGB e958; - public DXGI_RGB e959; - public DXGI_RGB e960; - public DXGI_RGB e961; - public DXGI_RGB e962; - public DXGI_RGB e963; - public DXGI_RGB e964; - public DXGI_RGB e965; - public DXGI_RGB e966; - public DXGI_RGB e967; - public DXGI_RGB e968; - public DXGI_RGB e969; - public DXGI_RGB e970; - public DXGI_RGB e971; - public DXGI_RGB e972; - public DXGI_RGB e973; - public DXGI_RGB e974; - public DXGI_RGB e975; - public DXGI_RGB e976; - public DXGI_RGB e977; - public DXGI_RGB e978; - public DXGI_RGB e979; - public DXGI_RGB e980; - public DXGI_RGB e981; - public DXGI_RGB e982; - public DXGI_RGB e983; - public DXGI_RGB e984; - public DXGI_RGB e985; - public DXGI_RGB e986; - public DXGI_RGB e987; - public DXGI_RGB e988; - public DXGI_RGB e989; - public DXGI_RGB e990; - public DXGI_RGB e991; - public DXGI_RGB e992; - public DXGI_RGB e993; - public DXGI_RGB e994; - public DXGI_RGB e995; - public DXGI_RGB e996; - public DXGI_RGB e997; - public DXGI_RGB e998; - public DXGI_RGB e999; - public DXGI_RGB e1000; - public DXGI_RGB e1001; - public DXGI_RGB e1002; - public DXGI_RGB e1003; - public DXGI_RGB e1004; - public DXGI_RGB e1005; - public DXGI_RGB e1006; - public DXGI_RGB e1007; - public DXGI_RGB e1008; - public DXGI_RGB e1009; - public DXGI_RGB e1010; - public DXGI_RGB e1011; - public DXGI_RGB e1012; - public DXGI_RGB e1013; - public DXGI_RGB e1014; - public DXGI_RGB e1015; - public DXGI_RGB e1016; - public DXGI_RGB e1017; - public DXGI_RGB e1018; - public DXGI_RGB e1019; - public DXGI_RGB e1020; - public DXGI_RGB e1021; - public DXGI_RGB e1022; - public DXGI_RGB e1023; - public DXGI_RGB e1024; -#if NET6_0_OR_GREATER - public ref DXGI_RGB this[int index] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - return ref AsSpan()[index]; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Span AsSpan() => MemoryMarshal.CreateSpan(ref e0, 1025); -#else - // there is no way to do this outside of terrible unsafe code. Don't do this in .Net Standard 2.0 - - public DXGI_RGB this[int index] - { - get - { - if ((uint)index > 1025) - throw new ArgumentOutOfRangeException("index"); - - fixed (DXGI_RGB* basePtr = &e0) - { - DXGI_RGB* newPtr = basePtr + index; - return *newPtr; - } - } - set - { - if ((uint)index > 1025) - throw new ArgumentOutOfRangeException("index"); - - fixed (DXGI_RGB* basePtr = &e0) - { - DXGI_RGB* newPtr = basePtr + index; - *newPtr = value; - } - } - } -#endif - } - } - + internal unsafe struct DEVMODEW { public fixed ushort dmDeviceName[32]; @@ -1196,7 +119,7 @@ namespace Avalonia.Win32.DxgiSwapchain public uint Flags; } - internal unsafe struct DXGI_FRAME_STATISTICS + internal struct DXGI_FRAME_STATISTICS { public uint PresentCount; @@ -1225,11 +148,10 @@ namespace Avalonia.Win32.DxgiSwapchain internal unsafe struct DXGI_MAPPED_RECT { public int Pitch; - public byte* pBits; } - internal unsafe partial struct DXGI_MODE_DESC + internal struct DXGI_MODE_DESC { public ushort Width; public ushort Height; @@ -1239,7 +161,7 @@ namespace Avalonia.Win32.DxgiSwapchain public DXGI_MODE_SCALING Scaling; } - internal unsafe partial struct DXGI_OUTPUT_DESC + internal unsafe struct DXGI_OUTPUT_DESC { internal fixed ushort DeviceName[32]; @@ -1263,13 +185,13 @@ namespace Avalonia.Win32.DxgiSwapchain public POINT* pScrollOffset; } - internal unsafe partial struct DXGI_RATIONAL + internal struct DXGI_RATIONAL { public ushort Numerator; public ushort Denominator; } - internal partial struct DXGI_RGB + internal struct DXGI_RGB { public float Red; @@ -1278,7 +200,7 @@ namespace Avalonia.Win32.DxgiSwapchain public float Blue; } - internal partial struct DXGI_RGBA + internal struct DXGI_RGBA { public float r; @@ -1295,7 +217,7 @@ namespace Avalonia.Win32.DxgiSwapchain public uint Quality; } - internal unsafe struct DXGI_SURFACE_DESC + internal struct DXGI_SURFACE_DESC { public uint Width; @@ -1306,7 +228,7 @@ namespace Avalonia.Win32.DxgiSwapchain public DXGI_SAMPLE_DESC SampleDesc; } - internal unsafe partial struct DXGI_SWAP_CHAIN_DESC + internal struct DXGI_SWAP_CHAIN_DESC { public DXGI_MODE_DESC BufferDesc; public DXGI_SAMPLE_DESC SampleDesc; @@ -1333,7 +255,7 @@ namespace Avalonia.Win32.DxgiSwapchain public uint Flags; } - internal unsafe struct DXGI_SWAP_CHAIN_FULLSCREEN_DESC + internal struct DXGI_SWAP_CHAIN_FULLSCREEN_DESC { public DXGI_RATIONAL RefreshRate; @@ -1344,7 +266,7 @@ namespace Avalonia.Win32.DxgiSwapchain public int Windowed; } - internal partial struct D3D11_TEXTURE2D_DESC + internal struct D3D11_TEXTURE2D_DESC { public uint Width; diff --git a/src/Windows/Avalonia.Win32/DirectX/DirectXUnmanagedMethods.cs b/src/Windows/Avalonia.Win32/DirectX/DirectXUnmanagedMethods.cs index 1a739ce3d1..14a9ff1277 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DirectXUnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DirectXUnmanagedMethods.cs @@ -5,7 +5,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; -namespace Avalonia.Win32.DxgiSwapchain +namespace Avalonia.Win32.DirectX { internal unsafe class DirectXUnmanagedMethods { @@ -23,7 +23,16 @@ namespace Avalonia.Win32.DxgiSwapchain [DllImport("user32", ExactSpelling = true)] internal static extern bool EnumDisplaySettingsW(ushort* lpszDeviceName, uint iModeNum, DEVMODEW* lpDevMode); - [DllImport("user32", ExactSpelling = true, SetLastError = true)] - internal static extern bool GetClientRect(IntPtr hWnd, Interop.UnmanagedMethods.RECT* lpRect); + [DllImport("d3d11", ExactSpelling = true, PreserveSig = false)] + public static extern void D3D11CreateDevice( + IntPtr adapter, D3D_DRIVER_TYPE DriverType, + IntPtr Software, + uint Flags, + D3D_FEATURE_LEVEL[] pFeatureLevels, + uint FeatureLevels, + uint SDKVersion, + out IntPtr ppDevice, + out D3D_FEATURE_LEVEL pFeatureLevel, + IntPtr* ppImmediateContext); } } diff --git a/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs b/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs index 8c13ecdcc1..e82f7633be 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs @@ -10,11 +10,12 @@ using Avalonia.Logging; using Avalonia.OpenGL.Angle; using Avalonia.OpenGL.Egl; using Avalonia.Rendering; +using Avalonia.Win32.OpenGl.Angle; using static Avalonia.Win32.Interop.UnmanagedMethods; -using static Avalonia.Win32.DxgiSwapchain.DirectXUnmanagedMethods; +using static Avalonia.Win32.DirectX.DirectXUnmanagedMethods; using MicroCom.Runtime; -namespace Avalonia.Win32.DxgiSwapchain +namespace Avalonia.Win32.DirectX { #pragma warning disable CA1416 // This should only be reachable on Windows #nullable enable @@ -25,34 +26,27 @@ namespace Avalonia.Win32.DxgiSwapchain public bool RunsInBackground => true; public event Action? Tick; - - private AngleWin32EglDisplay _angle; - private EglPlatformOpenGlInterface _gl; private object _syncLock; private IDXGIOutput? _output = null; private Stopwatch? _stopwatch = null; + private const string LogArea = "DXGI"; - public DxgiConnection(EglPlatformOpenGlInterface gl, object syncLock) + public DxgiConnection(object syncLock) { - _syncLock = syncLock; - _angle = (AngleWin32EglDisplay)gl.Display; - _gl = gl; } - - public EglPlatformOpenGlInterface Egl => _gl; - - public static void TryCreateAndRegister(EglPlatformOpenGlInterface angle) + + public static void TryCreateAndRegister() { try { - TryCreateAndRegisterCore(angle); + TryCreateAndRegisterCore(); } catch (Exception ex) { - Logger.TryGet(LogEventLevel.Error, nameof(DxgiSwapchain)) + Logger.TryGet(LogEventLevel.Error, LogArea) ?.Log(null, "Unable to establish Dxgi: {0}", ex); } } @@ -66,7 +60,7 @@ namespace Avalonia.Win32.DxgiSwapchain } catch (Exception ex) { - Logger.TryGet(LogEventLevel.Error, nameof(DxgiSwapchain)) + Logger.TryGet(LogEventLevel.Error, LogArea) ?.Log(this, $"Failed to wait for vblank, Exception: {ex.Message}, HRESULT = {ex.HResult}"); } @@ -84,7 +78,7 @@ namespace Avalonia.Win32.DxgiSwapchain } catch (Exception ex) { - Logger.TryGet(LogEventLevel.Error, nameof(DxgiSwapchain)) + Logger.TryGet(LogEventLevel.Error, LogArea) ?.Log(this, $"Failed to wait for vblank, Exception: {ex.Message}, HRESULT = {ex.HResult}"); _output.Dispose(); _output = null; @@ -103,7 +97,7 @@ namespace Avalonia.Win32.DxgiSwapchain } catch (Exception ex) { - Logger.TryGet(LogEventLevel.Error, nameof(DxgiSwapchain)) + Logger.TryGet(LogEventLevel.Error, LogArea) ?.Log(this, $"Failed to wait for vblank, Exception: {ex.Message}, HRESULT = {ex.HResult}"); } } @@ -168,7 +162,7 @@ namespace Avalonia.Win32.DxgiSwapchain } // Used the windows composition as a blueprint for this startup/creation - static private bool TryCreateAndRegisterCore(EglPlatformOpenGlInterface gl) + static private bool TryCreateAndRegisterCore() { var tcs = new TaskCompletionSource(); var pumpLock = new object(); @@ -178,7 +172,7 @@ namespace Avalonia.Win32.DxgiSwapchain { DxgiConnection connection; - connection = new DxgiConnection(gl, pumpLock); + connection = new DxgiConnection(pumpLock); AvaloniaLocator.CurrentMutable.BindToSelf(connection); AvaloniaLocator.CurrentMutable.Bind().ToConstant(connection); diff --git a/src/Windows/Avalonia.Win32/DirectX/DxgiRenderTarget.cs b/src/Windows/Avalonia.Win32/DirectX/DxgiRenderTarget.cs index 2ab9b30d43..cb7826e185 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DxgiRenderTarget.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DxgiRenderTarget.cs @@ -8,11 +8,12 @@ using System.Threading.Tasks; using Avalonia.OpenGL.Angle; using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; +using Avalonia.Win32.OpenGl.Angle; using MicroCom.Runtime; using static Avalonia.OpenGL.Egl.EglGlPlatformSurfaceBase; using static Avalonia.Win32.Interop.UnmanagedMethods; -namespace Avalonia.Win32.DxgiSwapchain +namespace Avalonia.Win32.DirectX { #pragma warning disable CA1416 // Validate platform compatibility, if you enter this not on windows you have messed up badly #nullable enable @@ -22,8 +23,7 @@ namespace Avalonia.Win32.DxgiSwapchain public const uint DXGI_USAGE_RENDER_TARGET_OUTPUT = 0x00000020U; - private IEglWindowGlPlatformSurfaceInfo _window; - private EglPlatformOpenGlInterface _egl; + private EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _window; private DxgiConnection _connection; private IDXGIDevice? _dxgiDevice = null; private IDXGIFactory2? _dxgiFactory = null; @@ -36,15 +36,14 @@ namespace Avalonia.Win32.DxgiSwapchain private Guid ID3D11Texture2DGuid = Guid.Parse("6F15AAF2-D208-4E89-9AB4-489535D34F9C"); - public DxgiRenderTarget(IEglWindowGlPlatformSurfaceInfo window, EglPlatformOpenGlInterface egl, DxgiConnection connection) : base(egl) + public DxgiRenderTarget(EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo window, EglContext context, DxgiConnection connection) : base(context) { _window = window; - _egl = egl; _connection = connection; // the D3D device is expected to at least be an ID3D11Device // but how do I wrap an IntPtr as a managed IUnknown now? Like this. - IUnknown pdevice = MicroComRuntime.CreateProxyFor(((AngleWin32EglDisplay)_egl.Display).GetDirect3DDevice(), false); + IUnknown pdevice = MicroComRuntime.CreateProxyFor(((AngleWin32EglDisplay)context.Display).GetDirect3DDevice(), false); _dxgiDevice = pdevice.QueryInterface(); @@ -86,14 +85,14 @@ namespace Avalonia.Win32.DxgiSwapchain _clientRect = pClientRect; } - public override IGlPlatformSurfaceRenderingSession BeginDraw() + public override IGlPlatformSurfaceRenderingSession BeginDrawCore() { if (_swapChain is null) { throw new InvalidOperationException("No chain to draw on"); } - var contextLock = _egl.PrimaryContext.EnsureCurrent(); + var contextLock = Context.EnsureCurrent(); EglSurface? surface = null; IDisposable? transaction = null; var success = false; @@ -132,10 +131,10 @@ namespace Avalonia.Win32.DxgiSwapchain _renderTexture = texture; // I also have to get the pointer to this texture directly - surface = ((AngleWin32EglDisplay)_egl.Display).WrapDirect3D11Texture(_egl, MicroComRuntime.GetNativeIntPtr(_renderTexture), + surface = ((AngleWin32EglDisplay)Context.Display).WrapDirect3D11Texture(MicroComRuntime.GetNativeIntPtr(_renderTexture), 0, 0, size.Width, size.Height); - var res = base.BeginDraw(surface, _window, () => + var res = base.BeginDraw(surface, _window.Size, _window.Scaling, () => { _swapChain.Present((ushort)0U, (ushort)0U); surface?.Dispose(); diff --git a/src/Windows/Avalonia.Win32/DirectX/DxgiSwapchainWindow.cs b/src/Windows/Avalonia.Win32/DirectX/DxgiSwapchainWindow.cs index a40d8abf80..88226c5c89 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DxgiSwapchainWindow.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DxgiSwapchainWindow.cs @@ -3,29 +3,29 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Avalonia.OpenGL; using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; -namespace Avalonia.Win32.DxgiSwapchain +namespace Avalonia.Win32.DirectX { public class DxgiSwapchainWindow : EglGlPlatformSurfaceBase { private DxgiConnection _connection; - private EglPlatformOpenGlInterface _egl; - private IEglWindowGlPlatformSurfaceInfo _window; + private EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _window; - public DxgiSwapchainWindow(DxgiConnection connection, IEglWindowGlPlatformSurfaceInfo window) + public DxgiSwapchainWindow(DxgiConnection connection, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo window) { _connection = connection; _window = window; - _egl = connection.Egl; } - public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { - using (_egl.PrimaryContext.EnsureCurrent()) + var eglContext = (EglContext)context; + using (eglContext.EnsureCurrent()) { - return new DxgiRenderTarget(_window, _egl, _connection); + return new DxgiRenderTarget(_window, eglContext, _connection); } } } diff --git a/src/Windows/Avalonia.Win32/DirectX/IDirect3D11TexturePlatformSurface.cs b/src/Windows/Avalonia.Win32/DirectX/IDirect3D11TexturePlatformSurface.cs new file mode 100644 index 0000000000..d2a2b2f513 --- /dev/null +++ b/src/Windows/Avalonia.Win32/DirectX/IDirect3D11TexturePlatformSurface.cs @@ -0,0 +1,26 @@ +using System; +using Avalonia.OpenGL; +using Avalonia.Platform; + +namespace Avalonia.Win32.DirectX; + +public interface IDirect3D11TexturePlatformSurface +{ + public IDirect3D11TextureRenderTarget CreateRenderTarget(IPlatformGraphicsContext graphicsContext, IntPtr d3dDevice); +} + + + +public interface IDirect3D11TextureRenderTarget : IDisposable +{ + bool IsCorrupted { get; } + IDirect3D11TextureRenderTargetRenderSession BeginDraw(); +} + +public interface IDirect3D11TextureRenderTargetRenderSession : IDisposable +{ + public IntPtr D3D11Texture2D { get; } + public PixelSize Size { get; } + public PixelPoint Offset { get; } + public double Scaling { get; } +} diff --git a/src/Windows/Avalonia.Win32/DirectX/directx.idl b/src/Windows/Avalonia.Win32/DirectX/directx.idl index 09d1adf574..a4552eedea 100644 --- a/src/Windows/Avalonia.Win32/DirectX/directx.idl +++ b/src/Windows/Avalonia.Win32/DirectX/directx.idl @@ -1,4 +1,4 @@ -@clr-namespace Avalonia.Win32.DxgiSwapchain +@clr-namespace Avalonia.Win32.DirectX @clr-access internal @clr-map FLOAT float @clr-map HSTRING IntPtr @@ -12,6 +12,7 @@ @clr-map HWND IntPtr @clr-map BOOL int @clr-map DWORD int +@clr-map SIZE_T IntPtr @clr-map boolean int @clr-map BYTE byte @clr-map INT16 short @@ -242,9 +243,9 @@ interface IDXGIOutput : IDXGIObject HRESULT WaitForVBlank(); HRESULT TakeOwnership([in, annotation("_In_")] IUnknown* pDevice, BOOL Exclusive); void ReleaseOwnership(); - HRESULT GetGammaControlCapabilities([out, annotation("_Out_")] DXGI_GAMMA_CONTROL_CAPABILITIES* pGammaCaps); - HRESULT SetGammaControl([in, annotation("_In_")] DXGI_GAMMA_CONTROL* pArray); - HRESULT GetGammaControl([out, annotation("_Out_")] DXGI_GAMMA_CONTROL* pArray); + HRESULT GetGammaControlCapabilities(IntPtr pGammaCaps); + HRESULT SetGammaControl([in, annotation("_In_")] void* pArray); + HRESULT GetGammaControl(IntPtr pArray); HRESULT SetDisplaySurface([in, annotation("_In_")] IDXGISurface* pScanoutSurface); HRESULT GetDisplaySurfaceData([in, annotation("_In_")] IDXGISurface* pDestination); HRESULT GetFrameStatistics([out, annotation("_Out_")] DXGI_FRAME_STATISTICS* pStats); @@ -303,3 +304,180 @@ interface IDXGISwapChain1 : IDXGISwapChain HRESULT GetRotation([out, annotation("_Out_")] DXGI_MODE_ROTATION* pRotation); } +enum D3D11_FEATURE +{ + D3D11_FEATURE_THREADING, + D3D11_FEATURE_DOUBLES, + D3D11_FEATURE_FORMAT_SUPPORT, + D3D11_FEATURE_FORMAT_SUPPORT2, + D3D11_FEATURE_D3D10_X_HARDWARE_OPTIONS, + D3D11_FEATURE_D3D11_OPTIONS, + D3D11_FEATURE_ARCHITECTURE_INFO, + D3D11_FEATURE_D3D9_OPTIONS, + D3D11_FEATURE_SHADER_MIN_PRECISION_SUPPORT, + D3D11_FEATURE_D3D9_SHADOW_SUPPORT, + D3D11_FEATURE_D3D11_OPTIONS1, + D3D11_FEATURE_D3D9_SIMPLE_INSTANCING_SUPPORT, + D3D11_FEATURE_MARKER_SUPPORT, + D3D11_FEATURE_D3D9_OPTIONS1, + D3D11_FEATURE_D3D11_OPTIONS2, + D3D11_FEATURE_D3D11_OPTIONS3, + D3D11_FEATURE_GPU_VIRTUAL_ADDRESS_SUPPORT, + D3D11_FEATURE_D3D11_OPTIONSS, + D3D11_FEATURE_SHADER_CACHE +} + +[uuid(db6f6ddb-ac77-4e88-8253-819df9bbf140)] +interface ID3D11Device : IUnknown +{ + HRESULT CreateBuffer( + IntPtr pDesc, + IntPtr pInitialData, + [out, retval] IUnknown** ppBuffer ); + HRESULT CreateTexture1D( + IntPtr pDesc, + IntPtr pInitialData, + [out, retval] IUnknown** ppTexture1D ); + HRESULT CreateTexture2D( + IntPtr pDesc, + IntPtr pInitialData, + [out, retval] IUnknown** ppTexture2D ); + HRESULT CreateTexture3D( + IntPtr pDesc, + IntPtr pInitialData, + [out, retval] IUnknown** ppTexture3D ); + HRESULT CreateShaderResourceView( + IntPtr pResource, + IntPtr pDesc, + [out, retval] IUnknown** ppSRView ); + HRESULT CreateUnorderedAccessView( + IntPtr pResource, + IntPtr pDesc, + [out, retval] IUnknown** ppUAView ); + HRESULT CreateRenderTargetView( + IntPtr pResource, + IntPtr pDesc, + [out, retval] IUnknown** ppRTView); + HRESULT CreateDepthStencilView( + IntPtr pResource, + IntPtr pDesc, + [out, retval] IUnknown** ppDepthStencilView ); + HRESULT CreateInputLayout( + IntPtr pInputElementDescs, + UINT NumElements, + void* pShaderBytecodeWithInputSignature, + IntPtr BytecodeLength, + [out, retval] IUnknown** ppInputLayout ); + HRESULT CreateVertexShader( + IntPtr pShaderBytecode, + [annotation("_In_")] SIZE_T BytecodeLength, + IntPtr pClassLinkage, + [out, retval] IUnknown** ppVertexShader ); + HRESULT CreateGeometryShader( + IntPtr pShaderBytecode, + [annotation("_In_")] SIZE_T BytecodeLength, + IntPtr pClassLinkage, + [out, retval] IUnknown** ppGeometryShader ); + HRESULT CreateGeometryShaderWithStreamOutput( + IntPtr pShaderBytecode, + [annotation("_In_")] SIZE_T BytecodeLength, + IntPtr pSODeclaration, + UINT NumEntries, + UINT* pBufferStrides, + UINT NumStrides, + UINT RasterizedStream, + IntPtr pClassLinkage, + [out, retval] IUnknown** ppGeometryShader ); + HRESULT CreatePixelShader( + IntPtr pShaderBytecode, + [annotation("_In_")] SIZE_T BytecodeLength, + IntPtr pClassLinkage, + [out, retval] IUnknown** ppPixelShader ); + HRESULT CreateHullShader( + IntPtr pShaderBytecode, + [annotation("_In_")] SIZE_T BytecodeLength, + IntPtr pClassLinkage, + [out, retval] IUnknown** ppHullShader ); + HRESULT CreateDomainShader( + IntPtr pShaderBytecode, + [annotation("_In_")] SIZE_T BytecodeLength, + IntPtr pClassLinkage, + [out, retval] IUnknown** ppDomainShader ); + HRESULT CreateComputeShader( + IntPtr pShaderBytecode, + [annotation("_In_")] SIZE_T BytecodeLength, + IntPtr pClassLinkage, + [out, retval] IUnknown** ppComputeShader ); + HRESULT CreateClassLinkage([out, retval]IUnknown** ppLinkage); + HRESULT CreateBlendState( + IntPtr pBlendStateDesc, + [out, retval] IUnknown** ppBlendState ); + HRESULT CreateDepthStencilState( + IntPtr pDepthStencilDesc, + [out, retval] IUnknown** ppDepthStencilState ); + HRESULT CreateRasterizerState( + IntPtr pRasterizerDesc, + [out, retval] IUnknown** ppRasterizerState ); + HRESULT CreateSamplerState( + IntPtr pSamplerDesc, + [out, retval] IUnknown** ppSamplerState ); + HRESULT CreateQuery( + IntPtr pQueryDesc, + [out, retval] IUnknown** ppQuery ); + HRESULT CreatePredicate( + IntPtr pPredicateDesc, + [out, retval] IUnknown** ppPredicate ); + HRESULT CreateCounter( + IntPtr pCounterDesc, + [out, retval] IUnknown** ppCounter ); + HRESULT CreateDeferredContext( + UINT ContextFlags, // Reserved parameter; must be 0 + [out, retval] IUnknown** ppDeferredContext ); + HRESULT OpenSharedResource( + IntPtr hResource, + [out] Guid* ReturnedInterface, + [out, retval] IUnknown** ppResource); + + // Check* + HRESULT CheckFormatSupport( + [annotation("_In_")] DXGI_FORMAT Format, + [annotation("_Out_")] UINT* pFormatSupport ); + HRESULT CheckMultisampleQualityLevels( + [annotation("_In_")] DXGI_FORMAT Format, + [annotation("_In_")] UINT SampleCount, + [annotation("_Out_")] UINT* pNumQualityLevels ); + void CheckCounterInfo(IntPtr pCounterInfo ); + HRESULT CheckCounter( + IntPtr pDesc, + IntPtr pType, + IntPtr pActiveCounters, + IntPtr szName, + UINT* pNameLength, + IntPtr szUnits, + UINT* pUnitsLength, + IntPtr szDescription, + UINT* pDescriptionLength ); + HRESULT CheckFeatureSupport( + D3D11_FEATURE Feature, + void* pFeatureSupportData, + UINT FeatureSupportDataSize ); + + HRESULT GetPrivateData( + Guid* guid, + UINT* pDataSize, + void* pData ); + HRESULT SetPrivateData( + Guid* guid, + UINT DataSize, + IntPtr* pData ); + HRESULT SetPrivateDataInterface( + Guid* guid, + IUnknown* pData ); + + D3D_FEATURE_LEVEL GetFeatureLevel(); + UINT GetCreationFlags(); + int GetDeviceRemovedReason(); + void GetImmediateContext(IntPtr* ppImmediateContext ); + HRESULT SetExceptionMode( UINT RaiseFlags ); + UINT GetExceptionMode(); +} 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 9b86154043..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")] @@ -1627,7 +1635,7 @@ namespace Avalonia.Win32.Interop public static extern bool wglDeleteContext(IntPtr context); - [DllImport("opengl32.dll")] + [DllImport("opengl32.dll", SetLastError = true)] public static extern bool wglMakeCurrent(IntPtr hdc, IntPtr context); [DllImport("opengl32.dll")] 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/AngleD3DTextureFeature.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleD3DTextureFeature.cs new file mode 100644 index 0000000000..6c951010c8 --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleD3DTextureFeature.cs @@ -0,0 +1,103 @@ +using System.Runtime.InteropServices; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Egl; +using Avalonia.OpenGL.Surfaces; +using Avalonia.Win32.DirectX; + +namespace Avalonia.Win32.OpenGl.Angle; + +internal class AngleD3DTextureFeature : IGlPlatformSurfaceRenderTargetFactory +{ + public bool CanRenderToSurface(IGlContext context, object surface) => + context is EglContext + { + Display: AngleWin32EglDisplay { PlatformApi: AngleOptions.PlatformApi.DirectX11 } + } && surface is IDirect3D11TexturePlatformSurface; + + class RenderTargetWrapper : EglPlatformSurfaceRenderTargetBase + { + private readonly AngleWin32EglDisplay _angle; + private readonly IDirect3D11TextureRenderTarget _target; + + public RenderTargetWrapper(EglContext context, + AngleWin32EglDisplay angle, + IDirect3D11TextureRenderTarget target) : base(context) + { + _angle = angle; + _target = target; + } + + public override IGlPlatformSurfaceRenderingSession BeginDrawCore() + { + var success = false; + var contextLock = Context.EnsureCurrent(); + IDirect3D11TextureRenderTargetRenderSession session = null; + EglSurface surface = null; + try + { + try + { + session = _target.BeginDraw(); + } + catch (RenderTargetCorruptedException e) + { + if (e.InnerException is COMException com + && ((DXGI_ERROR)com.HResult).IsDeviceLostError()) + Context.NotifyContextLost(); + + throw; + } + + surface = _angle.WrapDirect3D11Texture(session.D3D11Texture2D, session.Offset.X, session.Offset.Y, + session.Size.Width, session.Size.Height); + var rv = BeginDraw(surface, session.Size, session.Scaling, () => + { + using(contextLock) + using (session) + using (surface) + { + } + }, true); + success = true; + return rv; + } + finally + { + if (!success) + { + using(contextLock) + using (session) + using (surface) + { + } + } + } + } + + public override void Dispose() + { + _target.Dispose(); + base.Dispose(); + } + + public override bool IsCorrupted => _target.IsCorrupted || base.IsCorrupted; + } + + public IGlPlatformSurfaceRenderTarget CreateRenderTarget(IGlContext context, object surface) + { + var ctx = (EglContext)context; + var angle = (AngleWin32EglDisplay)ctx.Display; + var textureSurface = (IDirect3D11TexturePlatformSurface)surface; + try + { + var target = textureSurface.CreateRenderTarget(context, angle.GetDirect3DDevice()); + return new RenderTargetWrapper(ctx, angle, target); + } + catch (COMException com) + { + if (((DXGI_ERROR)com.HResult).IsDeviceLostError()) + ctx.NotifyContextLost(); + throw; + } + } +} diff --git a/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleEglInterface.cs similarity index 56% rename from src/Avalonia.OpenGL/Angle/AngleEglInterface.cs rename to src/Windows/Avalonia.Win32/OpenGl/Angle/AngleEglInterface.cs index 3a332f37ad..4e00bc2c72 100644 --- a/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleEglInterface.cs @@ -1,18 +1,31 @@ using System; using System.Runtime.InteropServices; using Avalonia.OpenGL.Egl; +using Avalonia.SourceGenerator; namespace Avalonia.OpenGL.Angle { - public class AngleEglInterface : EglInterface + internal partial class Win32AngleEglInterface : EglInterface { [DllImport("av_libGLESv2.dll", CharSet = CharSet.Ansi)] static extern IntPtr EGL_GetProcAddress(string proc); - public AngleEglInterface() : base(LoadAngle()) - { + public Win32AngleEglInterface() : this(LoadAngle()) + { + } + + private Win32AngleEglInterface(Func getProcAddress) : base(getProcAddress) + { + Initialize(getProcAddress); + } + + [GetProcAddress("eglCreateDeviceANGLE", true)] + public partial IntPtr CreateDeviceANGLE(int deviceType, IntPtr nativeDevice, int[] attribs); + + [GetProcAddress("eglReleaseDeviceANGLE", true)] + public partial void ReleaseDeviceANGLE(IntPtr device); static Func LoadAngle() { diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs new file mode 100644 index 0000000000..b2d7b2014b --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs @@ -0,0 +1,140 @@ +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Angle; +using Avalonia.OpenGL.Egl; +using Avalonia.Win32.DirectX; +using MicroCom.Runtime; +using static Avalonia.OpenGL.Egl.EglConsts; + +namespace Avalonia.Win32.OpenGl.Angle +{ + internal class AngleWin32EglDisplay : EglDisplay + { + protected override bool DisplayLockIsSharedWithContexts => true; + + public static AngleWin32EglDisplay CreateD3D9Display(EglInterface egl) + { + var display = egl.GetPlatformDisplayExt(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, + new[] { EGL_PLATFORM_ANGLE_TYPE_ANGLE, EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE, EGL_NONE }); + + return new AngleWin32EglDisplay(display, new EglDisplayOptions() + { + Egl = egl, + ContextLossIsDisplayLoss = true, + GlVersions = AvaloniaLocator.Current.GetService()?.GlProfiles + }, AngleOptions.PlatformApi.DirectX9); + } + + public static AngleWin32EglDisplay CreateSharedD3D11Display(EglInterface egl) + { + var display = egl.GetPlatformDisplayExt(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, + new[] { EGL_PLATFORM_ANGLE_TYPE_ANGLE, EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE, EGL_NONE }); + + return new AngleWin32EglDisplay(display, new EglDisplayOptions() + { + Egl = egl, + ContextLossIsDisplayLoss = true, + GlVersions = AvaloniaLocator.Current.GetService()?.GlProfiles + }, AngleOptions.PlatformApi.DirectX11); + } + + public static AngleWin32EglDisplay CreateD3D11Display(Win32AngleEglInterface egl) + { + unsafe + { + var featureLevels = new[] + { + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_10_0, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_3, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_2, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_1 + }; + + DirectXUnmanagedMethods.D3D11CreateDevice(IntPtr.Zero, D3D_DRIVER_TYPE.D3D_DRIVER_TYPE_HARDWARE, + IntPtr.Zero, 0, featureLevels, (uint)featureLevels.Length, + 7, out var pD3dDevice, out var featureLevel, null); + if (pD3dDevice == IntPtr.Zero) + throw new Win32Exception("Unable to create D3D11 Device"); + + var d3dDevice = MicroComRuntime.CreateProxyFor(pD3dDevice, true); + var angleDevice = IntPtr.Zero; + var display = IntPtr.Zero; + + void Cleanup() + { + if (angleDevice != IntPtr.Zero) + egl.ReleaseDeviceANGLE(angleDevice); + d3dDevice.Dispose(); + } + + bool success = false; + try + { + angleDevice = egl.CreateDeviceANGLE(EGL_D3D11_DEVICE_ANGLE, pD3dDevice, null); + if (angleDevice == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreateDeviceANGLE", egl); + + display = egl.GetPlatformDisplayExt(EGL_PLATFORM_DEVICE_EXT, angleDevice, null); + if (display == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglGetPlatformDisplayEXT", egl); + + + var rv = new AngleWin32EglDisplay(display, new EglDisplayOptions + { + DisposeCallback = Cleanup, + Egl = egl, + ContextLossIsDisplayLoss = true, + DeviceLostCheckCallback = () => d3dDevice.DeviceRemovedReason != 0, + GlVersions = AvaloniaLocator.Current.GetService()?.GlProfiles + }, AngleOptions.PlatformApi.DirectX11); + success = true; + return rv; + } + finally + { + if (!success) + { + if (display != IntPtr.Zero) + egl.Terminate(display); + Cleanup(); + } + } + } + } + + private AngleWin32EglDisplay(IntPtr display, EglDisplayOptions options, AngleOptions.PlatformApi platformApi) : base(display, options) + { + PlatformApi = platformApi; + } + + public AngleOptions.PlatformApi PlatformApi { get; } + + public IntPtr GetDirect3DDevice() + { + if (!EglInterface.QueryDisplayAttribExt(Handle, EglConsts.EGL_DEVICE_EXT, out var eglDevice)) + throw new OpenGlException("Unable to get EGL_DEVICE_EXT"); + if (!EglInterface.QueryDeviceAttribExt(eglDevice, PlatformApi == AngleOptions.PlatformApi.DirectX9 ? EGL_D3D9_DEVICE_ANGLE : EGL_D3D11_DEVICE_ANGLE, out var d3dDeviceHandle)) + throw new OpenGlException("Unable to get EGL_D3D9_DEVICE_ANGLE"); + return d3dDeviceHandle; + } + + public EglSurface WrapDirect3D11Texture( IntPtr handle) + { + if (PlatformApi != AngleOptions.PlatformApi.DirectX11) + throw new InvalidOperationException("Current platform API is " + PlatformApi); + return CreatePBufferFromClientBuffer(EGL_D3D_TEXTURE_ANGLE, handle, new[] { EGL_NONE, EGL_NONE }); + } + + public EglSurface WrapDirect3D11Texture(IntPtr handle, int offsetX, int offsetY, int width, int height) + { + if (PlatformApi != AngleOptions.PlatformApi.DirectX11) + throw new InvalidOperationException("Current platform API is " + PlatformApi); + return CreatePBufferFromClientBuffer(EGL_D3D_TEXTURE_ANGLE, handle, new[] { EGL_WIDTH, width, EGL_HEIGHT, height, EGL_FLEXIBLE_SURFACE_COMPATIBILITY_SUPPORTED_ANGLE, EGL_TRUE, EGL_TEXTURE_OFFSET_X_ANGLE, offsetX, EGL_TEXTURE_OFFSET_Y_ANGLE, offsetY, EGL_NONE }); + } + } +} diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs new file mode 100644 index 0000000000..9a829aff92 --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Logging; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Angle; +using Avalonia.OpenGL.Egl; +using Avalonia.Platform; + +namespace Avalonia.Win32.OpenGl.Angle; + +internal class AngleWin32PlatformGraphics : IPlatformGraphics +{ + private readonly Win32AngleEglInterface _egl; + private AngleWin32EglDisplay _sharedDisplay; + private EglContext _sharedContext; + public bool UsesSharedContext => PlatformApi == AngleOptions.PlatformApi.DirectX9; + + public AngleOptions.PlatformApi PlatformApi { get; } = AngleOptions.PlatformApi.DirectX11; + public IPlatformGraphicsContext CreateContext() + { + if (UsesSharedContext) + throw new InvalidOperationException(); + + var display = AngleWin32EglDisplay.CreateD3D11Display(_egl); + var success = false; + try + { + var rv = display.CreateContext(new EglContextOptions + { + DisposeCallback = display.Dispose, + ExtraFeatures = new Dictionary + { + [typeof(IGlPlatformSurfaceRenderTargetFactory)] = new AngleD3DTextureFeature() + } + }); + success = true; + return rv; + } + finally + { + if (!success) + display.Dispose(); + } + } + + public IPlatformGraphicsContext GetSharedContext() + { + if (!UsesSharedContext) + throw new InvalidOperationException(); + if (_sharedContext == null || _sharedContext.IsLost) + { + _sharedContext?.Dispose(); + _sharedContext = null; + _sharedContext = _sharedDisplay.CreateContext(new EglContextOptions()); + } + + return _sharedContext; + } + + public AngleWin32PlatformGraphics(Win32AngleEglInterface egl, AngleWin32EglDisplay display) + : this(egl, display.PlatformApi) + { + _sharedDisplay = display; + } + + public AngleWin32PlatformGraphics(Win32AngleEglInterface egl, AngleOptions.PlatformApi api) + { + _egl = egl; + PlatformApi = api; + } + + + public static AngleWin32PlatformGraphics TryCreate(AngleOptions options) + { + + + Win32AngleEglInterface egl; + try + { + egl = new(); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL") + ?.Log(null, "Unable to load ANGLE: {0}", e); + return null; + } + + foreach (var api in (options?.AllowedPlatformApis ?? new [] + { + AngleOptions.PlatformApi.DirectX11 + }).Distinct()) + if (api == AngleOptions.PlatformApi.DirectX11) + { + try + { + using var display = AngleWin32EglDisplay.CreateD3D11Display(egl); + using var ctx = display.CreateContext(new EglContextOptions()); + ctx.MakeCurrent().Dispose(); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL") + ?.Log(null, "Unable to initialize ANGLE-based rendering with DirectX11 : {0}", e); + continue; + } + + return new AngleWin32PlatformGraphics(egl, AngleOptions.PlatformApi.DirectX11); + } + else + { + AngleWin32EglDisplay sharedDisplay = null; + try + { + sharedDisplay = AngleWin32EglDisplay.CreateD3D9Display(egl); + using (var ctx = sharedDisplay.CreateContext(new EglContextOptions())) + ctx.MakeCurrent().Dispose(); + + return new AngleWin32PlatformGraphics(egl, sharedDisplay); + } + catch (Exception e) + { + sharedDisplay?.Dispose(); + Logger.TryGet(LogEventLevel.Error, "OpenGL") + ?.Log(null, "Unable to initialize ANGLE-based rendering with DirectX9 : {0}", e); + } + } + return null; + } +} diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs index d6633ddb61..b4002d8bc7 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs @@ -1,6 +1,10 @@ using System; -using System.Reactive.Disposables; +using System.Collections.Generic; +using System.Linq; +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; @@ -43,8 +47,9 @@ namespace Avalonia.Win32.OpenGl public void Dispose() { wglDeleteContext(_context); - ReleaseDC(_hWnd, _dc); - DestroyWindow(_hWnd); + WglGdiResourceManager.ReleaseDC(_hWnd, _dc); + WglGdiResourceManager.DestroyWindow(_hWnd); + IsLost = true; } public GlVersion Version { get; } @@ -55,17 +60,26 @@ namespace Avalonia.Win32.OpenGl private bool IsCurrent => wglGetCurrentContext() == _context && wglGetCurrentDC() == _dc; public IDisposable MakeCurrent() { + if (IsLost) + throw new PlatformGraphicsContextLostException(); if(IsCurrent) return Disposable.Empty; return new WglRestoreContext(_dc, _context, _lock); } + public bool IsLost { get; private set; } public IDisposable EnsureCurrent() => MakeCurrent(); + internal IDisposable Lock() + { + Monitor.Enter(_lock); + return Disposable.Create(_lock, Monitor.Exit); + } + public IntPtr CreateConfiguredDeviceContext(IntPtr hWnd) { - var dc = GetDC(hWnd); + var dc = WglGdiResourceManager.GetDC(hWnd); var fmt = _formatDescriptor; SetPixelFormat(dc, _pixelFormat, ref fmt); return dc; @@ -81,5 +95,14 @@ namespace Avalonia.Win32.OpenGl || _sharedWith == context || _sharedWith != null && _sharedWith == c._sharedWith; } + + public bool CanCreateSharedContext => true; + public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) + { + var versions = preferredVersions?.Append(Version).ToArray() ?? new[] { Version }; + return WglDisplay.CreateContext(versions, _sharedWith ?? this); + } + + public object TryGetFeature(Type featureType) => null; } } diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs b/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs index bc27589689..9a1963a97a 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.InteropServices; using Avalonia.OpenGL; +using Avalonia.Threading; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; using static Avalonia.Win32.OpenGl.WglConsts; @@ -9,8 +10,6 @@ namespace Avalonia.Win32.OpenGl internal class WglDisplay { private static bool? _initialized; - private static ushort _windowClass; - private static readonly WndProc _wndProcDelegate = WndProc; private static readonly DebugCallbackDelegate _debugCallback = DebugCallback; private static IntPtr _bootstrapContext; @@ -44,18 +43,9 @@ namespace Avalonia.Win32.OpenGl } static bool InitializeCore() { - var wndClassEx = new WNDCLASSEX - { - cbSize = Marshal.SizeOf(), - hInstance = GetModuleHandle(null), - lpfnWndProc = _wndProcDelegate, - lpszClassName = "AvaloniaGlWindow-" + Guid.NewGuid(), - style = (int)ClassStyles.CS_OWNDC - }; - - _windowClass = RegisterClassEx(ref wndClassEx); - _bootstrapWindow = CreateOffscreenWindow(); - _bootstrapDc = GetDC(_bootstrapWindow); + Dispatcher.UIThread.VerifyAccess(); + _bootstrapWindow = WglGdiResourceManager.CreateOffscreenWindow(); + _bootstrapDc = WglGdiResourceManager.GetDC(_bootstrapWindow); _defaultPfd = new PixelFormatDescriptor { Size = (ushort)Marshal.SizeOf(), @@ -105,17 +95,11 @@ namespace Avalonia.Win32.OpenGl DescribePixelFormat(_bootstrapDc, formats[0], Marshal.SizeOf(), ref _defaultPfd); _defaultPixelFormat = formats[0]; } - - + wglMakeCurrent(IntPtr.Zero, IntPtr.Zero); return true; } - - static IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) - { - return DefWindowProc(hWnd, msg, wParam, lParam); - } - + private static void DebugCallback(int source, int type, int id, int severity, int len, IntPtr message, IntPtr userparam) { var err = Marshal.PtrToStringAnsi(message, len); @@ -131,27 +115,32 @@ namespace Avalonia.Win32.OpenGl using (new WglRestoreContext(_bootstrapDc, _bootstrapContext, null)) { - var window = CreateOffscreenWindow(); - var dc = GetDC(window); + var window = WglGdiResourceManager.CreateOffscreenWindow(); + var dc = WglGdiResourceManager.GetDC(window); SetPixelFormat(dc, _defaultPixelFormat, ref _defaultPfd); foreach (var version in versions) { if(version.Type != GlProfileType.OpenGL) continue; - var context = WglCreateContextAttribsArb(dc, shareContext?.Handle ?? IntPtr.Zero, - new[] - { - // major - WGL_CONTEXT_MAJOR_VERSION_ARB, version.Major, - // minor - WGL_CONTEXT_MINOR_VERSION_ARB, version.Minor, - // core profile - WGL_CONTEXT_PROFILE_MASK_ARB, 1, - // debug - // WGL_CONTEXT_FLAGS_ARB, 1, - // end - 0, 0 - }); + IntPtr context; + using (shareContext?.Lock()) + { + context = WglCreateContextAttribsArb(dc, shareContext?.Handle ?? IntPtr.Zero, + new[] + { + // major + WGL_CONTEXT_MAJOR_VERSION_ARB, version.Major, + // minor + WGL_CONTEXT_MINOR_VERSION_ARB, version.Minor, + // core profile + WGL_CONTEXT_PROFILE_MASK_ARB, 1, + // debug + // WGL_CONTEXT_FLAGS_ARB, 1, + // end + 0, 0 + }); + } + using(new WglRestoreContext(dc, context, null)) GlDebugMessageCallback(Marshal.GetFunctionPointerForDelegate(_debugCallback), IntPtr.Zero); if (context != IntPtr.Zero) @@ -159,26 +148,13 @@ namespace Avalonia.Win32.OpenGl _defaultPixelFormat, _defaultPfd); } - ReleaseDC(window, dc); - DestroyWindow(window); + WglGdiResourceManager.ReleaseDC(window, dc); + WglGdiResourceManager.DestroyWindow(window); return null; } } - static IntPtr CreateOffscreenWindow() => - CreateWindowEx( - 0, - _windowClass, - null, - (int)WindowStyles.WS_OVERLAPPEDWINDOW, - 0, - 0, - 640, - 480, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero); + } } diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglGdiResourceManager.cs b/src/Windows/Avalonia.Win32/OpenGl/WglGdiResourceManager.cs new file mode 100644 index 0000000000..ed40488c70 --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/WglGdiResourceManager.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Win32.Interop; + +namespace Avalonia.Win32.OpenGl; + +/// +/// - ReleaseDC can only happen from the same thread that has called GetDC +/// - When thread exits all of its windows and HDCs are getting destroyed +/// - We need to create OpenGL context (require a window and an HDC) and render targets (require an HDC) from thread pool threads +/// +/// So this class hosts a dedicated thread for managing offscreen windows and HDCs for OpenGL +/// + +internal class WglGdiResourceManager +{ + class GetDCOp + { + public IntPtr Window; + public TaskCompletionSource Result; + } + + class ReleaseDCOp + { + public IntPtr Window; + public IntPtr DC; + public TaskCompletionSource Result; + } + + class CreateWindowOp + { + public TaskCompletionSource Result; + } + + class DestroyWindowOp + { + public IntPtr Window; + public TaskCompletionSource Result; + } + + private static readonly Queue s_Queue = new(); + private static readonly AutoResetEvent s_Event = new(false); + private static readonly ushort s_WindowClass; + private static readonly UnmanagedMethods.WndProc s_wndProcDelegate = WndProc; + + static void Worker() + { + while (true) + { + s_Event.WaitOne(); + lock (s_Queue) + { + if(s_Queue.Count == 0) + continue; + var job = s_Queue.Dequeue(); + if (job is GetDCOp getDc) + getDc.Result.TrySetResult(UnmanagedMethods.GetDC(getDc.Window)); + else if (job is ReleaseDCOp releaseDc) + { + UnmanagedMethods.ReleaseDC(releaseDc.Window, releaseDc.DC); + releaseDc.Result.SetResult(null); + } + else if (job is CreateWindowOp createWindow) + createWindow.Result.TrySetResult(UnmanagedMethods.CreateWindowEx( + 0, + s_WindowClass, + null, + (int)UnmanagedMethods.WindowStyles.WS_OVERLAPPEDWINDOW, + 0, + 0, + 640, + 480, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero)); + else if (job is DestroyWindowOp destroyWindow) + { + UnmanagedMethods.DestroyWindow(destroyWindow.Window); + destroyWindow.Result.TrySetResult(null); + } + } + } + } + + static WglGdiResourceManager() + { + var wndClassEx = new UnmanagedMethods.WNDCLASSEX + { + cbSize = Marshal.SizeOf(), + hInstance = UnmanagedMethods.GetModuleHandle(null), + lpfnWndProc = s_wndProcDelegate, + lpszClassName = "AvaloniaGlWindow-" + Guid.NewGuid(), + style = (int)UnmanagedMethods.ClassStyles.CS_OWNDC + }; + + s_WindowClass = UnmanagedMethods.RegisterClassEx(ref wndClassEx); + var th = new Thread(Worker) { IsBackground = true, Name = "Win32 OpenGL HDC manager" }; + // This makes CLR to automatically pump the event queue from WaitOne + th.SetApartmentState(ApartmentState.STA); + th.Start(); + } + + + + static IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); + } + + public static IntPtr CreateOffscreenWindow() + { + var tcs = new TaskCompletionSource(); + lock(s_Queue) + s_Queue.Enqueue(new CreateWindowOp() + { + Result = tcs + }); + s_Event.Set(); + return tcs.Task.Result; + } + + public static IntPtr GetDC(IntPtr hWnd) + { + var tcs = new TaskCompletionSource(); + lock(s_Queue) + s_Queue.Enqueue(new GetDCOp + { + Window = hWnd, + Result = tcs + }); + s_Event.Set(); + return tcs.Task.Result; + } + + public static void ReleaseDC(IntPtr hWnd, IntPtr hDC) + { + var tcs = new TaskCompletionSource(); + lock(s_Queue) + s_Queue.Enqueue(new ReleaseDCOp() + { + Window = hWnd, + DC = hDC, + Result = tcs + }); + s_Event.Set(); + tcs.Task.Wait(); + } + + public static void DestroyWindow(IntPtr hWnd) + { + var tcs = new TaskCompletionSource(); + lock(s_Queue) + s_Queue.Enqueue(new DestroyWindowOp() + { + Window = hWnd, + Result = tcs + }); + s_Event.Set(); + tcs.Task.Wait(); + + } +} diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs b/src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs index 72bcffd447..6d3d2d28c4 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs @@ -11,18 +11,16 @@ namespace Avalonia.Win32.OpenGl class WglGlPlatformSurface: IGlPlatformSurface { - private readonly WglContext _context; private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info; - public WglGlPlatformSurface(WglContext context, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) + public WglGlPlatformSurface( EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) { - _context = context; _info = info; } - public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { - return new RenderTarget(_context, _info); + return new RenderTarget((WglContext)context, _info); } class RenderTarget : IGlPlatformSurfaceRenderTarget @@ -39,7 +37,7 @@ namespace Avalonia.Win32.OpenGl public void Dispose() { - UnmanagedMethods.ReleaseDC(_hdc, _info.Handle); + WglGdiResourceManager.ReleaseDC(_info.Handle, _hdc); } public IGlPlatformSurfaceRenderingSession BeginDraw() diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs b/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs index 1d0880a468..39dd330d52 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs @@ -6,15 +6,13 @@ using Avalonia.Platform; namespace Avalonia.Win32.OpenGl { - class WglPlatformOpenGlInterface : IPlatformOpenGlInterface + class WglPlatformOpenGlInterface : IPlatformGraphics { public WglContext PrimaryContext { get; } - IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; - IGlContext IPlatformOpenGlInterface.PrimaryContext => PrimaryContext; - public IGlContext CreateSharedContext() => WglDisplay.CreateContext(new[] { PrimaryContext.Version }, PrimaryContext); - - public bool CanShareContexts => true; - public bool CanCreateContexts => true; + public bool UsesSharedContext => false; + IPlatformGraphicsContext IPlatformGraphics.CreateContext() => CreateContext(); + public IPlatformGraphicsContext GetSharedContext() => throw new NotSupportedException(); + public IGlContext CreateContext() => WglDisplay.CreateContext(new[] { PrimaryContext.Version }, null); private WglPlatformOpenGlInterface(WglContext primary) diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglRestoreContext.cs b/src/Windows/Avalonia.Win32/OpenGl/WglRestoreContext.cs index 265f078a5c..b145ffbb37 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglRestoreContext.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglRestoreContext.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using System.Threading; using Avalonia.OpenGL; using static Avalonia.Win32.Interop.UnmanagedMethods; @@ -22,9 +23,11 @@ namespace Avalonia.Win32.OpenGl if (!wglMakeCurrent(gc, context)) { + var lastError = Marshal.GetLastWin32Error(); + var caps = GetDeviceCaps(gc, (DEVICECAP)12); if(monitor != null && takeMonitor) Monitor.Exit(monitor); - throw new OpenGlException("Unable to make the context current"); + throw new OpenGlException($"Unable to make the context current: {lastError}, DC valid: {caps != 0}"); } } diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index 204acc82c9..25ea060576 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -2,8 +2,9 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Angle; using Avalonia.OpenGL.Egl; using Avalonia.Platform; -using Avalonia.Win32.DxgiSwapchain; +using Avalonia.Win32.DirectX; using Avalonia.Win32.OpenGl; +using Avalonia.Win32.OpenGl.Angle; using Avalonia.Win32.WinRT.Composition; namespace Avalonia.Win32 @@ -11,15 +12,15 @@ namespace Avalonia.Win32 static class Win32GlManager { - public static IPlatformOpenGlInterface Initialize() + public static IPlatformGraphics Initialize() { var gl = InitializeCore(); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); return gl; } - static IPlatformOpenGlInterface InitializeCore() + static IPlatformGraphics InitializeCore() { var opts = AvaloniaLocator.Current.GetService() ?? new Win32PlatformOptions(); @@ -31,28 +32,18 @@ namespace Avalonia.Win32 if (opts.AllowEglInitialization ?? Win32Platform.WindowsVersion > PlatformConstants.Windows7) { - var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); + var egl = AngleWin32PlatformGraphics.TryCreate(AvaloniaLocator.Current.GetService() ?? + new()); - if (egl != null) + if (egl != null && egl.PlatformApi == AngleOptions.PlatformApi.DirectX11) { - if (opts.EglRendererBlacklist != null) - { - foreach (var item in opts.EglRendererBlacklist) - { - if (egl.PrimaryEglContext.GlInterface.Renderer.Contains(item)) - { - return null; - } - } - } - if (opts.UseWindowsUIComposition) { - WinUICompositorConnection.TryCreateAndRegister(egl, opts.CompositionBackdropCornerRadius); + WinUiCompositorConnection.TryCreateAndRegister(); } else if (opts.UseLowLatencyDxgiSwapChain) { - DxgiConnection.TryCreateAndRegister(egl); + DxgiConnection.TryCreateAndRegister(); } } 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 63cad9679a..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; @@ -23,11 +23,10 @@ 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( @@ -59,11 +58,6 @@ namespace Avalonia /// GPU rendering will not be enabled if this is set to false. /// public bool? AllowEglInitialization { get; set; } - - public IList EglRendererBlacklist { get; set; } = new List - { - "Microsoft Basic Render" - }; /// /// Embeds popups to the window when set to true. The default value is false. @@ -106,8 +100,14 @@ namespace Avalonia /// which if active will override this setting. /// public bool UseLowLatencyDxgiSwapChain { get; set; } = false; + + /// + /// Provides a way to use a custom-implemented graphics context such as a custom ISkiaGpu + /// + public IPlatformGraphics? CustomPlatformGraphics { get; set; } } } +#nullable restore namespace Avalonia.Win32 { @@ -139,6 +139,7 @@ namespace Avalonia.Win32 public static Win32PlatformOptions Options { get; private set; } internal static Compositor Compositor { get; private set; } + internal static PlatformRenderInterfaceContextManager RenderInterface { get; private set; } public static void Initialize() { @@ -169,16 +170,19 @@ namespace Avalonia.Win32 .Bind().ToConstant(new NonPumpingSyncContext.HelperImpl()) .Bind().ToConstant(new WindowsMountedVolumeInfoProvider()) .Bind().ToConstant(s_instance); - - var gl = Win32GlManager.Initialize(); - + _uiThread = Thread.CurrentThread; + var platformGraphics = options?.CustomPlatformGraphics + ?? Win32GlManager.Initialize(); + if (OleContext.Current != null) AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); if (Options.UseCompositor) - Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), gl); + Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), platformGraphics); + else + RenderInterface = new PlatformRenderInterfaceContextManager(platformGraphics); } public bool HasMessages() @@ -254,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 deleted file mode 100644 index ef3de9fbe1..0000000000 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositedWindow.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Numerics; -using System.Reactive.Disposables; -using System.Threading; -using Avalonia.MicroCom; -using Avalonia.OpenGL; -using Avalonia.OpenGL.Egl; -using Avalonia.Win32.Interop; -using MicroCom.Runtime; - -namespace Avalonia.Win32.WinRT.Composition -{ - public class WinUICompositedWindow : IDisposable - { - private EglContext _syncContext; - private readonly object _pumpLock; - private readonly IVisual _micaVisual; - private readonly ICompositionRoundedRectangleGeometry _roundedRectangleGeometry; - private readonly IVisual _blurVisual; - private ICompositionTarget _compositionTarget; - private IVisual _contentVisual; - private ICompositionDrawingSurfaceInterop _surfaceInterop; - private PixelSize _size; - - private static Guid IID_ID3D11Texture2D = Guid.Parse("6f15aaf2-d208-4e89-9ab4-489535d34f9c"); - private ICompositor _compositor; - - - internal WinUICompositedWindow(EglContext syncContext, - ICompositor compositor, - object pumpLock, - ICompositionTarget compositionTarget, - ICompositionDrawingSurfaceInterop surfaceInterop, - IVisual contentVisual, IVisual blurVisual, IVisual micaVisual, - ICompositionRoundedRectangleGeometry roundedRectangleGeometry) - { - _compositor = compositor.CloneReference(); - _syncContext = syncContext; - _pumpLock = pumpLock; - _micaVisual = micaVisual; - _roundedRectangleGeometry = roundedRectangleGeometry; - _blurVisual = blurVisual.CloneReference(); - _compositionTarget = compositionTarget.CloneReference(); - _contentVisual = contentVisual.CloneReference(); - _surfaceInterop = surfaceInterop.CloneReference(); - } - - - public void ResizeIfNeeded(PixelSize size) - { - using (_syncContext.EnsureLocked()) - { - if (_size != size) - { - _surfaceInterop.Resize(new UnmanagedMethods.POINT { X = size.Width, Y = size.Height }); - _contentVisual.SetSize(new Vector2(size.Width, size.Height)); - _roundedRectangleGeometry?.SetSize(new Vector2(size.Width, size.Height)); - _size = size; - } - } - } - - public unsafe IUnknown BeginDrawToTexture(out PixelPoint offset) - { - if (!_syncContext.IsCurrent) - throw new InvalidOperationException(); - - var iid = IID_ID3D11Texture2D; - void* pTexture; - var off = _surfaceInterop.BeginDraw(null, &iid, &pTexture); - offset = new PixelPoint(off.X, off.Y); - return MicroComRuntime.CreateProxyFor(pTexture, true); - } - - public void EndDraw() - { - if (!_syncContext.IsCurrent) - throw new InvalidOperationException(); - _surfaceInterop.EndDraw(); - } - - public void SetBlur(BlurEffect blurEffect) - { - using (_syncContext.EnsureLocked()) - { - _blurVisual.SetIsVisible(blurEffect == BlurEffect.Acrylic ? 1 : 0); - _micaVisual?.SetIsVisible(blurEffect == BlurEffect.Mica ? 1 : 0); - } - } - - public IDisposable BeginTransaction() - { - Monitor.Enter(_pumpLock); - return Disposable.Create(() => Monitor.Exit(_pumpLock)); - } - - public void Dispose() - { - if (_syncContext == null) - { - _compositor.Dispose(); - _blurVisual.Dispose(); - _contentVisual.Dispose(); - _surfaceInterop.Dispose(); - _compositionTarget.Dispose(); - } - } - } -} diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs deleted file mode 100644 index 6da17b8ea5..0000000000 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs +++ /dev/null @@ -1,311 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Numerics; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -using Avalonia.Logging; -using Avalonia.MicroCom; -using Avalonia.OpenGL; -using Avalonia.OpenGL.Angle; -using Avalonia.OpenGL.Egl; -using Avalonia.Rendering; -using Avalonia.Win32.Interop; -using MicroCom.Runtime; - -namespace Avalonia.Win32.WinRT.Composition -{ - class WinUICompositorConnection : IRenderTimer - { - public static readonly Version MinHostBackdropVersion = new Version(10, 0, 22000); - private readonly float? _backdropCornerRadius; - private readonly EglContext _syncContext; - private readonly ICompositionBrush _micaBrush; - private ICompositor _compositor; - private ICompositor5 _compositor5; - private ICompositorInterop _compositorInterop; - private AngleWin32EglDisplay _angle; - private ICompositionGraphicsDevice _device; - private EglPlatformOpenGlInterface _gl; - private ICompositorDesktopInterop _compositorDesktopInterop; - private ICompositionBrush _blurBrush; - private object _pumpLock = new object(); - - public WinUICompositorConnection(EglPlatformOpenGlInterface gl, object pumpLock, float? backdropCornerRadius) - { - _gl = gl; - _pumpLock = pumpLock; - _backdropCornerRadius = backdropCornerRadius; - _syncContext = _gl.PrimaryEglContext; - _angle = (AngleWin32EglDisplay)_gl.Display; - _compositor = NativeWinRTMethods.CreateInstance("Windows.UI.Composition.Compositor"); - _compositor5 = _compositor.QueryInterface(); - _compositorInterop = _compositor.QueryInterface(); - _compositorDesktopInterop = _compositor.QueryInterface(); - using var device = MicroComRuntime.CreateProxyFor(_angle.GetDirect3DDevice(), true); - - _device = _compositorInterop.CreateGraphicsDevice(device); - _blurBrush = CreateAcrylicBlurBackdropBrush(); - _micaBrush = CreateMicaBackdropBrush(); - } - - public EglPlatformOpenGlInterface Egl => _gl; - - static bool TryCreateAndRegisterCore(EglPlatformOpenGlInterface angle, float? backdropCornerRadius) - { - var tcs = new TaskCompletionSource(); - var pumpLock = new object(); - var th = new Thread(() => - { - WinUICompositorConnection connect; - try - { - NativeWinRTMethods.CreateDispatcherQueueController(new NativeWinRTMethods.DispatcherQueueOptions - { - apartmentType = NativeWinRTMethods.DISPATCHERQUEUE_THREAD_APARTMENTTYPE.DQTAT_COM_NONE, - dwSize = Marshal.SizeOf(), - threadType = NativeWinRTMethods.DISPATCHERQUEUE_THREAD_TYPE.DQTYPE_THREAD_CURRENT - }); - connect = new WinUICompositorConnection(angle, pumpLock, backdropCornerRadius); - AvaloniaLocator.CurrentMutable.BindToSelf(connect); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect); - tcs.SetResult(true); - - } - catch (Exception e) - { - tcs.SetException(e); - return; - } - connect.RunLoop(); - }) - { - IsBackground = true - }; - th.SetApartmentState(ApartmentState.STA); - th.Start(); - return tcs.Task.Result; - } - - class RunLoopHandler : IAsyncActionCompletedHandler, IMicroComShadowContainer - { - private readonly WinUICompositorConnection _parent; - private Stopwatch _st = Stopwatch.StartNew(); - - public RunLoopHandler(WinUICompositorConnection parent) - { - _parent = parent; - } - public void Dispose() - { - - } - - public void Invoke(IAsyncAction asyncInfo, AsyncStatus asyncStatus) - { - _parent.Tick?.Invoke(_st.Elapsed); - using var act = _parent._compositor5.RequestCommitAsync(); - act.SetCompleted(this); - } - - public MicroComShadow Shadow { get; set; } - public void OnReferencedFromNative() - { - } - - public void OnUnreferencedFromNative() - { - } - } - - private void RunLoop() - { - var cts = new CancellationTokenSource(); - AppDomain.CurrentDomain.ProcessExit += (sender, args) => - cts.Cancel(); - - using (var act = _compositor5.RequestCommitAsync()) - act.SetCompleted(new RunLoopHandler(this)); - - while (!cts.IsCancellationRequested) - { - UnmanagedMethods.GetMessage(out var msg, IntPtr.Zero, 0, 0); - lock (_pumpLock) - UnmanagedMethods.DispatchMessage(ref msg); - } - } - - public static void TryCreateAndRegister(EglPlatformOpenGlInterface angle, - float? backdropCornerRadius) - { - const int majorRequired = 10; - const int buildRequired = 17134; - - var majorInstalled = Win32Platform.WindowsVersion.Major; - var buildInstalled = Win32Platform.WindowsVersion.Build; - - if (majorInstalled >= majorRequired && - buildInstalled >= buildRequired) - { - try - { - TryCreateAndRegisterCore(angle, backdropCornerRadius); - return; - } - catch (Exception e) - { - Logger.TryGet(LogEventLevel.Error, "WinUIComposition") - ?.Log(null, "Unable to initialize WinUI compositor: {0}", e); - - } - } - - var osVersionNotice = - $"Windows {majorRequired} Build {buildRequired} is required. Your machine has Windows {majorInstalled} Build {buildInstalled} installed."; - - Logger.TryGet(LogEventLevel.Warning, "WinUIComposition")?.Log(null, - $"Unable to initialize WinUI compositor: {osVersionNotice}"); - } - - - public WinUICompositedWindow CreateWindow(IntPtr hWnd) - { - using var sc = _syncContext.EnsureLocked(); - using var desktopTarget = _compositorDesktopInterop.CreateDesktopWindowTarget(hWnd, 0); - using var target = desktopTarget.QueryInterface(); - using var device2 = _device.QueryInterface(); - - using var drawingSurface = device2.CreateDrawingSurface2(new UnmanagedMethods.SIZE(), DirectXPixelFormat.B8G8R8A8UIntNormalized, - DirectXAlphaMode.Premultiplied); - using var surface = drawingSurface.QueryInterface(); - using var surfaceInterop = drawingSurface.QueryInterface(); - - using var surfaceBrush = _compositor.CreateSurfaceBrushWithSurface(surface); - using var brush = surfaceBrush.QueryInterface(); - - using var spriteVisual = _compositor.CreateSpriteVisual(); - spriteVisual.SetBrush(brush); - using var visual = spriteVisual.QueryInterface(); - using var visual2 = spriteVisual.QueryInterface(); - using var container = _compositor.CreateContainerVisual(); - using var containerVisual = container.QueryInterface(); - using var containerVisual2 = container.QueryInterface(); - containerVisual2.SetRelativeSizeAdjustment(new Vector2(1, 1)); - using var containerChildren = container.Children; - - target.SetRoot(containerVisual); - - using var blur = CreateBlurVisual(_blurBrush); - IVisual mica = null; - if (_micaBrush != null) - { - mica = CreateBlurVisual(_micaBrush); - containerChildren.InsertAtTop(mica); - } - - var compositionRoundedRectangleGeometry = ClipVisual(blur, mica); - - containerChildren.InsertAtTop(blur); - containerChildren.InsertAtTop(visual); - - return new WinUICompositedWindow(_syncContext, _compositor, _pumpLock, target, surfaceInterop, visual, - blur, mica, compositionRoundedRectangleGeometry); - } - - private ICompositionBrush CreateMicaBackdropBrush() - { - if (Win32Platform.WindowsVersion.Build < 22000) - return null; - - using var compositorWithBlurredWallpaperBackdropBrush = - _compositor.QueryInterface(); - using var blurredWallpaperBackdropBrush = - compositorWithBlurredWallpaperBackdropBrush?.TryCreateBlurredWallpaperBackdropBrush(); - using var micaBackdropBrush = blurredWallpaperBackdropBrush?.QueryInterface(); - return micaBackdropBrush.CloneReference(); - } - - private unsafe ICompositionBrush CreateAcrylicBlurBackdropBrush() - { - using var backDropParameterFactory = NativeWinRTMethods.CreateActivationFactory( - "Windows.UI.Composition.CompositionEffectSourceParameter"); - using var backdropString = new HStringInterop("backdrop"); - using var backDropParameter = - backDropParameterFactory.Create(backdropString.Handle); - using var backDropParameterAsSource = backDropParameter.QueryInterface(); - var blurEffect = new WinUIGaussianBlurEffect(backDropParameterAsSource); - using var blurEffectFactory = _compositor.CreateEffectFactory(blurEffect); - using var compositionEffectBrush = blurEffectFactory.CreateBrush(); - using var backdropBrush = CreateBackdropBrush(); - - var saturateEffect = new SaturationEffect(blurEffect); - using var satEffectFactory = _compositor.CreateEffectFactory(saturateEffect); - using var sat = satEffectFactory.CreateBrush(); - compositionEffectBrush.SetSourceParameter(backdropString.Handle, backdropBrush); - return compositionEffectBrush.QueryInterface(); - } - - private ICompositionRoundedRectangleGeometry ClipVisual(params IVisual[] containerVisuals) - { - if (!_backdropCornerRadius.HasValue) - return null; - using var roundedRectangleGeometry = _compositor5.CreateRoundedRectangleGeometry(); - roundedRectangleGeometry.SetCornerRadius(new Vector2(_backdropCornerRadius.Value, _backdropCornerRadius.Value)); - - using var compositor6 = _compositor.QueryInterface(); - using var compositionGeometry = roundedRectangleGeometry - .QueryInterface(); - - using var geometricClipWithGeometry = - compositor6.CreateGeometricClipWithGeometry(compositionGeometry); - foreach (var visual in containerVisuals) - { - visual?.SetClip(geometricClipWithGeometry.QueryInterface()); - } - - return roundedRectangleGeometry.CloneReference(); - } - - private unsafe IVisual CreateBlurVisual(ICompositionBrush compositionBrush) - { - using var spriteVisual = _compositor.CreateSpriteVisual(); - using var visual = spriteVisual.QueryInterface(); - using var visual2 = spriteVisual.QueryInterface(); - - - spriteVisual.SetBrush(compositionBrush); - visual.SetIsVisible(0); - visual2.SetRelativeSizeAdjustment(new Vector2(1.0f, 1.0f)); - - return visual.CloneReference(); - } - - private ICompositionBrush CreateBackdropBrush() - { - ICompositionBackdropBrush brush = null; - try - { - if (Win32Platform.WindowsVersion >= MinHostBackdropVersion) - { - using var compositor3 = _compositor.QueryInterface(); - brush = compositor3.CreateHostBackdropBrush(); - } - else - { - using var compositor2 = _compositor.QueryInterface(); - brush = compositor2.CreateBackdropBrush(); - } - - return brush.QueryInterface(); - } - finally - { - brush?.Dispose(); - } - } - - public event Action Tick; - public bool RunsInBackground => true; - } -} diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs new file mode 100644 index 0000000000..5be25a3d4b --- /dev/null +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs @@ -0,0 +1,110 @@ +using System; +using System.Numerics; +using System.Threading; +using Avalonia.OpenGL.Egl; +using Avalonia.Reactive; +using Avalonia.Win32.Interop; +using MicroCom.Runtime; + +namespace Avalonia.Win32.WinRT.Composition; + +internal class WinUiCompositedWindow : IDisposable +{ + public EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo WindowInfo { get; } + private readonly WinUiCompositionShared _shared; + private readonly ICompositionRoundedRectangleGeometry _compositionRoundedRectangleGeometry; + private readonly IVisual _mica; + private readonly IVisual _blur; + private readonly IVisual _visual; + private PixelSize _size; + private readonly ICompositionSurfaceBrush _surfaceBrush; + private readonly ICompositionTarget _target; + + public void Dispose() + { + lock (_shared.SyncRoot) + { + _compositionRoundedRectangleGeometry?.Dispose(); + _blur?.Dispose(); + _mica?.Dispose(); + _visual.Dispose(); + _surfaceBrush.Dispose(); + _target.Dispose(); + } + } + + public WinUiCompositedWindow(EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info, + WinUiCompositionShared shared, float? backdropCornerRadius) + { + WindowInfo = info; + _shared = shared; + using var desktopTarget = shared.DesktopInterop.CreateDesktopWindowTarget(WindowInfo.Handle, 0); + _target = desktopTarget.QueryInterface(); + + + using var container = shared.Compositor.CreateContainerVisual(); + using var containerVisual = container.QueryInterface(); + using var containerVisual2 = container.QueryInterface(); + containerVisual2.SetRelativeSizeAdjustment(new Vector2(1, 1)); + using var containerChildren = container.Children; + + _target.SetRoot(containerVisual); + + _blur = WinUiCompositionUtils.CreateBlurVisual(shared.Compositor, shared.BlurBrush); + if (shared.MicaBrush != null) + { + _mica = WinUiCompositionUtils.CreateBlurVisual(shared.Compositor, shared.MicaBrush); + containerChildren.InsertAtTop(_mica); + } + + _compositionRoundedRectangleGeometry = + WinUiCompositionUtils.ClipVisual(shared.Compositor, backdropCornerRadius, _blur, _mica); + + containerChildren.InsertAtTop(_blur); + using var spriteVisual = shared.Compositor.CreateSpriteVisual(); + _visual = spriteVisual.QueryInterface(); + containerChildren.InsertAtTop(_visual); + + _surfaceBrush = shared.Compositor.CreateSurfaceBrush(); + using var compositionBrush = _surfaceBrush.QueryInterface(); + spriteVisual.SetBrush(compositionBrush); + _target.SetRoot(containerVisual); + + + + } + + public void SetSurface(ICompositionSurface surface) => _surfaceBrush.SetSurface(surface); + + public void SetBlur(BlurEffect blurEffect) + { + lock (_shared.SyncRoot) + { + + _blur.SetIsVisible(blurEffect == BlurEffect.Acrylic + || blurEffect == BlurEffect.Mica && _mica == null ? + 1 : + 0); + _mica?.SetIsVisible(blurEffect == BlurEffect.Mica ? 1 : 0); + } + } + + public IDisposable BeginTransaction() + { + Monitor.Enter(_shared.SyncRoot); + return Disposable.Create(() => Monitor.Exit(_shared.SyncRoot)); + } + + public void ResizeIfNeeded(PixelSize size) + { + lock (_shared.SyncRoot) + { + if (_size != size) + { + _visual.SetSize(new Vector2(size.Width, size.Height)); + _compositionRoundedRectangleGeometry?.SetSize(new Vector2(size.Width, size.Height)); + _size = size; + } + } + } +} diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs index fc04dcda26..9ee024f9ad 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs @@ -1,119 +1,203 @@ using System; using System.Runtime.InteropServices; using Avalonia.MicroCom; +using Avalonia.OpenGL; using Avalonia.OpenGL.Angle; using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; +using Avalonia.Platform; using Avalonia.Utilities; +using Avalonia.Win32.DirectX; using Avalonia.Win32.Interop; +using Avalonia.Win32.OpenGl.Angle; using MicroCom.Runtime; namespace Avalonia.Win32.WinRT.Composition { - internal class WinUiCompositedWindowSurface : EglGlPlatformSurfaceBase, IBlurHost, IDisposable + internal class WinUiCompositedWindowSurface : IDirect3D11TexturePlatformSurface, IDisposable, IBlurHost { - private readonly WinUICompositorConnection _connection; - private EglPlatformOpenGlInterface _egl; - private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info; - private IRef _window; + private readonly WinUiCompositionShared _shared; + private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info; + private WinUiCompositedWindow _window; private BlurEffect _blurEffect; - public WinUiCompositedWindowSurface(WinUICompositorConnection connection, IEglWindowGlPlatformSurfaceInfo info) : base() + public WinUiCompositedWindowSurface(WinUiCompositionShared shared, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) { - _connection = connection; - _egl = connection.Egl; + _shared = shared; _info = info; } - public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public IDirect3D11TextureRenderTarget CreateRenderTarget(IPlatformGraphicsContext context, IntPtr d3dDevice) { - using (_egl.PrimaryContext.EnsureCurrent()) - { - if (_window?.Item == null) - { - _window = RefCountable.Create(_connection.CreateWindow(_info.Handle)); - _window.Item.SetBlur(_blurEffect); - } + var cornerRadius = AvaloniaLocator.Current.GetService() + ?.CompositionBackdropCornerRadius; + _window ??= new WinUiCompositedWindow(_info, _shared, cornerRadius); + _window.SetBlur(_blurEffect); - return new CompositionRenderTarget(_egl, _window, _info); - } + return new WinUiCompositedWindowRenderTarget(context, _window, d3dDevice, _shared.Compositor); + } + + public void Dispose() + { + _window?.Dispose(); + _window = null; + } + + public void SetBlur(BlurEffect enable) + { + _blurEffect = enable; + _window?.SetBlur(enable); } + } - class CompositionRenderTarget : EglPlatformSurfaceRenderTargetBase + internal class WinUiCompositedWindowRenderTarget : IDirect3D11TextureRenderTarget + { + private readonly IPlatformGraphicsContext _context; + private readonly WinUiCompositedWindow _window; + private readonly IUnknown _d3dDevice; + private readonly ICompositor _compositor; + private readonly ICompositorInterop _interop; + private readonly ICompositionGraphicsDevice _compositionDevice; + private readonly ICompositionGraphicsDevice2 _compositionDevice2; + private readonly ICompositionSurface _surface; + private PixelSize _size; + private bool _lost; + private readonly ICompositionDrawingSurfaceInterop _surfaceInterop; + + public WinUiCompositedWindowRenderTarget(IPlatformGraphicsContext context, + WinUiCompositedWindow window, IntPtr device, + ICompositor compositor) { - private readonly EglPlatformOpenGlInterface _egl; - private readonly IRef _window; - private readonly IEglWindowGlPlatformSurfaceInfo _info; - - public CompositionRenderTarget(EglPlatformOpenGlInterface egl, - IRef window, - IEglWindowGlPlatformSurfaceInfo info) - : base(egl) + _context = context; + _window = window; + + try { - _egl = egl; - _window = window.Clone(); - _info = info; - _window.Item.ResizeIfNeeded(_info.Size); + _d3dDevice = MicroComRuntime.CreateProxyFor(device, false).CloneReference(); + _compositor = compositor.CloneReference(); + _interop = compositor.QueryInterface(); + _compositionDevice = _interop.CreateGraphicsDevice(_d3dDevice); + _compositionDevice2 = _compositionDevice.QueryInterface(); + _drawingSurface = _compositionDevice2.CreateDrawingSurface2(new UnmanagedMethods.SIZE(), + DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied); + _surface = _drawingSurface.QueryInterface(); + _surfaceInterop = _drawingSurface.QueryInterface(); } + finally + { + if (_surfaceInterop == null) + Dispose(); + } + } - public override IGlPlatformSurfaceRenderingSession BeginDraw() + public void Dispose() + { + _surface?.Dispose(); + _surfaceInterop?.Dispose(); + _drawingSurface?.Dispose(); + _compositionDevice2?.Dispose(); + _compositionDevice?.Dispose(); + _interop?.Dispose(); + _compositor?.Dispose(); + _d3dDevice?.Dispose(); + } + + public bool IsCorrupted => _context.IsLost || _lost; + private static Guid IID_ID3D11Texture2D = Guid.Parse("6f15aaf2-d208-4e89-9ab4-489535d34f9c"); + private readonly ICompositionDrawingSurface _drawingSurface; + + public unsafe IDirect3D11TextureRenderTargetRenderSession BeginDraw() + { + if (IsCorrupted) + throw new RenderTargetCorruptedException(); + var transaction = _window.BeginTransaction(); + bool needsEndDraw = false; + try { - var contextLock = _egl.PrimaryEglContext.EnsureCurrent(); - IUnknown texture = null; - EglSurface surface = null; - IDisposable transaction = null; - var success = false; + var size = _window.WindowInfo.Size; + var scale = _window.WindowInfo.Scaling; + _window.ResizeIfNeeded(size); + _window.SetSurface(_surface); + + void* pTexture; + UnmanagedMethods.POINT off; try { - if (_window?.Item == null) - throw new ObjectDisposedException(GetType().FullName); - - var size = _info.Size; - transaction = _window.Item.BeginTransaction(); - _window.Item.ResizeIfNeeded(size); - texture = _window.Item.BeginDrawToTexture(out var offset); - - surface = ((AngleWin32EglDisplay) _egl.Display).WrapDirect3D11Texture(_egl, - texture.GetNativeIntPtr(), - offset.X, offset.Y, size.Width, size.Height); - - var res = base.BeginDraw(surface, _info, () => + if (_size != size) { - surface?.Dispose(); - texture?.Dispose(); - _window.Item.EndDraw(); - transaction?.Dispose(); - contextLock?.Dispose(); - }, true); - success = true; - return res; + _surfaceInterop.Resize(new UnmanagedMethods.POINT + { + X = size.Width, + Y = size.Height + }); + _size = size; + } + var iid = IID_ID3D11Texture2D; + off = _surfaceInterop.BeginDraw(null, &iid, &pTexture); } - finally + catch (Exception e) { - if (!success) - { - surface?.Dispose(); - texture?.Dispose(); - transaction?.Dispose(); - contextLock.Dispose(); - } + _lost = true; + throw new RenderTargetCorruptedException(e); + } + + needsEndDraw = true; + var offset = new PixelPoint(off.X, off.Y); + using var texture = MicroComRuntime.CreateProxyFor(pTexture, true); + + var session = new Session(_surfaceInterop, texture, transaction, _size, offset, scale); + transaction = null; + return session; + } + finally + { + if (transaction != null) + { + if (needsEndDraw) + _surfaceInterop.EndDraw(); + transaction?.Dispose(); } } } - public void SetBlur(BlurEffect blurEffect) + class Session : IDirect3D11TextureRenderTargetRenderSession { - _blurEffect = blurEffect; - _window?.Item?.SetBlur(blurEffect); - } + private readonly IDisposable _transaction; + private readonly PixelSize _size; + private readonly PixelPoint _offset; + private readonly double _scaling; + private readonly ICompositionDrawingSurfaceInterop _surfaceInterop; + private readonly IUnknown _texture; - public void Dispose() - { - using (_egl.PrimaryEglContext.EnsureLocked()) + public Session(ICompositionDrawingSurfaceInterop surfaceInterop, IUnknown texture, IDisposable transaction, + PixelSize size, PixelPoint offset, double scaling) { - _window?.Dispose(); - _window = null; + _transaction = transaction; + _size = size; + _offset = offset; + _scaling = scaling; + _surfaceInterop = surfaceInterop.CloneReference(); + _texture = texture.CloneReference(); } + + public void Dispose() + { + try + { + _texture.Dispose(); + _surfaceInterop.EndDraw(); + _surfaceInterop.Dispose(); + } + finally + { + _transaction.Dispose(); + } + } + + public IntPtr D3D11Texture2D => _texture.GetNativeIntPtr(); + public PixelSize Size => _size; + public PixelPoint Offset => _offset; + public double Scaling => _scaling; } } -} +} \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionShared.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionShared.cs new file mode 100644 index 0000000000..301e605e7a --- /dev/null +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionShared.cs @@ -0,0 +1,34 @@ +using System; +using MicroCom.Runtime; + +namespace Avalonia.Win32.WinRT.Composition; + +internal class WinUiCompositionShared : IDisposable +{ + public ICompositor Compositor { get; } + public ICompositor5 Compositor5 { get; } + public ICompositorDesktopInterop DesktopInterop { get; } + public ICompositionBrush BlurBrush; + public ICompositionBrush MicaBrush; + public object SyncRoot { get; } = new(); + + public static readonly Version MinHostBackdropVersion = new Version(10, 0, 22000); + + public WinUiCompositionShared(ICompositor compositor) + { + Compositor = compositor.CloneReference(); + Compositor5 = compositor.QueryInterface(); + BlurBrush = WinUiCompositionUtils.CreateAcrylicBlurBackdropBrush(compositor); + MicaBrush = WinUiCompositionUtils.CreateMicaBackdropBrush(compositor); + DesktopInterop = compositor.QueryInterface(); + } + + public void Dispose() + { + BlurBrush.Dispose(); + MicaBrush.Dispose(); + DesktopInterop.Dispose(); + Compositor.Dispose(); + Compositor5.Dispose(); + } +} \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionUtils.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionUtils.cs new file mode 100644 index 0000000000..e7af4046d6 --- /dev/null +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionUtils.cs @@ -0,0 +1,101 @@ +using System; +using System.Numerics; +using MicroCom.Runtime; + +namespace Avalonia.Win32.WinRT.Composition; + +internal static class WinUiCompositionUtils +{ + public static ICompositionBrush CreateMicaBackdropBrush(ICompositor compositor) + { + if (Win32Platform.WindowsVersion.Build < 22000) + return null; + + using var compositorWithBlurredWallpaperBackdropBrush = + compositor.QueryInterface(); + using var blurredWallpaperBackdropBrush = + compositorWithBlurredWallpaperBackdropBrush?.TryCreateBlurredWallpaperBackdropBrush(); + return blurredWallpaperBackdropBrush?.QueryInterface(); + } + + public static unsafe ICompositionBrush CreateAcrylicBlurBackdropBrush(ICompositor compositor) + { + using var backDropParameterFactory = + NativeWinRTMethods.CreateActivationFactory( + "Windows.UI.Composition.CompositionEffectSourceParameter"); + using var backdropString = new HStringInterop("backdrop"); + using var backDropParameter = + backDropParameterFactory.Create(backdropString.Handle); + using var backDropParameterAsSource = backDropParameter.QueryInterface(); + var blurEffect = new WinUIGaussianBlurEffect(backDropParameterAsSource); + using var blurEffectFactory = compositor.CreateEffectFactory(blurEffect); + using var compositionEffectBrush = blurEffectFactory.CreateBrush(); + using var backdropBrush = CreateBackdropBrush(compositor); + + var saturateEffect = new SaturationEffect(blurEffect); + using var satEffectFactory = compositor.CreateEffectFactory(saturateEffect); + using var sat = satEffectFactory.CreateBrush(); + compositionEffectBrush.SetSourceParameter(backdropString.Handle, backdropBrush); + return compositionEffectBrush.QueryInterface(); + } + + public static ICompositionRoundedRectangleGeometry ClipVisual(ICompositor compositor, float? _backdropCornerRadius, params IVisual[] containerVisuals) + { + if (!_backdropCornerRadius.HasValue) + return null; + using var compositor5 = compositor.QueryInterface(); + using var roundedRectangleGeometry = compositor5.CreateRoundedRectangleGeometry(); + roundedRectangleGeometry.SetCornerRadius(new Vector2(_backdropCornerRadius.Value, _backdropCornerRadius.Value)); + + using var compositor6 = compositor.QueryInterface(); + using var compositionGeometry = roundedRectangleGeometry + .QueryInterface(); + + using var geometricClipWithGeometry = + compositor6.CreateGeometricClipWithGeometry(compositionGeometry); + foreach (var visual in containerVisuals) + { + visual?.SetClip(geometricClipWithGeometry.QueryInterface()); + } + + return roundedRectangleGeometry.CloneReference(); + } + + public static IVisual CreateBlurVisual(ICompositor compositor, ICompositionBrush compositionBrush) + { + using var spriteVisual = compositor.CreateSpriteVisual(); + using var visual = spriteVisual.QueryInterface(); + using var visual2 = spriteVisual.QueryInterface(); + + + spriteVisual.SetBrush(compositionBrush); + visual.SetIsVisible(0); + visual2.SetRelativeSizeAdjustment(new Vector2(1.0f, 1.0f)); + + return visual.CloneReference(); + } + + public static ICompositionBrush CreateBackdropBrush(ICompositor compositor) + { + ICompositionBackdropBrush brush = null; + try + { + if (Win32Platform.WindowsVersion >= WinUiCompositionShared.MinHostBackdropVersion) + { + using var compositor3 = compositor.QueryInterface(); + brush = compositor3.CreateHostBackdropBrush(); + } + else + { + using var compositor2 = compositor.QueryInterface(); + brush = compositor2.CreateBackdropBrush(); + } + + return brush.QueryInterface(); + } + finally + { + brush?.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs new file mode 100644 index 0000000000..a1408baae0 --- /dev/null +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs @@ -0,0 +1,151 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Logging; +using Avalonia.MicroCom; +using Avalonia.OpenGL.Egl; +using Avalonia.Rendering; +using Avalonia.Win32.DirectX; +using Avalonia.Win32.Interop; +using Avalonia.Win32.OpenGl.Angle; +using MicroCom.Runtime; + +namespace Avalonia.Win32.WinRT.Composition; + +internal class WinUiCompositorConnection : IRenderTimer +{ + private readonly WinUiCompositionShared _shared; + public event Action Tick; + public bool RunsInBackground => true; + + public unsafe WinUiCompositorConnection() + { + using var compositor = NativeWinRTMethods.CreateInstance("Windows.UI.Composition.Compositor"); + /* + var levels = new[] { D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1 }; + DirectXUnmanagedMethods.D3D11CreateDevice(IntPtr.Zero, D3D_DRIVER_TYPE.D3D_DRIVER_TYPE_HARDWARE, + IntPtr.Zero, 0, levels, (uint)levels.Length, 7, out var pD3dDevice, out var level, null); + + var d3dDevice = MicroComRuntime.CreateProxyFor(pD3dDevice, true); + + var compositionDevice = compositor.QueryInterface().CreateGraphicsDevice(d3dDevice); + var surf = compositionDevice.CreateDrawingSurface(new UnmanagedMethods.SIZE_F { X = 100, Y = 100 }, + DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied); + var surfInterop = surf.QueryInterface(); + var IID_ID3D11Texture2D = Guid.Parse("6f15aaf2-d208-4e89-9ab4-489535d34f9c"); + void* texture = null; + surfInterop.BeginDraw(null, &IID_ID3D11Texture2D, &texture); + */ + + _shared = new WinUiCompositionShared(compositor); + } + + static bool TryCreateAndRegisterCore() + { + var tcs = new TaskCompletionSource(); + var pumpLock = new object(); + var th = new Thread(() => + { + WinUiCompositorConnection connect; + try + { + NativeWinRTMethods.CreateDispatcherQueueController(new NativeWinRTMethods.DispatcherQueueOptions + { + apartmentType = NativeWinRTMethods.DISPATCHERQUEUE_THREAD_APARTMENTTYPE.DQTAT_COM_NONE, + dwSize = Marshal.SizeOf(), + threadType = NativeWinRTMethods.DISPATCHERQUEUE_THREAD_TYPE.DQTYPE_THREAD_CURRENT + }); + connect = new WinUiCompositorConnection(); + AvaloniaLocator.CurrentMutable.BindToSelf(connect); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect); + tcs.SetResult(true); + + } + catch (Exception e) + { + tcs.SetException(e); + return; + } + + connect.RunLoop(); + }) + { + IsBackground = true, + Name = "DwmRenderTimerLoop" + }; + th.SetApartmentState(ApartmentState.STA); + th.Start(); + return tcs.Task.Result; + } + + class RunLoopHandler : CallbackBase, IAsyncActionCompletedHandler + { + private readonly WinUiCompositorConnection _parent; + private Stopwatch _st = Stopwatch.StartNew(); + + public RunLoopHandler(WinUiCompositorConnection parent) + { + _parent = parent; + } + + public void Invoke(IAsyncAction asyncInfo, AsyncStatus asyncStatus) + { + _parent.Tick?.Invoke(_st.Elapsed); + using var act = _parent._shared.Compositor5.RequestCommitAsync(); + act.SetCompleted(this); + } + } + + private void RunLoop() + { + var cts = new CancellationTokenSource(); + AppDomain.CurrentDomain.ProcessExit += (sender, args) => + cts.Cancel(); + + lock (_shared.SyncRoot) + using (var act = _shared.Compositor5.RequestCommitAsync()) + act.SetCompleted(new RunLoopHandler(this)); + + while (!cts.IsCancellationRequested) + { + UnmanagedMethods.GetMessage(out var msg, IntPtr.Zero, 0, 0); + lock (_shared.SyncRoot) + UnmanagedMethods.DispatchMessage(ref msg); + } + } + + public static void TryCreateAndRegister() + { + const int majorRequired = 10; + const int buildRequired = 17134; + + var majorInstalled = Win32Platform.WindowsVersion.Major; + var buildInstalled = Win32Platform.WindowsVersion.Build; + + if (majorInstalled >= majorRequired && + buildInstalled >= buildRequired) + { + try + { + TryCreateAndRegisterCore(); + return; + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "WinUIComposition") + ?.Log(null, "Unable to initialize WinUI compositor: {0}", e); + + } + } + + var osVersionNotice = + $"Windows {majorRequired} Build {buildRequired} is required. Your machine has Windows {majorInstalled} Build {buildInstalled} installed."; + + Logger.TryGet(LogEventLevel.Warning, "WinUIComposition")?.Log(null, + $"Unable to initialize WinUI compositor: {osVersionNotice}"); + } + + public WinUiCompositedWindowSurface CreateSurface(EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) => new(_shared, info); +} diff --git a/src/Windows/Avalonia.Win32/WinRT/WinRTColor.cs b/src/Windows/Avalonia.Win32/WinRT/WinRTColor.cs index 786d698daa..35f0737d5f 100644 --- a/src/Windows/Avalonia.Win32/WinRT/WinRTColor.cs +++ b/src/Windows/Avalonia.Win32/WinRT/WinRTColor.cs @@ -3,7 +3,7 @@ namespace Avalonia.Win32.WinRT { [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct WinRTColor + public record struct WinRTColor { public byte A; public byte R; 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 5f813eec1d..dbcc31c5f5 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -26,7 +26,8 @@ using static Avalonia.Win32.Interop.UnmanagedMethods; using Avalonia.Collections.Pooled; using Avalonia.Metadata; using Avalonia.Platform.Storage; -using Avalonia.Win32.DxgiSwapchain; +using Avalonia.Win32.DirectX; +using Avalonia.Win32.OpenGl.Angle; namespace Avalonia.Win32 { @@ -64,7 +65,6 @@ namespace Avalonia.Win32 private Thickness _offScreenMargin; private double _extendTitleBarHint = -1; private bool _isUsingComposition; - private bool _isUsingDxgiSwapchain; private IBlurHost _blurHost; private PlatformResizeReason _resizeReason; private MOUSEMOVEPOINT _lastWmMousePoint; @@ -79,7 +79,7 @@ namespace Avalonia.Win32 private readonly PenDevice _penDevice; private readonly ManagedDeferredRendererLock _rendererLock; private readonly FramebufferManager _framebuffer; - private readonly IGlPlatformSurface _gl; + private readonly object _gl; private readonly bool _wmPointerEnabled; private Win32NativeControlHost _nativeControlHost; @@ -136,23 +136,20 @@ namespace Avalonia.Win32 }; _rendererLock = new ManagedDeferredRendererLock(); - var glPlatform = AvaloniaLocator.Current.GetService(); + var glPlatform = AvaloniaLocator.Current.GetService(); - var compositionConnector = AvaloniaLocator.Current.GetService(); + var compositionConnector = AvaloniaLocator.Current.GetService(); - _isUsingComposition = compositionConnector is { } && - glPlatform is EglPlatformOpenGlInterface egl && - egl.Display is AngleWin32EglDisplay angleDisplay && - angleDisplay.PlatformApi == AngleOptions.PlatformApi.DirectX11; + var isUsingAngleDX11 = glPlatform is AngleWin32PlatformGraphics angle && + angle.PlatformApi == AngleOptions.PlatformApi.DirectX11; + _isUsingComposition = compositionConnector is { } && isUsingAngleDX11; DxgiConnection dxgiConnection = null; + var isUsingDxgiSwapchain = false; if (!_isUsingComposition) { dxgiConnection = AvaloniaLocator.Current.GetService(); - _isUsingDxgiSwapchain = dxgiConnection is { } && - glPlatform is EglPlatformOpenGlInterface eglDxgi && - eglDxgi.Display is AngleWin32EglDisplay angleDisplayDxgi && - angleDisplayDxgi.PlatformApi == AngleOptions.PlatformApi.DirectX11; + isUsingDxgiSwapchain = dxgiConnection is { } && isUsingAngleDX11; } _wmPointerEnabled = Win32Platform.WindowsVersion >= PlatformConstants.Windows8; @@ -164,24 +161,24 @@ namespace Avalonia.Win32 { if (_isUsingComposition) { - var cgl = new WinUiCompositedWindowSurface(compositionConnector, this); + var cgl = compositionConnector.CreateSurface(this); _blurHost = cgl; _gl = cgl; _isUsingComposition = true; } - else if (_isUsingDxgiSwapchain) + else if (isUsingDxgiSwapchain) { var dxgigl = new DxgiSwapchainWindow(dxgiConnection, this); _gl = dxgigl; } else { - if (glPlatform is EglPlatformOpenGlInterface egl2) - _gl = new EglGlPlatformSurface(egl2, this); + if (glPlatform is AngleWin32PlatformGraphics egl2) + _gl = new EglGlPlatformSurface(this); else if (glPlatform is WglPlatformOpenGlInterface wgl) - _gl = new WglGlPlatformSurface(wgl.PrimaryContext, this); + _gl = new WglGlPlatformSurface(this); } } @@ -194,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; } @@ -434,7 +431,7 @@ namespace Avalonia.Win32 _ => BlurEffect.None }; - if (Win32Platform.WindowsVersion >= WinUICompositorConnection.MinHostBackdropVersion) + if (Win32Platform.WindowsVersion >= WinUiCompositionShared.MinHostBackdropVersion) { unsafe { @@ -443,7 +440,7 @@ namespace Avalonia.Win32 } } - if (Win32Platform.WindowsVersion < WinUICompositorConnection.MinHostBackdropVersion && effect == BlurEffect.Mica) + if (Win32Platform.WindowsVersion < WinUiCompositionShared.MinHostBackdropVersion && effect == BlurEffect.Mica) { effect = BlurEffect.Acrylic; } @@ -570,16 +567,22 @@ namespace Avalonia.Win32 return customRendererFactory.Create(root, loop); if (Win32Platform.Compositor != null) - return new CompositingRenderer(root, Win32Platform.Compositor); - + return new CompositingRenderer(root, Win32Platform.Compositor, () => Surfaces); + return Win32Platform.UseDeferredRendering ? _isUsingComposition - ? new DeferredRenderer(root, loop) + ? new DeferredRenderer(root, loop, + () => Win32Platform.RenderInterface.CreateRenderTarget(Surfaces), + Win32Platform.RenderInterface) { RenderOnlyOnRenderThread = true } - : (IRenderer)new DeferredRenderer(root, loop, rendererLock: _rendererLock) - : new ImmediateRenderer((Visual)root); + : (IRenderer)new DeferredRenderer(root, loop, rendererLock: _rendererLock, + renderTargetFactory: () => Win32Platform.RenderInterface.CreateRenderTarget(Surfaces), + renderInterface: Win32Platform.RenderInterface) + : new ImmediateRenderer((Visual)root, + () => Win32Platform.RenderInterface.CreateRenderTarget(Surfaces), + Win32Platform.RenderInterface); } public void Resize(Size value, PlatformResizeReason reason) @@ -640,12 +643,6 @@ namespace Avalonia.Win32 _hwnd = IntPtr.Zero; } - if (_className != null) - { - UnregisterClass(_className, GetModuleHandle(null)); - _className = null; - } - _framebuffer.Dispose(); } @@ -1032,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 { @@ -1042,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) && @@ -1141,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); @@ -1340,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/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index 2080352020..48358c745f 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -70,7 +70,8 @@ namespace Avalonia.iOS // No-op } - public IRenderer CreateRenderer(IRenderRoot root) => new CompositingRenderer(root, Platform.Compositor); + public IRenderer CreateRenderer(IRenderRoot root) => + new CompositingRenderer(root, Platform.Compositor, () => Surfaces); public void Invalidate(Rect rect) diff --git a/src/iOS/Avalonia.iOS/EaglDisplay.cs b/src/iOS/Avalonia.iOS/EaglDisplay.cs index 906bbc29e7..8f7c1f3308 100644 --- a/src/iOS/Avalonia.iOS/EaglDisplay.cs +++ b/src/iOS/Avalonia.iOS/EaglDisplay.cs @@ -1,34 +1,43 @@ using System; -using System.Reactive.Disposables; +using System.Collections.Generic; using Avalonia.OpenGL; using Avalonia.Platform; +using Avalonia.Reactive; using OpenGLES; namespace Avalonia.iOS { - class EaglFeature : IPlatformOpenGlInterface + class EaglPlatformGraphics : IPlatformGraphics { - public IGlContext PrimaryContext => Context; - public IGlContext CreateSharedContext() => throw new NotSupportedException(); - IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; - public bool CanShareContexts => false; - public bool CanCreateContexts => false; - public IGlContext CreateContext() => throw new System.NotSupportedException(); - public GlContext Context { get; } = new GlContext(); + public IPlatformGraphicsContext GetSharedContext() => Context; + + public bool UsesSharedContext => true; + public IPlatformGraphicsContext CreateContext() => throw new System.NotSupportedException(); + public GlContext Context { get; } + public static GlVersion GlVersion { get; } = new(GlProfileType.OpenGLES, 3, 0); + + public EaglPlatformGraphics() + { + + const string path = "/System/Library/Frameworks/OpenGLES.framework/OpenGLES"; + var libGl = ObjCRuntime.Dlfcn.dlopen(path, 1); + if (libGl == IntPtr.Zero) + throw new OpenGlException("Unable to load " + path); + var iface = new GlInterface(GlVersion, proc => ObjCRuntime.Dlfcn.dlsym(libGl, proc)); + Context = new(iface, null); + } } class GlContext : IGlContext { public EAGLContext Context { get; private set; } - public GlContext() + public GlContext(GlInterface glInterface, EAGLSharegroup sharegroup) { - const string path = "/System/Library/Frameworks/OpenGLES.framework/OpenGLES"; - var libGl = ObjCRuntime.Dlfcn.dlopen(path, 1); - if (libGl == IntPtr.Zero) - throw new OpenGlException("Unable to load " + path); - GlInterface = new GlInterface(Version, proc => ObjCRuntime.Dlfcn.dlsym(libGl, proc)); - Context = new EAGLContext(EAGLRenderingAPI.OpenGLES3); + GlInterface = glInterface; + Context = sharegroup == null ? + new EAGLContext(EAGLRenderingAPI.OpenGLES3) : + new(EAGLRenderingAPI.OpenGLES3, sharegroup); } public void Dispose() @@ -59,22 +68,34 @@ namespace Avalonia.iOS public IDisposable MakeCurrent() { + if (Context == null) + throw new PlatformGraphicsContextLostException(); var old = EAGLContext.CurrentContext; if (!EAGLContext.SetCurrentContext(Context)) throw new OpenGlException("Unable to make context current"); return new ResetContext(old); } + public bool IsLost => Context == null; + public IDisposable EnsureCurrent() { + if (Context == null) + throw new PlatformGraphicsContextLostException(); if(EAGLContext.CurrentContext == Context) return Disposable.Empty; return MakeCurrent(); } - public bool IsSharedWith(IGlContext context) => false; + public bool IsSharedWith(IGlContext context) => context is GlContext other + && ReferenceEquals(other.Context?.ShareGroup, Context?.ShareGroup); + public bool CanCreateSharedContext => true; + public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) + { + return new GlContext(GlInterface, Context.ShareGroup); + } - public GlVersion Version { get; } = new GlVersion(GlProfileType.OpenGLES, 3, 0); + public GlVersion Version => EaglPlatformGraphics.GlVersion; public GlInterface GlInterface { get; } public int SampleCount { @@ -92,5 +113,7 @@ namespace Avalonia.iOS return stencil; } } + + public object TryGetFeature(Type featureType) => null; } } diff --git a/src/iOS/Avalonia.iOS/EaglLayerSurface.cs b/src/iOS/Avalonia.iOS/EaglLayerSurface.cs index 0e8945d921..88812653d7 100644 --- a/src/iOS/Avalonia.iOS/EaglLayerSurface.cs +++ b/src/iOS/Avalonia.iOS/EaglLayerSurface.cs @@ -78,10 +78,12 @@ namespace Avalonia.iOS throw new InvalidOperationException("Invalid thread, go away"); } - public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { CheckThread(); var ctx = Platform.GlFeature.Context; + if (ctx != context) + throw new InvalidOperationException("Platform surface is only usable with tha main context"); using (ctx.MakeCurrent()) { var fbo = new SizeSynchronizedLayerFbo(ctx.Context, ctx.GlInterface, _layer); diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index c2cf8c6f6c..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") @@ -25,18 +25,18 @@ namespace Avalonia.iOS { static class Platform { - public static EaglFeature GlFeature; + public static EaglPlatformGraphics GlFeature; public static DisplayLinkTimer Timer; internal static Compositor Compositor { get; private set; } public static void Register() { - GlFeature ??= new EaglFeature(); + GlFeature ??= new EaglPlatformGraphics(); Timer ??= new DisplayLinkTimer(); var keyboard = new KeyboardDevice(); AvaloniaLocator.CurrentMutable - .Bind().ToConstant(GlFeature) + .Bind().ToConstant(GlFeature) .Bind().ToConstant(new CursorFactoryStub()) .Bind().ToConstant(new WindowingPlatformStub()) .Bind().ToConstant(new ClipboardImpl()) @@ -50,7 +50,7 @@ namespace Avalonia.iOS Compositor = new Compositor( AvaloniaLocator.Current.GetRequiredService(), - AvaloniaLocator.Current.GetService()); + AvaloniaLocator.Current.GetService()); } 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/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..84ee35ba61 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_Exceded() + { + 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 7b987d8e68..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; @@ -76,7 +75,7 @@ public class CompositorTestsBase public IRenderer CreateRenderer(IRenderRoot root) { - return Renderer = new CompositingRenderer(root, _compositor); + return Renderer = new CompositingRenderer(root, _compositor, () => Surfaces); } public void Invalidate(Rect rect) diff --git a/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests.cs index 08e5955ec5..f0c5a24cc4 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests.cs @@ -46,6 +46,7 @@ namespace Avalonia.Base.UnitTests.Rendering var target = new DeferredRenderer( root, loop.Object, + renderTargetFactory: root.CreateRenderTarget, sceneBuilder: sceneBuilder.Object); target.Start(); @@ -83,6 +84,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder.Object, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); target.Start(); @@ -133,6 +135,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; @@ -178,6 +181,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; @@ -223,6 +227,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; @@ -270,6 +275,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; @@ -317,6 +323,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; @@ -366,6 +373,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; @@ -415,6 +423,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); var otherSceneBuilder = new SceneBuilder(); @@ -423,6 +432,7 @@ namespace Avalonia.Base.UnitTests.Rendering otherRoot, loop.Object, sceneBuilder: otherSceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; @@ -738,6 +748,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, new RenderLoop(timer.Object, dispatcher), sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; target.Start(); diff --git a/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs b/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs index 2cf42d9604..4f11af7327 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs @@ -35,7 +35,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -63,7 +63,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -100,7 +100,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -129,7 +129,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -173,7 +173,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); @@ -227,7 +227,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); @@ -276,7 +276,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); container.Measure(Size.Infinity); container.Arrange(new Rect(container.DesiredSize)); @@ -324,7 +324,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); @@ -399,11 +399,11 @@ namespace Avalonia.Base.UnitTests.Rendering scroll.UpdateChild(); - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); 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); @@ -447,7 +447,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -486,7 +486,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); Assert.Equal(new Rect(100, 100, 200, 200), border.Bounds); @@ -522,7 +522,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -560,7 +560,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); diff --git a/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests.cs index 26a155d203..a07191f464 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests.cs @@ -36,7 +36,7 @@ namespace Avalonia.Base.UnitTests.Rendering root.LayoutManager.ExecuteInitialLayoutPass(); - var target = new ImmediateRenderer(root); + var target = new ImmediateRenderer(root, root.CreateRenderTarget); target.AddDirty(child); @@ -69,7 +69,7 @@ namespace Avalonia.Base.UnitTests.Rendering child.RenderTransform = new ScaleTransform() { ScaleX = 2, ScaleY = 2 }; - var target = new ImmediateRenderer(root); + var target = new ImmediateRenderer(root, root.CreateRenderTarget); target.AddDirty(child); @@ -97,7 +97,7 @@ namespace Avalonia.Base.UnitTests.Rendering Height = 400, }; - var target = new ImmediateRenderer(root); + var target = new ImmediateRenderer(root, root.CreateRenderTarget); root.LayoutManager.ExecuteInitialLayoutPass(); target.AddDirty(child); @@ -165,7 +165,7 @@ namespace Avalonia.Base.UnitTests.Rendering stackPanel.Children.Add(control3); var root = new TestRoot(rootGrid); - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.LayoutManager.ExecuteInitialLayoutPass(); var rootSize = new Size(RootWidth, RootHeight); @@ -223,7 +223,7 @@ namespace Avalonia.Base.UnitTests.Rendering stackPanel.Children.Add(control3); var root = new TestRoot(rootGrid); - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.LayoutManager.ExecuteInitialLayoutPass(); var rootSize = new Size(RootWidth, RootHeight); diff --git a/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests_HitTesting.cs b/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests_HitTesting.cs index 70e4acba1e..9ce8c42e33 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests_HitTesting.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests_HitTesting.cs @@ -32,7 +32,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -70,7 +70,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -100,7 +100,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -145,7 +145,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -200,7 +200,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -259,7 +259,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); container.Measure(Size.Infinity); container.Arrange(new Rect(container.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -308,7 +308,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -384,7 +384,7 @@ namespace Avalonia.Base.UnitTests.Rendering scroll.UpdateChild(); - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -434,7 +434,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); 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/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs index 10db08f302..42e33729ac 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs @@ -8,13 +8,15 @@ using Avalonia.Media.Imaging; namespace Avalonia.Base.UnitTests.VisualTree { - class MockRenderInterface : IPlatformRenderInterface + class MockRenderInterface : IPlatformRenderInterface, IPlatformRenderInterfaceContext { public IRenderTarget CreateRenderTarget(IEnumerable surfaces) { throw new NotImplementedException(); } + public object TryGetFeature(Type featureType) => null; + public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) { throw new NotImplementedException(); @@ -77,6 +79,11 @@ namespace Avalonia.Base.UnitTests.VisualTree throw new NotImplementedException(); } + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) + { + return this; + } + public bool SupportsIndividualRoundRects { get; set; } public AlphaFormat DefaultAlphaFormat { get; } public PixelFormat DefaultPixelFormat { get; } @@ -263,6 +270,11 @@ namespace Avalonia.Base.UnitTests.VisualTree } } } + + public void Dispose() + { + + } } } diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs b/tests/Avalonia.Base.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs index b40ea40985..0c516a0481 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs @@ -44,7 +44,7 @@ namespace Avalonia.Base.UnitTests.VisualTree } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -86,7 +86,7 @@ namespace Avalonia.Base.UnitTests.VisualTree } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); 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/NullRenderer.cs b/tests/Avalonia.Benchmarks/NullRenderer.cs index 16f8998038..feb325f630 100644 --- a/tests/Avalonia.Benchmarks/NullRenderer.cs +++ b/tests/Avalonia.Benchmarks/NullRenderer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Avalonia.Rendering; using Avalonia.VisualTree; @@ -43,5 +44,7 @@ namespace Avalonia.Benchmarks public void Stop() { } + + public ValueTask TryGetRenderInterfaceFeature(Type featureType) => new(0); } } diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index 4170de71e6..a272d89b8a 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -9,7 +9,7 @@ using Microsoft.Diagnostics.Runtime; namespace Avalonia.Benchmarks { - internal class NullRenderingPlatform : IPlatformRenderInterface + internal class NullRenderingPlatform : IPlatformRenderInterface, IPlatformRenderInterfaceContext { public IGeometryImpl CreateEllipseGeometry(Rect rect) { @@ -46,6 +46,8 @@ namespace Avalonia.Benchmarks throw new NotImplementedException(); } + public object TryGetFeature(Type featureType) => null; + public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) { throw new NotImplementedException(); @@ -123,10 +125,19 @@ namespace Avalonia.Benchmarks return new MockGlyphRun(); } + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) + { + return this; + } + public bool SupportsIndividualRoundRects => true; public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul; public PixelFormat DefaultPixelFormat => PixelFormat.Rgba8888; + public void Dispose() + { + + } } } diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs new file mode 100644 index 0000000000..e1b3c6a99c --- /dev/null +++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs @@ -0,0 +1,62 @@ +using System; +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; + + public HugeTextLayout() + { + _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() => new TextLayout(Text, Typeface.Default, 12d, Brushes.Black); + + private const string Emojis = @"😀 😁 😂 🤣 😃 😄 😅 😆 😉 😊 😋 😎 😍 😘 🥰 😗 😙 😚 ☺️ 🙂 🤗 🤩 🤔 🤨 😐 😑 😶 🙄 😏 😣 😥 😮 🤐 😯 😪 😫 😴 😌 😛 😜 😝 🤤 😒 😓 😔 😕 🙃 🤑 😲 ☹️ 🙁 😖 😞 😟 😤 😢 😭 😦 😧 😨 😩 🤯 😬 😰 😱 🥵 🥶 😳 🤪 😵 😡 😠 🤬 😷 🤒 🤕 🤢 🤮 🤧 😇 🤠 🤡 🥳 🥴 🥺 🤥 🤫 🤭 🧐 🤓 😈 👿 👹 👺 💀 👻 👽 🤖 💩 😺 😸 😹 😻 😼 😽 🙀 😿 😾 +👶 👧 🧒 👦 👩 🧑 👨 👵 🧓 👴 👲 👳‍♀️ 👳‍♂️ 🧕 🧔 👱‍♂️ 👱‍♀️ 👨‍🦰 👩‍🦰 👨‍🦱 👩‍🦱 👨‍🦲 👩‍🦲 👨‍🦳 👩‍🦳 🦸‍♀️ 🦸‍♂️ 🦹‍♀️ 🦹‍♂️ 👮‍♀️ 👮‍♂️ 👷‍♀️ 👷‍♂️ 💂‍♀️ 💂‍♂️ 🕵️‍♀️ 🕵️‍♂️ 👩‍⚕️ 👨‍⚕️ 👩‍🌾 👨‍🌾 👩‍🍳 👨‍🍳 👩‍🎓 👨‍🎓 👩‍🎤 👨‍🎤 👩‍🏫 👨‍🏫 👩‍🏭 👨‍🏭 👩‍💻 👨‍💻 👩‍💼 👨‍💼 👩‍🔧 👨‍🔧 👩‍🔬 👨‍🔬 👩‍🎨 👨‍🎨 👩‍🚒 👨‍🚒 👩‍✈️ 👨‍✈️ 👩‍🚀 👨‍🚀 👩‍⚖️ 👨‍⚖️ 👰 🤵 👸 🤴 🤶 🎅 🧙‍♀️ 🧙‍♂️ 🧝‍♀️ 🧝‍♂️ 🧛‍♀️ 🧛‍♂️ 🧟‍♀️ 🧟‍♂️ 🧞‍♀️ 🧞‍♂️ 🧜‍♀️ 🧜‍♂️ 🧚‍♀️ 🧚‍♂️ 👼 🤰 🤱 🙇‍♀️ 🙇‍♂️ 💁‍♀️ 💁‍♂️ 🙅‍♀️ 🙅‍♂️ 🙆‍♀️ 🙆‍♂️ 🙋‍♀️ 🙋‍♂️ 🤦‍♀️ 🤦‍♂️ 🤷‍♀️ 🤷‍♂️ 🙎‍♀️ 🙎‍♂️ 🙍‍♀️ 🙍‍♂️ 💇‍♀️ 💇‍♂️ 💆‍♀️ 💆‍♂️ 🧖‍♀️ 🧖‍♂️ 💅 🤳 💃 🕺 👯‍♀️ 👯‍♂️ 🕴 🚶‍♀️ 🚶‍♂️ 🏃‍♀️ 🏃‍♂️ 👫 👭 👬 💑 👩‍❤️‍👩 👨‍❤️‍👨 💏 👩‍❤️‍💋‍👩 👨‍❤️‍💋‍👨 👪 👨‍👩‍👧 👨‍👩‍👧‍👦 👨‍👩‍👦‍👦 👨‍👩‍👧‍👧 👩‍👩‍👦 👩‍👩‍👧 👩‍👩‍👧‍👦 👩‍👩‍👦‍👦 👩‍👩‍👧‍👧 👨‍👨‍👦 👨‍👨‍👧 👨‍👨‍👧‍👦 👨‍👨‍👦‍👦 👨‍👨‍👧‍👧 👩‍👦 👩‍👧 👩‍👧‍👦 👩‍👦‍👦 👩‍👧‍👧 👨‍👦 👨‍👧 👨‍👧‍👦 👨‍👦‍👦 👨‍👧‍👧 🤲 👐 🙌 👏 🤝 👍 👎 👊 ✊ 🤛 🤜 🤞 ✌️ 🤟 🤘 👌 👈 👉 👆 👇 ☝️ ✋ 🤚 🖐 🖖 👋 🤙 💪 🦵 🦶 🖕 ✍️ 🙏 💍 💄 💋 👄 👅 👂 👃 👣 👁 👀 🧠 🦴 🦷 🗣 👤 👥 +🧥 👚 👕 👖 👔 👗 👙 👘 👠 👡 👢 👞 👟 🥾 🥿 🧦 🧤 🧣 🎩 🧢 👒 🎓 ⛑ 👑 👝 👛 👜 💼 🎒 👓 🕶 🥽 🥼 🌂 🧵 🧶 +👶🏻 👦🏻 👧🏻 👨🏻 👩🏻 👱🏻‍♀️ 👱🏻 👴🏻 👵🏻 👲🏻 👳🏻‍♀️ 👳🏻 👮🏻‍♀️ 👮🏻 👷🏻‍♀️ 👷🏻 💂🏻‍♀️ 💂🏻 🕵🏻‍♀️ 🕵🏻 👩🏻‍⚕️ 👨🏻‍⚕️ 👩🏻‍🌾 👨🏻‍🌾 👩🏻‍🍳 👨🏻‍🍳 👩🏻‍🎓 👨🏻‍🎓 👩🏻‍🎤 👨🏻‍🎤 👩🏻‍🏫 👨🏻‍🏫 👩🏻‍🏭 👨🏻‍🏭 👩🏻‍💻 👨🏻‍💻 👩🏻‍💼 👨🏻‍💼 👩🏻‍🔧 👨🏻‍🔧 👩🏻‍🔬 👨🏻‍🔬 👩🏻‍🎨 👨🏻‍🎨 👩🏻‍🚒 👨🏻‍🚒 👩🏻‍✈️ 👨🏻‍✈️ 👩🏻‍🚀 👨🏻‍🚀 👩🏻‍⚖️ 👨🏻‍⚖️ 🤶🏻 🎅🏻 👸🏻 🤴🏻 👰🏻 🤵🏻 👼🏻 🤰🏻 🙇🏻‍♀️ 🙇🏻 💁🏻 💁🏻‍♂️ 🙅🏻 🙅🏻‍♂️ 🙆🏻 🙆🏻‍♂️ 🙋🏻 🙋🏻‍♂️ 🤦🏻‍♀️ 🤦🏻‍♂️ 🤷🏻‍♀️ 🤷🏻‍♂️ 🙎🏻 🙎🏻‍♂️ 🙍🏻 🙍🏻‍♂️ 💇🏻 💇🏻‍♂️ 💆🏻 💆🏻‍♂️ 🕴🏻 💃🏻 🕺🏻 🚶🏻‍♀️ 🚶🏻 🏃🏻‍♀️ 🏃🏻 🤲🏻 👐🏻 🙌🏻 👏🏻 🙏🏻 👍🏻 👎🏻 👊🏻 ✊🏻 🤛🏻 🤜🏻 🤞🏻 ✌🏻 🤟🏻 🤘🏻 👌🏻 👈🏻 👉🏻 👆🏻 👇🏻 ☝🏻 ✋🏻 🤚🏻 🖐🏻 🖖🏻 👋🏻 🤙🏻 💪🏻 🖕🏻 ✍🏻 🤳🏻 💅🏻 👂🏻 👃🏻 +👶🏼 👦🏼 👧🏼 👨🏼 👩🏼 👱🏼‍♀️ 👱🏼 👴🏼 👵🏼 👲🏼 👳🏼‍♀️ 👳🏼 👮🏼‍♀️ 👮🏼 👷🏼‍♀️ 👷🏼 💂🏼‍♀️ 💂🏼 🕵🏼‍♀️ 🕵🏼 👩🏼‍⚕️ 👨🏼‍⚕️ 👩🏼‍🌾 👨🏼‍🌾 👩🏼‍🍳 👨🏼‍🍳 👩🏼‍🎓 👨🏼‍🎓 👩🏼‍🎤 👨🏼‍🎤 👩🏼‍🏫 👨🏼‍🏫 👩🏼‍🏭 👨🏼‍🏭 👩🏼‍💻 👨🏼‍💻 👩🏼‍💼 👨🏼‍💼 👩🏼‍🔧 👨🏼‍🔧 👩🏼‍🔬 👨🏼‍🔬 👩🏼‍🎨 👨🏼‍🎨 👩🏼‍🚒 👨🏼‍🚒 👩🏼‍✈️ 👨🏼‍✈️ 👩🏼‍🚀 👨🏼‍🚀 👩🏼‍⚖️ 👨🏼‍⚖️ 🤶🏼 🎅🏼 👸🏼 🤴🏼 👰🏼 🤵🏼 👼🏼 🤰🏼 🙇🏼‍♀️ 🙇🏼 💁🏼 💁🏼‍♂️ 🙅🏼 🙅🏼‍♂️ 🙆🏼 🙆🏼‍♂️ 🙋🏼 🙋🏼‍♂️ 🤦🏼‍♀️ 🤦🏼‍♂️ 🤷🏼‍♀️ 🤷🏼‍♂️ 🙎🏼 🙎🏼‍♂️ 🙍🏼 🙍🏼‍♂️ 💇🏼 💇🏼‍♂️ 💆🏼 💆🏼‍♂️ 🕴🏼 💃🏼 🕺🏼 🚶🏼‍♀️ 🚶🏼 🏃🏼‍♀️ 🏃🏼 🤲🏼 👐🏼 🙌🏼 👏🏼 🙏🏼 👍🏼 👎🏼 👊🏼 ✊🏼 🤛🏼 🤜🏼 🤞🏼 ✌🏼 🤟🏼 🤘🏼 👌🏼 👈🏼 👉🏼 👆🏼 👇🏼 ☝🏼 ✋🏼 🤚🏼 🖐🏼 🖖🏼 👋🏼 🤙🏼 💪🏼 🖕🏼 ✍🏼 🤳🏼 💅🏼 👂🏼 👃🏼 +👶🏽 👦🏽 👧🏽 👨🏽 👩🏽 👱🏽‍♀️ 👱🏽 👴🏽 👵🏽 👲🏽 👳🏽‍♀️ 👳🏽 👮🏽‍♀️ 👮🏽 👷🏽‍♀️ 👷🏽 💂🏽‍♀️ 💂🏽 🕵🏽‍♀️ 🕵🏽 👩🏽‍⚕️ 👨🏽‍⚕️ 👩🏽‍🌾 👨🏽‍🌾 👩🏽‍🍳 👨🏽‍🍳 👩🏽‍🎓 👨🏽‍🎓 👩🏽‍🎤 👨🏽‍🎤 👩🏽‍🏫 👨🏽‍🏫 👩🏽‍🏭 👨🏽‍🏭 👩🏽‍💻 👨🏽‍💻 👩🏽‍💼 👨🏽‍💼 👩🏽‍🔧 👨🏽‍🔧 👩🏽‍🔬 👨🏽‍🔬 👩🏽‍🎨 👨🏽‍🎨 👩🏽‍🚒 👨🏽‍🚒 👩🏽‍✈️ 👨🏽‍✈️ 👩🏽‍🚀 👨🏽‍🚀 👩🏽‍⚖️ 👨🏽‍⚖️ 🤶🏽 🎅🏽 👸🏽 🤴🏽 👰🏽 🤵🏽 👼🏽 🤰🏽 🙇🏽‍♀️ 🙇🏽 💁🏽 💁🏽‍♂️ 🙅🏽 🙅🏽‍♂️ 🙆🏽 🙆🏽‍♂️ 🙋🏽 🙋🏽‍♂️ 🤦🏽‍♀️ 🤦🏽‍♂️ 🤷🏽‍♀️ 🤷🏽‍♂️ 🙎🏽 🙎🏽‍♂️ 🙍🏽 🙍🏽‍♂️ 💇🏽 💇🏽‍♂️ 💆🏽 💆🏽‍♂️ 🕴🏼 💃🏽 🕺🏽 🚶🏽‍♀️ 🚶🏽 🏃🏽‍♀️ 🏃🏽 🤲🏽 👐🏽 🙌🏽 👏🏽 🙏🏽 👍🏽 👎🏽 👊🏽 ✊🏽 🤛🏽 🤜🏽 🤞🏽 ✌🏽 🤟🏽 🤘🏽 👌🏽 👈🏽 👉🏽 👆🏽 👇🏽 ☝🏽 ✋🏽 🤚🏽 🖐🏽 🖖🏽 👋🏽 🤙🏽 💪🏽 🖕🏽 ✍🏽 🤳🏽 💅🏽 👂🏽 👃🏽 +👶🏾 👦🏾 👧🏾 👨🏾 👩🏾 👱🏾‍♀️ 👱🏾 👴🏾 👵🏾 👲🏾 👳🏾‍♀️ 👳🏾 👮🏾‍♀️ 👮🏾 👷🏾‍♀️ 👷🏾 💂🏾‍♀️ 💂🏾 🕵🏾‍♀️ 🕵🏾 👩🏾‍⚕️ 👨🏾‍⚕️ 👩🏾‍🌾 👨🏾‍🌾 👩🏾‍🍳 👨🏾‍🍳 👩🏾‍🎓 👨🏾‍🎓 👩🏾‍🎤 👨🏾‍🎤 👩🏾‍🏫 👨🏾‍🏫 👩🏾‍🏭 👨🏾‍🏭 👩🏾‍💻 👨🏾‍💻 👩🏾‍💼 👨🏾‍💼 👩🏾‍🔧 👨🏾‍🔧 👩🏾‍🔬 👨🏾‍🔬 👩🏾‍🎨 👨🏾‍🎨 👩🏾‍🚒 👨🏾‍🚒 👩🏾‍✈️ 👨🏾‍✈️ 👩🏾‍🚀 👨🏾‍🚀 👩🏾‍⚖️ 👨🏾‍⚖️ 🤶🏾 🎅🏾 👸🏾 🤴🏾 👰🏾 🤵🏾 👼🏾 🤰🏾 🙇🏾‍♀️ 🙇🏾 💁🏾 💁🏾‍♂️ 🙅🏾 🙅🏾‍♂️ 🙆🏾 🙆🏾‍♂️ 🙋🏾 🙋🏾‍♂️ 🤦🏾‍♀️ 🤦🏾‍♂️ 🤷🏾‍♀️ 🤷🏾‍♂️ 🙎🏾 🙎🏾‍♂️ 🙍🏾 🙍🏾‍♂️ 💇🏾 💇🏾‍♂️ 💆🏾 💆🏾‍♂️ 🕴🏾 💃🏾 🕺🏾 🚶🏾‍♀️ 🚶🏾 🏃🏾‍♀️ 🏃🏾 🤲🏾 👐🏾 🙌🏾 👏🏾 🙏🏾 👍🏾 👎🏾 👊🏾 ✊🏾 🤛🏾 🤜🏾 🤞🏾 ✌🏾 🤟🏾 🤘🏾 👌🏾 👈🏾 👉🏾 👆🏾 👇🏾 ☝🏾 ✋🏾 🤚🏾 🖐🏾 🖖🏾 👋🏾 🤙🏾 💪🏾 🖕🏾 ✍🏾 🤳🏾 💅🏾 👂🏾 👃🏾 +👶🏿 👦🏿 👧🏿 👨🏿 👩🏿 👱🏿‍♀️ 👱🏿 👴🏿 👵🏿 👲🏿 👳🏿‍♀️ 👳🏿 👮🏿‍♀️ 👮🏿 👷🏿‍♀️ 👷🏿 💂🏿‍♀️ 💂🏿 🕵🏿‍♀️ 🕵🏿 👩🏿‍⚕️ 👨🏿‍⚕️ 👩🏿‍🌾 👨🏿‍🌾 👩🏿‍🍳 👨🏿‍🍳 👩🏿‍🎓 👨🏿‍🎓 👩🏿‍🎤 👨🏿‍🎤 👩🏿‍🏫 👨🏿‍🏫 👩🏿‍🏭 👨🏿‍🏭 👩🏿‍💻 👨🏿‍💻 👩🏿‍💼 👨🏿‍💼 👩🏿‍🔧 👨🏿‍🔧 👩🏿‍🔬 👨🏿‍🔬 👩🏿‍🎨 👨🏿‍🎨 👩🏿‍🚒 👨🏿‍🚒 👩🏿‍✈️ 👨🏿‍✈️ 👩🏿‍🚀 👨🏿‍🚀 👩🏿‍⚖️ 👨🏿‍⚖️ 🤶🏿 🎅🏿 👸🏿 🤴🏿 👰🏿 🤵🏿 👼🏿 🤰🏿 🙇🏿‍♀️ 🙇🏿 💁🏿 💁🏿‍♂️ 🙅🏿 🙅🏿‍♂️ 🙆🏿 🙆🏿‍♂️ 🙋🏿 🙋🏿‍♂️ 🤦🏿‍♀️ 🤦🏿‍♂️ 🤷🏿‍♀️ 🤷🏿‍♂️ 🙎🏿 🙎🏿‍♂️ 🙍🏿 🙍🏿‍♂️ 💇🏿 💇🏿‍♂️ 💆🏿 💆🏿‍♂️ 🕴🏿 💃🏿 🕺🏿 🚶🏿‍♀️ 🚶🏿 🏃🏿‍♀️ 🏃🏿 🤲🏿 👐🏿 🙌🏿 👏🏿 🙏🏿 👍🏿 👎🏿 👊🏿 ✊🏿 🤛🏿 🤜🏿 🤞🏿 ✌🏿 🤟🏿 🤘🏿 👌🏿 👈🏿 👉🏿 👆🏿 👇🏿 ☝🏿 ✋🏿 🤚🏿 🖐🏿 🖖🏿 👋🏿 🤙🏿 💪🏿 🖕🏿 ✍🏿 🤳🏿 💅🏿 👂🏿 👃🏿 +🐶 🐱 🐭 🐹 🐰 🦊 🦝 🐻 🐼 🦘 🦡 🐨 🐯 🦁 🐮 🐷 🐽 🐸 🐵 🙈 🙉 🙊 🐒 🐔 🐧 🐦 🐤 🐣 🐥 🦆 🦢 🦅 🦉 🦚 🦜 🦇 🐺 🐗 🐴 🦄 🐝 🐛 🦋 🐌 🐚 🐞 🐜 🦗 🕷 🕸 🦂 🦟 🦠 🐢 🐍 🦎 🦖 🦕 🐙 🦑 🦐 🦀 🐡 🐠 🐟 🐬 🐳 🐋 🦈 🐊 🐅 🐆 🦓 🦍 🐘 🦏 🦛 🐪 🐫 🦙 🦒 🐃 🐂 🐄 🐎 🐖 🐏 🐑 🐐 🦌 🐕 🐩 🐈 🐓 🦃 🕊 🐇 🐁 🐀 🐿 🦔 🐾 🐉 🐲 🌵 🎄 🌲 🌳 🌴 🌱 🌿 ☘️ 🍀 🎍 🎋 🍃 🍂 🍁 🍄 🌾 💐 🌷 🌹 🥀 🌺 🌸 🌼 🌻 🌞 🌝 🌛 🌜 🌚 🌕 🌖 🌗 🌘 🌑 🌒 🌓 🌔 🌙 🌎 🌍 🌏 💫 ⭐️ 🌟 ✨ ⚡️ ☄️ 💥 🔥 🌪 🌈 ☀️ 🌤 ⛅️ 🌥 ☁️ 🌦 🌧 ⛈ 🌩 🌨 ❄️ ☃️ ⛄️ 🌬 💨 💧 💦 ☔️ ☂️ 🌊 🌫 +🍏 🍎 🍐 🍊 🍋 🍌 🍉 🍇 🍓 🍈 🍒 🍑 🍍 🥭 🥥 🥝 🍅 🍆 🥑 🥦 🥒 🥬 🌶 🌽 🥕 🥔 🍠 🥐 🍞 🥖 🥨 🥯 🧀 🥚 🍳 🥞 🥓 🥩 🍗 🍖 🌭 🍔 🍟 🍕 🥪 🥙 🌮 🌯 🥗 🥘 🥫 🍝 🍜 🍲 🍛 🍣 🍱 🥟 🍤 🍙 🍚 🍘 🍥 🥮 🥠 🍢 🍡 🍧 🍨 🍦 🥧 🍰 🎂 🍮 🍭 🍬 🍫 🍿 🧂 🍩 🍪 🌰 🥜 🍯 🥛 🍼 ☕️ 🍵 🥤 🍶 🍺 🍻 🥂 🍷 🥃 🍸 🍹 🍾 🥄 🍴 🍽 🥣 🥡 🥢 +⚽️ 🏀 🏈 ⚾️ 🥎 🏐 🏉 🎾 🥏 🎱 🏓 🏸 🥅 🏒 🏑 🥍 🏏 ⛳️ 🏹 🎣 🥊 🥋 🎽 ⛸ 🥌 🛷 🛹 🎿 ⛷ 🏂 🏋️‍♀️ 🏋🏻‍♀️ 🏋🏼‍♀️ 🏋🏽‍♀️ 🏋🏾‍♀️ 🏋🏿‍♀️ 🏋️‍♂️ 🏋🏻‍♂️ 🏋🏼‍♂️ 🏋🏽‍♂️ 🏋🏾‍♂️ 🏋🏿‍♂️ 🤼‍♀️ 🤼‍♂️ 🤸‍♀️ 🤸🏻‍♀️ 🤸🏼‍♀️ 🤸🏽‍♀️ 🤸🏾‍♀️ 🤸🏿‍♀️ 🤸‍♂️ 🤸🏻‍♂️ 🤸🏼‍♂️ 🤸🏽‍♂️ 🤸🏾‍♂️ 🤸🏿‍♂️ ⛹️‍♀️ ⛹🏻‍♀️ ⛹🏼‍♀️ ⛹🏽‍♀️ ⛹🏾‍♀️ ⛹🏿‍♀️ ⛹️‍♂️ ⛹🏻‍♂️ ⛹🏼‍♂️ ⛹🏽‍♂️ ⛹🏾‍♂️ ⛹🏿‍♂️ 🤺 🤾‍♀️ 🤾🏻‍♀️ 🤾🏼‍♀️ 🤾🏾‍♀️ 🤾🏾‍♀️ 🤾🏿‍♀️ 🤾‍♂️ 🤾🏻‍♂️ 🤾🏼‍♂️ 🤾🏽‍♂️ 🤾🏾‍♂️ 🤾🏿‍♂️ 🏌️‍♀️ 🏌🏻‍♀️ 🏌🏼‍♀️ 🏌🏽‍♀️ 🏌🏾‍♀️ 🏌🏿‍♀️ 🏌️‍♂️ 🏌🏻‍♂️ 🏌🏼‍♂️ 🏌🏽‍♂️ 🏌🏾‍♂️ 🏌🏿‍♂️ 🏇 🏇🏻 🏇🏼 🏇🏽 🏇🏾 🏇🏿 🧘‍♀️ 🧘🏻‍♀️ 🧘🏼‍♀️ 🧘🏽‍♀️ 🧘🏾‍♀️ 🧘🏿‍♀️ 🧘‍♂️ 🧘🏻‍♂️ 🧘🏼‍♂️ 🧘🏽‍♂️ 🧘🏾‍♂️ 🧘🏿‍♂️ 🏄‍♀️ 🏄🏻‍♀️ 🏄🏼‍♀️ 🏄🏽‍♀️ 🏄🏾‍♀️ 🏄🏿‍♀️ 🏄‍♂️ 🏄🏻‍♂️ 🏄🏼‍♂️ 🏄🏽‍♂️ 🏄🏾‍♂️ 🏄🏿‍♂️ 🏊‍♀️ 🏊🏻‍♀️ 🏊🏼‍♀️ 🏊🏽‍♀️ 🏊🏾‍♀️ 🏊🏿‍♀️ 🏊‍♂️ 🏊🏻‍♂️ 🏊🏼‍♂️ 🏊🏽‍♂️ 🏊🏾‍♂️ 🏊🏿‍♂️ 🤽‍♀️ 🤽🏻‍♀️ 🤽🏼‍♀️ 🤽🏽‍♀️ 🤽🏾‍♀️ 🤽🏿‍♀️ 🤽‍♂️ 🤽🏻‍♂️ 🤽🏼‍♂️ 🤽🏽‍♂️ 🤽🏾‍♂️ 🤽🏿‍♂️ 🚣‍♀️ 🚣🏻‍♀️ 🚣🏼‍♀️ 🚣🏽‍♀️ 🚣🏾‍♀️ 🚣🏿‍♀️ 🚣‍♂️ 🚣🏻‍♂️ 🚣🏼‍♂️ 🚣🏽‍♂️ 🚣🏾‍♂️ 🚣🏿‍♂️ 🧗‍♀️ 🧗🏻‍♀️ 🧗🏼‍♀️ 🧗🏽‍♀️ 🧗🏾‍♀️ 🧗🏿‍♀️ 🧗‍♂️ 🧗🏻‍♂️ 🧗🏼‍♂️ 🧗🏽‍♂️ 🧗🏾‍♂️ 🧗🏿‍♂️ 🚵‍♀️ 🚵🏻‍♀️ 🚵🏼‍♀️ 🚵🏽‍♀️ 🚵🏾‍♀️ 🚵🏿‍♀️ 🚵‍♂️ 🚵🏻‍♂️ 🚵🏼‍♂️ 🚵🏽‍♂️ 🚵🏾‍♂️ 🚵🏿‍♂️ 🚴‍♀️ 🚴🏻‍♀️ 🚴🏼‍♀️ 🚴🏽‍♀️ 🚴🏾‍♀️ 🚴🏿‍♀️ 🚴‍♂️ 🚴🏻‍♂️ 🚴🏼‍♂️ 🚴🏽‍♂️ 🚴🏾‍♂️ 🚴🏿‍♂️ 🏆 🥇 🥈 🥉 🏅 🎖 🏵 🎗 🎫 🎟 🎪 🤹‍♀️ 🤹🏻‍♀️ 🤹🏼‍♀️ 🤹🏽‍♀️ 🤹🏾‍♀️ 🤹🏿‍♀️ 🤹‍♂️ 🤹🏻‍♂️ 🤹🏼‍♂️ 🤹🏽‍♂️ 🤹🏾‍♂️ 🤹🏿‍♂️ 🎭 🎨 🎬 🎤 🎧 🎼 🎹 🥁 🎷 🎺 🎸 🎻 🎲 🧩 ♟ 🎯 🎳 🎮 🎰 +🚗 🚕 🚙 🚌 🚎 🏎 🚓 🚑 🚒 🚐 🚚 🚛 🚜 🛴 🚲 🛵 🏍 🚨 🚔 🚍 🚘 🚖 🚡 🚠 🚟 🚃 🚋 🚞 🚝 🚄 🚅 🚈 🚂 🚆 🚇 🚊 🚉 ✈️ 🛫 🛬 🛩 💺 🛰 🚀 🛸 🚁 🛶 ⛵️ 🚤 🛥 🛳 ⛴ 🚢 ⚓️ ⛽️ 🚧 🚦 🚥 🚏 🗺 🗿 🗽 🗼 🏰 🏯 🏟 🎡 🎢 🎠 ⛲️ ⛱ 🏖 🏝 🏜 🌋 ⛰ 🏔 🗻 🏕 ⛺️ 🏠 🏡 🏘 🏚 🏗 🏭 🏢 🏬 🏣 🏤 🏥 🏦 🏨 🏪 🏫 🏩 💒 🏛 ⛪️ 🕌 🕍 🕋 ⛩ 🛤 🛣 🗾 🎑 🏞 🌅 🌄 🌠 🎇 🎆 🌇 🌆 🏙 🌃 🌌 🌉 🌁 +⌚️ 📱 📲 💻 ⌨️ 🖥 🖨 🖱 🖲 🕹 🗜 💽 💾 💿 📀 📼 📷 📸 📹 🎥 📽 🎞 📞 ☎️ 📟 📠 📺 📻 🎙 🎚 🎛 ⏱ ⏲ ⏰ 🕰 ⌛️ ⏳ 📡 🔋 🔌 💡 🔦 🕯 🗑 🛢 💸 💵 💴 💶 💷 💰 💳 🧾 💎 ⚖️ 🔧 🔨 ⚒ 🛠 ⛏ 🔩 ⚙️ ⛓ 🔫 💣 🔪 🗡 ⚔️ 🛡 🚬 ⚰️ ⚱️ 🏺 🧭 🧱 🔮 🧿 🧸 📿 💈 ⚗️ 🔭 🧰 🧲 🧪 🧫 🧬 🧯 🔬 🕳 💊 💉 🌡 🚽 🚰 🚿 🛁 🛀 🛀🏻 🛀🏼 🛀🏽 🛀🏾 🛀🏿 🧴 🧵 🧶 🧷 🧹 🧺 🧻 🧼 🧽 🛎 🔑 🗝 🚪 🛋 🛏 🛌 🖼 🛍 🧳 🛒 🎁 🎈 🎏 🎀 🎊 🎉 🧨 🎎 🏮 🎐 🧧 ✉️ 📩 📨 📧 💌 📥 📤 📦 🏷 📪 📫 📬 📭 📮 📯 📜 📃 📄 📑 📊 📈 📉 🗒 🗓 📆 📅 📇 🗃 🗳 🗄 📋 📁 📂 🗂 🗞 📰 📓 📔 📒 📕 📗 📘 📙 📚 📖 🔖 🔗 📎 🖇 📐 📏 📌 📍 ✂️ 🖊 🖋 ✒️ 🖌 🖍 📝 ✏️ 🔍 🔎 🔏 🔐 🔒 🔓 +❤️ 🧡 💛 💚 💙 💜 🖤 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ⏏️ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ ♾ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 🔜 ✔️ ☑️ 🔘 ⚪️ ⚫️ 🔴 🔵 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 💬 💭 🗯 ♠️ ♣️ ♥️ ♦️ 🃏 🎴 🀄️ 🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛 🕜 🕝 🕞 🕟 🕠 🕡 🕢 🕣 🕤 🕥 🕦 🕧 +🏳️ 🏴 🏁 🚩 🏳️‍🌈 🏴‍☠️ 🇦🇫 🇦🇽 🇦🇱 🇩🇿 🇦🇸 🇦🇩 🇦🇴 🇦🇮 🇦🇶 🇦🇬 🇦🇷 🇦🇲 🇦🇼 🇦🇺 🇦🇹 🇦🇿 🇧🇸 🇧🇭 🇧🇩 🇧🇧 🇧🇾 🇧🇪 🇧🇿 🇧🇯 🇧🇲 🇧🇹 🇧🇴 🇧🇦 🇧🇼 🇧🇷 🇮🇴 🇻🇬 🇧🇳 🇧🇬 🇧🇫 🇧🇮 🇰🇭 🇨🇲 🇨🇦 🇮🇨 🇨🇻 🇧🇶 🇰🇾 🇨🇫 🇹🇩 🇨🇱 🇨🇳 🇨🇽 🇨🇨 🇨🇴 🇰🇲 🇨🇬 🇨🇩 🇨🇰 🇨🇷 🇨🇮 🇭🇷 🇨🇺 🇨🇼 🇨🇾 🇨🇿 🇩🇰 🇩🇯 🇩🇲 🇩🇴 🇪🇨 🇪🇬 🇸🇻 🇬🇶 🇪🇷 🇪🇪 🇪🇹 🇪🇺 🇫🇰 🇫🇴 🇫🇯 🇫🇮 🇫🇷 🇬🇫 🇵🇫 🇹🇫 🇬🇦 🇬🇲 🇬🇪 🇩🇪 🇬🇭 🇬🇮 🇬🇷 🇬🇱 🇬🇩 🇬🇵 🇬🇺 🇬🇹 🇬🇬 🇬🇳 🇬🇼 🇬🇾 🇭🇹 🇭🇳 🇭🇰 🇭🇺 🇮🇸 🇮🇳 🇮🇩 🇮🇷 🇮🇶 🇮🇪 🇮🇲 🇮🇱 🇮🇹 🇯🇲 🇯🇵 🎌 🇯🇪 🇯🇴 🇰🇿 🇰🇪 🇰🇮 🇽🇰 🇰🇼 🇰🇬 🇱🇦 🇱🇻 🇱🇧 🇱🇸 🇱🇷 🇱🇾 🇱🇮 🇱🇹 🇱🇺 🇲🇴 🇲🇰 🇲🇬 🇲🇼 🇲🇾 🇲🇻 🇲🇱 🇲🇹 🇲🇭 🇲🇶 🇲🇷 🇲🇺 🇾🇹 🇲🇽 🇫🇲 🇲🇩 🇲🇨 🇲🇳 🇲🇪 🇲🇸 🇲🇦 🇲🇿 🇲🇲 🇳🇦 🇳🇷 🇳🇵 🇳🇱 🇳🇨 🇳🇿 🇳🇮 🇳🇪 🇳🇬 🇳🇺 🇳🇫 🇰🇵 🇲🇵 🇳🇴 🇴🇲 🇵🇰 🇵🇼 🇵🇸 🇵🇦 🇵🇬 🇵🇾 🇵🇪 🇵🇭 🇵🇳 🇵🇱 🇵🇹 🇵🇷 🇶🇦 🇷🇪 🇷🇴 🇷🇺 🇷🇼 🇼🇸 🇸🇲 🇸🇦 🇸🇳 🇷🇸 🇸🇨 🇸🇱 🇸🇬 🇸🇽 🇸🇰 🇸🇮 🇬🇸 🇸🇧 🇸🇴 🇿🇦 🇰🇷 🇸🇸 🇪🇸 🇱🇰 🇧🇱 🇸🇭 🇰🇳 🇱🇨 🇵🇲 🇻🇨 🇸🇩 🇸🇷 🇸🇿 🇸🇪 🇨🇭 🇸🇾 🇹🇼 🇹🇯 🇹🇿 🇹🇭 🇹🇱 🇹🇬 🇹🇰 🇹🇴 🇹🇹 🇹🇳 🇹🇷 🇹🇲 🇹🇨 🇹🇻 🇻🇮 🇺🇬 🇺🇦 🇦🇪 🇬🇧 🏴󠁧󠁢󠁥󠁮󠁧󠁿 🏴󠁧󠁢󠁳󠁣󠁴󠁿 🏴󠁧󠁢󠁷󠁬󠁳󠁿 🇺🇳 🇺🇸 🇺🇾 🇺🇿 🇻🇺 🇻🇦 🇻🇪 🇻🇳 🇼🇫 🇪🇭 🇾🇪 🇿🇲 🇿🇼 +🥱 🤏 🦾 🦿 🦻 🧏 🧏‍♂️ 🧏‍♀️ 🧍 🧍‍♂️ 🧍‍♀️ 🧎 🧎‍♂️ 🧎‍♀️ 👨‍🦯 👩‍🦯 👨‍🦼 👩‍🦼 👨‍🦽 👩‍🦽 🦧 🦮 🐕‍🦺 🦥 🦦 🦨 🦩 🧄 🧅 🧇 🧆 🧈 🦪 🧃 🧉 🧊 🛕 🦽 🦼 🛺 🪂 🪐 🤿 🪀 🪁 🦺 🥻 🩱 🩲 🩳 🩰 🪕 🪔 🪓 🦯 🩸 🩹 🩺 🪑 🪒 🤎 🤍 🟠 🟡 🟢 🟣 🟤 🟥 🟧 🟨 🟩 🟦 🟪 🟫"; + + [Benchmark] + public TextLayout BuildEmojisTextLayout() => new TextLayout(Emojis, Typeface.Default, 12d, Brushes.Black); + + public void Dispose() + { + _app?.Dispose(); + } +} diff --git a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs index e82576c7d9..70636d1fe6 100644 --- a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs @@ -16,6 +16,8 @@ namespace Avalonia.Benchmarks.Themes public class ThemeBenchmark : IDisposable { private IDisposable _app; + private readonly FluentTheme _reusableFluentTheme = new FluentTheme(); + private readonly SimpleTheme _reusableSimpleTheme = new SimpleTheme(); public ThemeBenchmark() { @@ -49,6 +51,26 @@ namespace Avalonia.Benchmarks.Themes }; return ((IResourceHost)UnitTestApplication.Current).TryGetResource("ThemeAccentColor", out _); } + + [Benchmark] + [Arguments(typeof(Button))] + [Arguments(typeof(TextBox))] + [Arguments(typeof(DatePicker))] + public object FindFluentControlTheme(Type type) + { + _reusableFluentTheme.TryGetResource(type, out var theme); + return theme; + } + + [Benchmark] + [Arguments(typeof(Button))] + [Arguments(typeof(TextBox))] + [Arguments(typeof(DatePicker))] + public object FindSimpleControlTheme(Type type) + { + _reusableSimpleTheme.TryGetResource(type, out var theme); + return theme; + } public void 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..9dc0cb95bd 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -155,12 +155,16 @@ namespace Avalonia.Controls.UnitTests window.Closing += (sender, e) => { + Assert.Equal(WindowCloseReason.WindowClosing, e.CloseReason); + Assert.Equal(programaticClose, e.IsProgrammatic); count++; windowClosing = count; }; child.Closing += (sender, e) => { + Assert.Equal(WindowCloseReason.OwnerWindowClosing, e.CloseReason); + Assert.Equal(programaticClose, e.IsProgrammatic); count++; childClosing = count; }; @@ -186,7 +190,7 @@ namespace Avalonia.Controls.UnitTests } else { - var cancel = window.PlatformImpl.Closing(); + var cancel = window.PlatformImpl.Closing(WindowCloseReason.WindowClosing); Assert.Equal(false, cancel); } @@ -248,7 +252,7 @@ namespace Avalonia.Controls.UnitTests } 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.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index ae8df6168d..c713a9e61d 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Disposables; - +using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Presenters; @@ -1060,6 +1060,8 @@ namespace Avalonia.LeakTests public void Stop() { } + + public ValueTask TryGetRenderInterfaceFeature(Type featureType) => new(null); } } } 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/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/MergeResourceIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs new file mode 100644 index 0000000000..92807b2cb9 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs @@ -0,0 +1,131 @@ +using System; +using System.Runtime.CompilerServices; +using System.Xml; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Media; +using Avalonia.Styling; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml; + +public class MergeResourceIncludeTests +{ + static MergeResourceIncludeTests() + { + RuntimeHelpers.RunClassConstructor(typeof(RelativeSource).TypeHandle); + } + + [Fact] + public void MergeResourceInclude_Works_With_Single_Resource() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources.xaml"), @" + + Red +"), + new RuntimeXamlLoaderDocument(@" + + + + Blue + + + + + +") + }; + + var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var contentControl = Assert.IsType(objects[1]); + + var resources = Assert.IsType(contentControl.Resources); + Assert.Empty(resources.MergedDictionaries); + + var initialResource = (ISolidColorBrush)resources["brush1"]!; + Assert.Equal(Colors.Blue, initialResource.Color); + + var mergedResource = (ISolidColorBrush)resources["brush2"]!; + Assert.Equal(Colors.Red, mergedResource.Color); + } + + [Fact] + public void Mixing_MergeResourceInclude_And_ResourceInclude_Is_Not_Allowed() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources1.xaml"), @" + + Red +"), + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources2.xaml"), @" + + Blue +"), + new RuntimeXamlLoaderDocument(@" + + + + + +") + }; + + Assert.ThrowsAny(() => AvaloniaRuntimeXamlLoader.LoadGroup(documents)); + } + + [Fact] + public void MergeResourceInclude_Works_With_Multiple_Resources() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources1.xaml"), @" + + Red + Blue +"), + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources2.xaml"), @" + + Yellow + + + +"), + new RuntimeXamlLoaderDocument(@" + + + + + + Black + White +"), + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources1_2.xaml"), @" + + Green +"), + }; + + var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var resources = Assert.IsType(objects[2]); + Assert.Empty(resources.MergedDictionaries); + + Assert.Equal(Colors.Red, ((ISolidColorBrush)resources["brush1"]!).Color); + Assert.Equal(Colors.Blue, ((ISolidColorBrush)resources["brush2"]!).Color); + Assert.Equal(Colors.Green, ((ISolidColorBrush)resources["brush3"]!).Color); + Assert.Equal(Colors.Yellow, ((ISolidColorBrush)resources["brush4"]!).Color); + Assert.Equal(Colors.Black, ((ISolidColorBrush)resources["brush5"]!).Color); + Assert.Equal(Colors.White, ((ISolidColorBrush)resources["brush6"]!).Color); + } +} 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/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/Media/BitmapTests.cs b/tests/Avalonia.RenderTests/Media/BitmapTests.cs index d52539c371..2d83f5ce0f 100644 --- a/tests/Avalonia.RenderTests/Media/BitmapTests.cs +++ b/tests/Avalonia.RenderTests/Media/BitmapTests.cs @@ -69,8 +69,9 @@ namespace Avalonia.Direct2D1.RenderTests.Media { var testName = nameof(FramebufferRenderResultsShouldBeUsableAsBitmap) + "_" + fmt; var fb = new Framebuffer(fmt, new PixelSize(80, 80)); - var r = Avalonia.AvaloniaLocator.Current.GetService(); - using (var target = r.CreateRenderTarget(new object[] { fb })) + var r = Avalonia.AvaloniaLocator.Current.GetRequiredService(); + using(var cpuContext = r.CreateBackendContext(null)) + using (var target = cpuContext.CreateRenderTarget(new object[] { fb })) using (var ctx = target.CreateDrawingContext(null)) { ctx.Clear(Colors.Transparent); diff --git a/tests/Avalonia.RenderTests/TestBase.cs b/tests/Avalonia.RenderTests/TestBase.cs index 3f918e2a73..edde62f041 100644 --- a/tests/Avalonia.RenderTests/TestBase.cs +++ b/tests/Avalonia.RenderTests/TestBase.cs @@ -13,6 +13,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; using System.Threading; +using Avalonia.Controls.Platform.Surfaces; using Avalonia.Media; using Avalonia.Rendering.Composition; using Avalonia.Threading; @@ -90,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); @@ -122,10 +123,13 @@ namespace Avalonia.Direct2D1.RenderTests var timer = new ManualRenderTimer(); var compositor = new Compositor(new RenderLoop(timer, Dispatcher.UIThread), null); - using (var rtb = factory.CreateRenderTargetBitmap(pixelSize, dpiVector)) + using (var writableBitmap = factory.CreateWriteableBitmap(pixelSize, dpiVector, factory.DefaultPixelFormat, factory.DefaultAlphaFormat)) { - var root = new TestRenderRoot(dpiVector.X / 96, rtb); - using (var renderer = new CompositingRenderer(root, compositor) { RenderOnlyOnRenderThread = false}) + var root = new TestRenderRoot(dpiVector.X / 96, null!); + using (var renderer = new CompositingRenderer(root, compositor, () => new[] + { + new BitmapFramebufferSurface(writableBitmap) + }) { RenderOnlyOnRenderThread = false }) { root.Initialize(renderer, target); renderer.Start(); @@ -136,11 +140,24 @@ namespace Avalonia.Direct2D1.RenderTests // Free pools for (var c = 0; c < 11; c++) TestThreadingInterface.RunTimers(); - rtb.Save(compositedPath); + writableBitmap.Save(compositedPath); } } - protected void CompareImages([CallerMemberName] string testName = "") + class BitmapFramebufferSurface : IFramebufferPlatformSurface + { + private readonly IWriteableBitmapImpl _bitmap; + + public BitmapFramebufferSurface(IWriteableBitmapImpl bitmap) + { + _bitmap = bitmap; + } + + public ILockedFramebuffer Lock() => _bitmap.Lock(); + } + + 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"); @@ -156,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/HitTesting.cs b/tests/Avalonia.Skia.UnitTests/HitTesting.cs index dceb0cdb9b..df267ee136 100644 --- a/tests/Avalonia.Skia.UnitTests/HitTesting.cs +++ b/tests/Avalonia.Skia.UnitTests/HitTesting.cs @@ -30,7 +30,7 @@ namespace Avalonia.Skia.UnitTests } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -64,7 +64,7 @@ namespace Avalonia.Skia.UnitTests } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 643bf513d9..1b6fd537eb 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -583,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(); @@ -602,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 f8e287d0a7..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; @@ -636,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; @@ -734,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; @@ -776,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) { @@ -822,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); @@ -887,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)); @@ -934,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/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index 0f951ed867..f9e1e45098 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -9,7 +9,7 @@ using Moq; namespace Avalonia.UnitTests { - public class MockPlatformRenderInterface : IPlatformRenderInterface + public class MockPlatformRenderInterface : IPlatformRenderInterface, IPlatformRenderInterfaceContext { public IGeometryImpl CreateEllipseGeometry(Rect rect) { @@ -48,6 +48,8 @@ namespace Avalonia.UnitTests return m.Object; } + + public bool IsCorrupted => false; } public IRenderTarget CreateRenderTarget(IEnumerable surfaces) @@ -55,6 +57,8 @@ namespace Avalonia.UnitTests return new MockRenderTarget(); } + public object TryGetFeature(Type featureType) => null; + public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) { return Mock.Of(); @@ -147,6 +151,8 @@ namespace Avalonia.UnitTests return Mock.Of(); } + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => this; + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { return Mock.Of(); @@ -172,5 +178,8 @@ namespace Avalonia.UnitTests public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul; public PixelFormat DefaultPixelFormat => PixelFormat.Rgba8888; + public void Dispose() + { + } } } 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/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