diff --git a/.gitignore b/.gitignore index a9a8fd36b4..583a2b8a2b 100644 --- a/.gitignore +++ b/.gitignore @@ -176,5 +176,9 @@ nuget Avalonia.XBuild.sln project.lock.json .idea/* -**/obj-Skia/* -**/obj-Direct2D1/* + + +################## +## BenchmarkDotNet +################## +BenchmarkDotNet.Artifacts/ diff --git a/.ncrunch/Avalonia.Designer.HostApp.NetFX.v3.ncrunchproject b/.ncrunch/Avalonia.Designer.HostApp.NetFX.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/Avalonia.Designer.HostApp.NetFX.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/BindingDemo.net461.v3.ncrunchproject b/.ncrunch/BindingDemo.net461.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/BindingDemo.net461.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/BindingDemo.netcoreapp2.0.v3.ncrunchproject b/.ncrunch/BindingDemo.netcoreapp2.0.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/BindingDemo.netcoreapp2.0.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/Previewer.v3.ncrunchproject b/.ncrunch/Previewer.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/Previewer.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/RemoteDemo.v3.ncrunchproject b/.ncrunch/RemoteDemo.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/RemoteDemo.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/RenderDemo.net461.v3.ncrunchproject b/.ncrunch/RenderDemo.net461.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/RenderDemo.net461.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/RenderDemo.netcoreapp2.0.v3.ncrunchproject b/.ncrunch/RenderDemo.netcoreapp2.0.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/RenderDemo.netcoreapp2.0.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/VirtualizationDemo.net461.v3.ncrunchproject b/.ncrunch/VirtualizationDemo.net461.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/VirtualizationDemo.net461.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/VirtualizationDemo.netcoreapp2.0.v3.ncrunchproject b/.ncrunch/VirtualizationDemo.netcoreapp2.0.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/VirtualizationDemo.netcoreapp2.0.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/Avalonia.sln b/Avalonia.sln index 54f6f5e7e7..75bca0bca8 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -71,7 +71,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup", "src\Mark EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.UnitTests", "tests\Avalonia.Markup.UnitTests\Avalonia.Markup.UnitTests.csproj", "{8EF392D5-1416-45AA-9956-7CBBC3229E8A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BindingTest", "samples\BindingTest\BindingTest.csproj", "{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BindingDemo", "samples\BindingDemo\BindingDemo.csproj", "{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}" EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "RenderHelpers", "src\Shared\RenderHelpers\RenderHelpers.shproj", "{3C4C0CB4-0C0F-4450-A37B-148C84FF905F}" EndProject @@ -109,7 +109,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.Te EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.TestApp", "tests\Avalonia.DesignerSupport.TestApp\Avalonia.DesignerSupport.TestApp.csproj", "{F1381F98-4D24-409A-A6C5-1C5B1E08BB08}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtualizationTest", "samples\VirtualizationTest\VirtualizationTest.csproj", "{FBCAF3D0-2808-4934-8E96-3F607594517B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtualizationDemo", "samples\VirtualizationDemo\VirtualizationDemo.csproj", "{FBCAF3D0-2808-4934-8E96-3F607594517B}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Interop", "Interop", "{A0CC0258-D18C-4AB3-854F-7101680FC3F9}" EndProject @@ -117,7 +117,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsInteropTest", "sampl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DotNetFrameworkRuntime", "src\Avalonia.DotNetFrameworkRuntime\Avalonia.DotNetFrameworkRuntime.csproj", "{4A1ABB09-9047-4BD5-A4AD-A055E52C5EE0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RenderTest", "samples\RenderTest\RenderTest.csproj", "{F1FDC5B0-4654-416F-AE69-E3E9BBD87801}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RenderDemo", "samples\RenderDemo\RenderDemo.csproj", "{F1FDC5B0-4654-416F-AE69-E3E9BBD87801}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Android", "samples\ControlCatalog.Android\ControlCatalog.Android.csproj", "{29132311-1848-4FD6-AE0C-4FF841151BD3}" EndProject @@ -141,6 +141,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\Microsoft.Reactive.Testing.props = build\Microsoft.Reactive.Testing.props build\Moq.props = build\Moq.props build\NetCore.props = build\NetCore.props + build\NetFX.props = build\NetFX.props build\ReactiveUI.props = build\ReactiveUI.props build\Rx.props = build\Rx.props build\SampleApp.props = build\SampleApp.props @@ -169,7 +170,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia.RenderTests", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Remote.Protocol", "src\Avalonia.Remote.Protocol\Avalonia.Remote.Protocol.csproj", "{D78A720C-C0C6-478B-8564-F167F9BDD01B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RemoteTest", "samples\RemoteTest\RemoteTest.csproj", "{E2999E4A-9086-401F-898C-AEB0AD38E676}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RemoteDemo", "samples\RemoteDemo\RemoteDemo.csproj", "{E2999E4A-9086-401F-898C-AEB0AD38E676}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{4ED8B739-6F4E-4CD4-B993-545E6B5CE637}" EndProject @@ -183,6 +184,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.MonoMac", "src\OSX EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Designer.HostApp.NetFX", "src\tools\Avalonia.Designer.HostApp.NetFX\Avalonia.Designer.HostApp.NetFX.csproj", "{4ADA61C8-D191-428D-9066-EF4F0D86520F}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia.UnitTests", "tests\Avalonia.Skia.UnitTests\Avalonia.Skia.UnitTests.csproj", "{E1240B49-7B4B-4371-A00E-068778C5CF0B}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -395,6 +398,7 @@ Global {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Debug|NetCoreOnly.ActiveCfg = Debug|Any CPU + {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Debug|NetCoreOnly.Build.0 = Debug|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Debug|x86.ActiveCfg = Debug|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Debug|x86.Build.0 = Debug|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -404,6 +408,7 @@ Global {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|NetCoreOnly.ActiveCfg = Release|Any CPU + {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|NetCoreOnly.Build.0 = Release|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|x86.ActiveCfg = Release|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|x86.Build.0 = Release|Any CPU {62024B2D-53EB-4638-B26B-85EEAA54866E}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU @@ -2468,6 +2473,46 @@ Global {4ADA61C8-D191-428D-9066-EF4F0D86520F}.Release|NetCoreOnly.ActiveCfg = Release|Any CPU {4ADA61C8-D191-428D-9066-EF4F0D86520F}.Release|x86.ActiveCfg = Release|Any CPU {4ADA61C8-D191-428D-9066-EF4F0D86520F}.Release|x86.Build.0 = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|NetCoreOnly.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|NetCoreOnly.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhone.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|NetCoreOnly.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|NetCoreOnly.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|x86.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|x86.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhone.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|NetCoreOnly.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|NetCoreOnly.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|x86.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|x86.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|Any CPU.Build.0 = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhone.ActiveCfg = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhone.Build.0 = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|NetCoreOnly.ActiveCfg = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|NetCoreOnly.Build.0 = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|x86.ActiveCfg = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2520,6 +2565,7 @@ Global {F40FC0A2-1BC3-401C-BFC1-928EC4D4A9CE} = {9B9E3891-2366-4253-A952-D08BCEB71098} {CBFD5788-567D-401B-9DFA-74E4224025A0} = {A59C4C0A-64DF-4621-B450-2BA00D6F61E2} {4ADA61C8-D191-428D-9066-EF4F0D86520F} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} + {E1240B49-7B4B-4371-A00E-068778C5CF0B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/build.cake b/build.cake index bf3ae41b58..78c166a3bc 100644 --- a/build.cake +++ b/build.cake @@ -6,6 +6,7 @@ #addin "nuget:?package=NuGet.Core&version=2.14.0" #tool "nuget:?package=NuGet.CommandLine&version=4.3.0" #tool "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2017.1.20170613.162720" + /////////////////////////////////////////////////////////////////////////////// // TOOLS /////////////////////////////////////////////////////////////////////////////// @@ -98,7 +99,7 @@ Teardown((context, buildContext) => // TASKS /////////////////////////////////////////////////////////////////////////////// -Task("Clean") +Task("Clean-Impl") .Does(data => { CleanDirectories(data.Parameters.BuildDirs); @@ -108,9 +109,9 @@ Task("Clean") CleanDirectory(data.Parameters.BinRoot); }); -Task("Restore-NuGet-Packages") - .IsDependentOn("Clean") +Task("Restore-NuGet-Packages-Impl") .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) + .WithCriteria((context, data) => !data.Parameters.IsPlatformNetCoreOnly) .Does(data => { var maxRetryCount = 5; @@ -148,11 +149,10 @@ void DotNetCoreBuild(Parameters parameters) DotNetCoreBuild(parameters.MSBuildSolution, settings); } -Task("Build") - .IsDependentOn("Restore-NuGet-Packages") +Task("Build-Impl") .Does(data => { - if(data.Parameters.IsRunningOnWindows) + if(data.Parameters.IsRunningOnWindows && !data.Parameters.IsPlatformNetCoreOnly) { MSBuild(data.Parameters.MSBuildSolution, settings => { settings.SetConfiguration(data.Parameters.Configuration); @@ -171,7 +171,6 @@ Task("Build") } }); - void RunCoreTest(string project, Parameters parameters, bool coreOnly = false) { if(!project.EndsWith(".csproj")) @@ -194,93 +193,91 @@ void RunCoreTest(string project, Parameters parameters, bool coreOnly = false) } } -Task("Run-Unit-Tests") - .IsDependentOn("Build") +Task("Run-Unit-Tests-Impl") .WithCriteria((context, data) => !data.Parameters.SkipTests) - .Does(data => { - RunCoreTest("./tests/Avalonia.Base.UnitTests", data.Parameters, false); - RunCoreTest("./tests/Avalonia.Controls.UnitTests", data.Parameters, false); - RunCoreTest("./tests/Avalonia.Input.UnitTests", data.Parameters, false); - RunCoreTest("./tests/Avalonia.Interactivity.UnitTests", data.Parameters, false); - RunCoreTest("./tests/Avalonia.Layout.UnitTests", data.Parameters, false); - RunCoreTest("./tests/Avalonia.Markup.UnitTests", data.Parameters, false); - RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", data.Parameters, false); - RunCoreTest("./tests/Avalonia.Styling.UnitTests", data.Parameters, false); - RunCoreTest("./tests/Avalonia.Visuals.UnitTests", data.Parameters, false); - if (data.Parameters.IsRunningOnWindows) - { - RunCoreTest("./tests/Avalonia.Direct2D1.UnitTests", data.Parameters, true); - } - }); + .Does(data => +{ + RunCoreTest("./tests/Avalonia.Base.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Controls.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Input.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Interactivity.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Layout.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Markup.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Styling.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Visuals.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Skia.UnitTests", data.Parameters, false); + if (data.Parameters.IsRunningOnWindows && !data.Parameters.IsPlatformNetCoreOnly) + { + RunCoreTest("./tests/Avalonia.Direct2D1.UnitTests", data.Parameters, true); + } +}); -Task("Run-Designer-Tests") - .IsDependentOn("Build") +Task("Run-Designer-Tests-Impl") .WithCriteria((context, data) => !data.Parameters.SkipTests) - .Does(data => { - RunCoreTest("./tests/Avalonia.DesignerSupport.Tests", data.Parameters, false); - }); + .Does(data => +{ + RunCoreTest("./tests/Avalonia.DesignerSupport.Tests", data.Parameters, false); +}); -Task("Run-Render-Tests") - .IsDependentOn("Build") - .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) - .Does(data => { - RunCoreTest("./tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj", data.Parameters, true); - RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj", data.Parameters, true); - }); +Task("Run-Render-Tests-Impl") + .WithCriteria((context, data) => !data.Parameters.SkipTests) + .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) + .WithCriteria((context, data) => !data.Parameters.IsPlatformNetCoreOnly) + .Does(data => +{ + RunCoreTest("./tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj", data.Parameters, true); + RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj", data.Parameters, true); +}); -Task("Run-Leak-Tests") - .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) - .IsDependentOn("Build") +Task("Run-Leak-Tests-Impl") + .WithCriteria((context, data) => !data.Parameters.SkipTests) + .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) + .WithCriteria((context, data) => !data.Parameters.IsPlatformNetCoreOnly) .Does(() => +{ + var dotMemoryUnit = Context.Tools.Resolve("dotMemoryUnit.exe"); + var leakTestsExitCode = StartProcess(dotMemoryUnit, new ProcessSettings { - var dotMemoryUnit = Context.Tools.Resolve("dotMemoryUnit.exe"); - var leakTestsExitCode = StartProcess(dotMemoryUnit, new ProcessSettings - { - Arguments = new ProcessArgumentBuilder() - .Append(Context.Tools.Resolve("xunit.console.x86.exe").FullPath) - .Append("--propagate-exit-code") - .Append("--") - .Append("tests\\Avalonia.LeakTests\\bin\\Release\\net461\\Avalonia.LeakTests.dll"), - Timeout = 120000 - }); - - if (leakTestsExitCode != 0) - { - throw new Exception("Leak Tests failed"); - } + Arguments = new ProcessArgumentBuilder() + .Append(Context.Tools.Resolve("xunit.console.x86.exe").FullPath) + .Append("--propagate-exit-code") + .Append("--") + .Append("tests\\Avalonia.LeakTests\\bin\\Release\\net461\\Avalonia.LeakTests.dll"), + Timeout = 120000 }); -Task("Run-Tests") - .IsDependentOn("Run-Unit-Tests") - .IsDependentOn("Run-Render-Tests") - .IsDependentOn("Run-Designer-Tests") - .IsDependentOn("Run-Leak-Tests"); + if (leakTestsExitCode != 0) + { + throw new Exception("Leak Tests failed"); + } +}); -Task("Copy-Files") - .IsDependentOn("Run-Tests") +Task("Copy-Files-Impl") .Does(data => { CopyFiles(data.Packages.BinFiles, data.Parameters.BinRoot); }); -Task("Zip-Files") - .IsDependentOn("Copy-Files") +Task("Zip-Files-Impl") .Does(data => { Zip(data.Parameters.BinRoot, data.Parameters.ZipCoreArtifacts); - Zip(data.Parameters.ZipSourceControlCatalogDesktopDirs, - data.Parameters.ZipTargetControlCatalogDesktopDirs, - GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.dll") + - GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.config") + - GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.so") + - GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.dylib") + - GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.exe")); + Zip(data.Parameters.NugetRoot, data.Parameters.ZipNuGetArtifacts); + + if (!data.Parameters.IsPlatformNetCoreOnly) { + Zip(data.Parameters.ZipSourceControlCatalogDesktopDirs, + data.Parameters.ZipTargetControlCatalogDesktopDirs, + GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.dll") + + GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.config") + + GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.so") + + GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.dylib") + + GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.exe")); + } }); -Task("Create-NuGet-Packages") - .IsDependentOn("Run-Tests") - .IsDependentOn("Inspect") +Task("Create-NuGet-Packages-Impl") .Does(data => { foreach(var nuspec in data.Packages.NuspecNuGetSettings) @@ -289,8 +286,7 @@ Task("Create-NuGet-Packages") } }); -Task("Publish-MyGet") - .IsDependentOn("Create-NuGet-Packages") +Task("Publish-MyGet-Impl") .WithCriteria((context, data) => !data.Parameters.IsLocalBuild) .WithCriteria((context, data) => !data.Parameters.IsPullRequest) .WithCriteria((context, data) => data.Parameters.IsMainRepo) @@ -323,8 +319,7 @@ Task("Publish-MyGet") Information("Publish-MyGet Task failed, but continuing with next Task..."); }); -Task("Publish-NuGet") - .IsDependentOn("Create-NuGet-Packages") +Task("Publish-NuGet-Impl") .WithCriteria((context, data) => !data.Parameters.IsLocalBuild) .WithCriteria((context, data) => !data.Parameters.IsPullRequest) .WithCriteria((context, data) => data.Parameters.IsMainRepo) @@ -356,54 +351,67 @@ Task("Publish-NuGet") Information("Publish-NuGet Task failed, but continuing with next Task..."); }); -Task("Inspect") +Task("Inspect-Impl") .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) - .IsDependentOn("Restore-NuGet-Packages") + .WithCriteria((context, data) => !data.Parameters.IsPlatformNetCoreOnly) .Does(() => - { - var badIssues = new []{"PossibleNullReferenceException"}; - var whitelist = new []{"tests", "src\\android", "src\\ios", - "src\\markup\\avalonia.markup.xaml\\portablexaml\\portable.xaml.github"}; - Information("Running code inspections"); - - var exitCode = StartProcess(Context.Tools.Resolve("inspectcode.exe"), - new ProcessSettings - { - Arguments = "--output=artifacts\\inspectcode.xml --profile=Avalonia.sln.DotSettings Avalonia.sln", - RedirectStandardOutput = true - }); +{ + var badIssues = new []{"PossibleNullReferenceException"}; + var whitelist = new []{"tests", "src\\android", "src\\ios", + "src\\markup\\avalonia.markup.xaml\\portablexaml\\portable.xaml.github"}; + Information("Running code inspections"); + + var exitCode = StartProcess(Context.Tools.Resolve("inspectcode.exe"), + new ProcessSettings + { + Arguments = "--output=artifacts\\inspectcode.xml --profile=Avalonia.sln.DotSettings Avalonia.sln", + RedirectStandardOutput = true + }); - Information("Analyzing report"); - var doc = XDocument.Parse(System.IO.File.ReadAllText("artifacts\\inspectcode.xml")); - var failBuild = false; - foreach(var xml in doc.Descendants("Issue")) + Information("Analyzing report"); + var doc = XDocument.Parse(System.IO.File.ReadAllText("artifacts\\inspectcode.xml")); + var failBuild = false; + foreach(var xml in doc.Descendants("Issue")) + { + var typeId = xml.Attribute("TypeId").Value.ToString(); + if(badIssues.Contains(typeId)) { - var typeId = xml.Attribute("TypeId").Value.ToString(); - if(badIssues.Contains(typeId)) - { - var file = xml.Attribute("File").Value.ToString().ToLower(); - if(whitelist.Any(wh => file.StartsWith(wh))) - continue; - var line = xml.Attribute("Line").Value.ToString(); - Error(typeId + " - " + file + " on line " + line); - failBuild = true; - } + var file = xml.Attribute("File").Value.ToString().ToLower(); + if(whitelist.Any(wh => file.StartsWith(wh))) + continue; + var line = xml.Attribute("Line").Value.ToString(); + Error(typeId + " - " + file + " on line " + line); + failBuild = true; } - if(failBuild) - throw new Exception("Issues found"); - }); + } + if(failBuild) + throw new Exception("Issues found"); +}); /////////////////////////////////////////////////////////////////////////////// // TARGETS /////////////////////////////////////////////////////////////////////////////// +Task("Run-Tests") + .IsDependentOn("Clean-Impl") + .IsDependentOn("Restore-NuGet-Packages-Impl") + .IsDependentOn("Build-Impl") + .IsDependentOn("Run-Unit-Tests-Impl") + .IsDependentOn("Run-Render-Tests-Impl") + .IsDependentOn("Run-Designer-Tests-Impl") + .IsDependentOn("Run-Leak-Tests-Impl"); + Task("Package") - .IsDependentOn("Create-NuGet-Packages"); + .IsDependentOn("Run-Tests") + .IsDependentOn("Inspect-Impl") + .IsDependentOn("Create-NuGet-Packages-Impl"); Task("AppVeyor") - .IsDependentOn("Zip-Files") - .IsDependentOn("Publish-MyGet") - .IsDependentOn("Publish-NuGet"); + .IsDependentOn("Package") + .IsDependentOn("Copy-Files-Impl") + .IsDependentOn("Zip-Files-Impl") + .IsDependentOn("Publish-MyGet-Impl") + .IsDependentOn("Publish-NuGet-Impl"); Task("Travis") .IsDependentOn("Run-Tests"); diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index 04e8a3ad4f..35c979a95e 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/build/System.Memory.props b/build/System.Memory.props new file mode 100644 index 0000000000..f3253f8882 --- /dev/null +++ b/build/System.Memory.props @@ -0,0 +1,5 @@ + + + + + diff --git a/packages.cake b/packages.cake index 7e7e722c82..51d9c48159 100644 --- a/packages.cake +++ b/packages.cake @@ -1,3 +1,6 @@ +using System; +using System.Collections; +using System.Collections.Generic; using System.Xml.Linq; public class Packages @@ -9,12 +12,11 @@ public class Packages public string SkiaSharpVersion {get; private set; } public string SkiaSharpLinuxVersion {get; private set; } public Dictionary>> PackageVersions{get; private set;} - - - + class DependencyBuilder : List { Packages _parent; + public DependencyBuilder(Packages parent) { _parent = parent; @@ -24,8 +26,7 @@ public class Packages { return _parent.PackageVersions[name].First().Item1; } - - + public DependencyBuilder Dep(string name, params string[] fws) { if(fws.Length == 0) @@ -212,17 +213,33 @@ public class Packages }; }); - var toolsContent = new[] { - new NuSpecContent{ - Source = ((FilePath)context.File("./src/tools/Avalonia.Designer.HostApp/bin/" + parameters.DirSuffix + "/netcoreapp2.0/Avalonia.Designer.HostApp.dll")).FullPath, - Target = "tools/netcoreapp2.0/previewer" - }, - new NuSpecContent{ - Source = ((FilePath)context.File("./src/tools/Avalonia.Designer.HostApp.NetFx/bin/" + parameters.DirSuffix + "/Avalonia.Designer.HostApp.exe")).FullPath, - Target = "tools/net461/previewer" - } + var toolHostApp = new NuSpecContent{ + Source = ((FilePath)context.File("./src/tools/Avalonia.Designer.HostApp/bin/" + parameters.DirSuffix + "/netcoreapp2.0/Avalonia.Designer.HostApp.dll")).FullPath, + Target = "tools/netcoreapp2.0/previewer" + }; + + var toolHostAppNetFx = new NuSpecContent{ + Source = ((FilePath)context.File("./src/tools/Avalonia.Designer.HostApp.NetFx/bin/" + parameters.DirSuffix + "/Avalonia.Designer.HostApp.exe")).FullPath, + Target = "tools/net461/previewer" }; + IList coreFiles; + + if (!parameters.IsPlatformNetCoreOnly) { + var toolsContent = new[] { toolHostApp, toolHostAppNetFx }; + coreFiles = coreLibrariesNuSpecContent + .Concat(win32CoreLibrariesNuSpecContent).Concat(net45RuntimePlatform) + .Concat(netcoreappCoreLibrariesNuSpecContent).Concat(netCoreRuntimePlatform) + .Concat(toolsContent) + .ToList(); + } else { + var toolsContent = new[] { toolHostApp }; + coreFiles = coreLibrariesNuSpecContent + .Concat(netcoreappCoreLibrariesNuSpecContent).Concat(netCoreRuntimePlatform) + .Concat(toolsContent) + .ToList(); + } + var nuspecNuGetSettingsCore = new [] { /////////////////////////////////////////////////////////////////////////////// @@ -253,13 +270,9 @@ public class Packages } .Deps(new string[]{null, "netcoreapp2.0"}, "System.ValueTuple", "System.ComponentModel.TypeConverter", "System.ComponentModel.Primitives", - "System.Runtime.Serialization.Primitives", "System.Xml.XmlDocument", "System.Xml.ReaderWriter") + "System.Runtime.Serialization.Primitives", "System.Xml.XmlDocument", "System.Xml.ReaderWriter", "System.Memory") .ToArray(), - Files = coreLibrariesNuSpecContent - .Concat(win32CoreLibrariesNuSpecContent).Concat(net45RuntimePlatform) - .Concat(netcoreappCoreLibrariesNuSpecContent).Concat(netCoreRuntimePlatform) - .Concat(toolsContent) - .ToList(), + Files = coreFiles, BasePath = context.Directory("./"), OutputDirectory = parameters.NugetRoot }, @@ -451,22 +464,6 @@ public class Packages BasePath = context.Directory("./"), OutputDirectory = parameters.NugetRoot }, - new NuGetPackSettings() - { - Id = "Avalonia.Win32.Interoperability", - Dependencies = new [] - { - new NuSpecDependency() { Id = "Avalonia.Win32", Version = parameters.Version }, - new NuSpecDependency() { Id = "Avalonia.Direct2D1", Version = parameters.Version }, - new NuSpecDependency() { Id = "SharpDX.Direct3D9", Version = SharpDXDirect3D9Version }, - }, - Files = new [] - { - new NuSpecContent { Source = "Avalonia.Win32.Interop/bin/" + parameters.DirSuffix + "/Avalonia.Win32.Interop.dll", Target = "lib/net45" } - }, - BasePath = context.Directory("./src/Windows"), - OutputDirectory = parameters.NugetRoot - }, /////////////////////////////////////////////////////////////////////////////// // Avalonia.LinuxFramebuffer /////////////////////////////////////////////////////////////////////////////// @@ -487,11 +484,32 @@ public class Packages } }; + var nuspecNuGetSettingInterop = new NuGetPackSettings() + { + Id = "Avalonia.Win32.Interoperability", + Dependencies = new [] + { + new NuSpecDependency() { Id = "Avalonia.Win32", Version = parameters.Version }, + new NuSpecDependency() { Id = "Avalonia.Direct2D1", Version = parameters.Version }, + new NuSpecDependency() { Id = "SharpDX.Direct3D9", Version = SharpDXDirect3D9Version }, + }, + Files = new [] + { + new NuSpecContent { Source = "Avalonia.Win32.Interop/bin/" + parameters.DirSuffix + "/Avalonia.Win32.Interop.dll", Target = "lib/net45" } + }, + BasePath = context.Directory("./src/Windows"), + OutputDirectory = parameters.NugetRoot + }; + NuspecNuGetSettings = new List(); NuspecNuGetSettings.AddRange(nuspecNuGetSettingsCore); NuspecNuGetSettings.AddRange(nuspecNuGetSettingsDesktop); - NuspecNuGetSettings.AddRange(nuspecNuGetSettingsMobile); + + if (!parameters.IsPlatformNetCoreOnly) { + NuspecNuGetSettings.Add(nuspecNuGetSettingInterop); + NuspecNuGetSettings.AddRange(nuspecNuGetSettingsMobile); + } NuspecNuGetSettings.ForEach((nuspec) => SetNuGetNuspecCommonProperties(nuspec)); diff --git a/parameters.cake b/parameters.cake index ffd472cbd4..e595b8159f 100644 --- a/parameters.cake +++ b/parameters.cake @@ -8,11 +8,11 @@ public class Parameters public string AssemblyInfoPath { get; private set; } public string ReleasePlatform { get; private set; } public string ReleaseConfiguration { get; private set; } - public string MSBuildSolution { get; private set; } - public string XBuildSolution { get; private set; } + public string MSBuildSolution { get; private set; } public bool IsPlatformAnyCPU { get; private set; } public bool IsPlatformX86 { get; private set; } public bool IsPlatformX64 { get; private set; } + public bool IsPlatformNetCoreOnly { get; private set; } public bool IsLocalBuild { get; private set; } public bool IsRunningOnUnix { get; private set; } public bool IsRunningOnWindows { get; private set; } @@ -34,6 +34,7 @@ public class Parameters public DirectoryPathCollection BuildDirs { get; private set; } public string FileZipSuffix { get; private set; } public FilePath ZipCoreArtifacts { get; private set; } + public FilePath ZipNuGetArtifacts { get; private set; } public DirectoryPath ZipSourceControlCatalogDesktopDirs { get; private set; } public FilePath ZipTargetControlCatalogDesktopDirs { get; private set; } @@ -53,12 +54,12 @@ public class Parameters ReleasePlatform = "Any CPU"; ReleaseConfiguration = "Release"; MSBuildSolution = "./Avalonia.sln"; - XBuildSolution = "./Avalonia.XBuild.sln"; // PARAMETERS IsPlatformAnyCPU = StringComparer.OrdinalIgnoreCase.Equals(Platform, "Any CPU"); IsPlatformX86 = StringComparer.OrdinalIgnoreCase.Equals(Platform, "x86"); IsPlatformX64 = StringComparer.OrdinalIgnoreCase.Equals(Platform, "x64"); + IsPlatformNetCoreOnly = StringComparer.OrdinalIgnoreCase.Equals(Platform, "NetCoreOnly"); IsLocalBuild = buildSystem.IsLocalBuild; IsRunningOnUnix = context.IsRunningOnUnix(); IsRunningOnWindows = context.IsRunningOnWindows(); @@ -71,7 +72,6 @@ public class Parameters IsReleasable = StringComparer.OrdinalIgnoreCase.Equals(ReleasePlatform, Platform) && StringComparer.OrdinalIgnoreCase.Equals(ReleaseConfiguration, Configuration); IsMyGetRelease = !IsTagged && IsReleasable; - // VERSION Version = context.Argument("force-nuget-version", context.ParseAssemblyInfo(AssemblyInfoPath).AssemblyVersion); @@ -103,14 +103,12 @@ public class Parameters NugetRoot = ArtifactsDir.Combine("nuget"); ZipRoot = ArtifactsDir.Combine("zip"); BinRoot = ArtifactsDir.Combine("bin"); - BuildDirs = context.GetDirectories("**/bin") + context.GetDirectories("**/obj"); - DirSuffix = Configuration; DirSuffixIOS = "iPhone" + "/" + Configuration; - FileZipSuffix = Version + ".zip"; ZipCoreArtifacts = ZipRoot.CombineWithFilePath("Avalonia-" + FileZipSuffix); + ZipNuGetArtifacts = ZipRoot.CombineWithFilePath("Avalonia-NuGet-" + FileZipSuffix); ZipSourceControlCatalogDesktopDirs = (DirectoryPath)context.Directory("./samples/ControlCatalog.Desktop/bin/" + DirSuffix + "/net461"); ZipTargetControlCatalogDesktopDirs = ZipRoot.CombineWithFilePath("ControlCatalog.Desktop-" + FileZipSuffix); } diff --git a/samples/BindingTest/App.config b/samples/BindingDemo/App.config similarity index 100% rename from samples/BindingTest/App.config rename to samples/BindingDemo/App.config diff --git a/samples/BindingTest/App.xaml b/samples/BindingDemo/App.xaml similarity index 100% rename from samples/BindingTest/App.xaml rename to samples/BindingDemo/App.xaml diff --git a/samples/BindingTest/App.xaml.cs b/samples/BindingDemo/App.xaml.cs similarity index 95% rename from samples/BindingTest/App.xaml.cs rename to samples/BindingDemo/App.xaml.cs index ccad1d0ba9..01c52a2a49 100644 --- a/samples/BindingTest/App.xaml.cs +++ b/samples/BindingDemo/App.xaml.cs @@ -5,7 +5,7 @@ using Avalonia.Logging.Serilog; using Avalonia.Markup.Xaml; using Serilog; -namespace BindingTest +namespace BindingDemo { public class App : Application { diff --git a/samples/BindingTest/BindingTest.csproj b/samples/BindingDemo/BindingDemo.csproj similarity index 100% rename from samples/BindingTest/BindingTest.csproj rename to samples/BindingDemo/BindingDemo.csproj diff --git a/samples/BindingTest/MainWindow.xaml b/samples/BindingDemo/MainWindow.xaml similarity index 98% rename from samples/BindingTest/MainWindow.xaml rename to samples/BindingDemo/MainWindow.xaml index 6b80225686..a69fb75742 100644 --- a/samples/BindingTest/MainWindow.xaml +++ b/samples/BindingDemo/MainWindow.xaml @@ -1,7 +1,7 @@ diff --git a/samples/BindingTest/MainWindow.xaml.cs b/samples/BindingDemo/MainWindow.xaml.cs similarity index 88% rename from samples/BindingTest/MainWindow.xaml.cs rename to samples/BindingDemo/MainWindow.xaml.cs index c1c3c09406..eaa57e1f5f 100644 --- a/samples/BindingTest/MainWindow.xaml.cs +++ b/samples/BindingDemo/MainWindow.xaml.cs @@ -1,9 +1,9 @@ -using BindingTest.ViewModels; +using BindingDemo.ViewModels; using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; -namespace BindingTest +namespace BindingDemo { public class MainWindow : Window { diff --git a/samples/BindingTest/TestItemView.xaml b/samples/BindingDemo/TestItemView.xaml similarity index 100% rename from samples/BindingTest/TestItemView.xaml rename to samples/BindingDemo/TestItemView.xaml diff --git a/samples/BindingTest/TestItemView.xaml.cs b/samples/BindingDemo/TestItemView.xaml.cs similarity index 93% rename from samples/BindingTest/TestItemView.xaml.cs rename to samples/BindingDemo/TestItemView.xaml.cs index 32f367ef68..8c0b592f00 100644 --- a/samples/BindingTest/TestItemView.xaml.cs +++ b/samples/BindingDemo/TestItemView.xaml.cs @@ -1,7 +1,7 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; -namespace BindingTest +namespace BindingDemo { public class TestItemView : UserControl { diff --git a/samples/BindingTest/ViewModels/DataAnnotationsErrorViewModel.cs b/samples/BindingDemo/ViewModels/DataAnnotationsErrorViewModel.cs similarity index 92% rename from samples/BindingTest/ViewModels/DataAnnotationsErrorViewModel.cs rename to samples/BindingDemo/ViewModels/DataAnnotationsErrorViewModel.cs index 634498c165..e274f9180e 100644 --- a/samples/BindingTest/ViewModels/DataAnnotationsErrorViewModel.cs +++ b/samples/BindingDemo/ViewModels/DataAnnotationsErrorViewModel.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; -namespace BindingTest.ViewModels +namespace BindingDemo.ViewModels { public class DataAnnotationsErrorViewModel { diff --git a/samples/BindingTest/ViewModels/ExceptionErrorViewModel.cs b/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs similarity index 95% rename from samples/BindingTest/ViewModels/ExceptionErrorViewModel.cs rename to samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs index e6071e0678..2ab6c26e68 100644 --- a/samples/BindingTest/ViewModels/ExceptionErrorViewModel.cs +++ b/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs @@ -4,7 +4,7 @@ using ReactiveUI; using System; -namespace BindingTest.ViewModels +namespace BindingDemo.ViewModels { public class ExceptionErrorViewModel : ReactiveObject { diff --git a/samples/BindingTest/ViewModels/IndeiErrorViewModel.cs b/samples/BindingDemo/ViewModels/IndeiErrorViewModel.cs similarity index 98% rename from samples/BindingTest/ViewModels/IndeiErrorViewModel.cs rename to samples/BindingDemo/ViewModels/IndeiErrorViewModel.cs index b4bb528abb..bb3b4d64e9 100644 --- a/samples/BindingTest/ViewModels/IndeiErrorViewModel.cs +++ b/samples/BindingDemo/ViewModels/IndeiErrorViewModel.cs @@ -6,7 +6,7 @@ using System; using System.ComponentModel; using System.Collections; -namespace BindingTest.ViewModels +namespace BindingDemo.ViewModels { public class IndeiErrorViewModel : ReactiveObject, INotifyDataErrorInfo { diff --git a/samples/BindingTest/ViewModels/MainWindowViewModel.cs b/samples/BindingDemo/ViewModels/MainWindowViewModel.cs similarity index 99% rename from samples/BindingTest/ViewModels/MainWindowViewModel.cs rename to samples/BindingDemo/ViewModels/MainWindowViewModel.cs index 1116810ccb..858fb5159a 100644 --- a/samples/BindingTest/ViewModels/MainWindowViewModel.cs +++ b/samples/BindingDemo/ViewModels/MainWindowViewModel.cs @@ -6,7 +6,7 @@ using System.Reactive.Linq; using System.Threading.Tasks; using System.Threading; -namespace BindingTest.ViewModels +namespace BindingDemo.ViewModels { public class MainWindowViewModel : ReactiveObject { diff --git a/samples/BindingTest/ViewModels/NestedCommandViewModel.cs b/samples/BindingDemo/ViewModels/NestedCommandViewModel.cs similarity index 92% rename from samples/BindingTest/ViewModels/NestedCommandViewModel.cs rename to samples/BindingDemo/ViewModels/NestedCommandViewModel.cs index 886ecbed8e..0e9139ab98 100644 --- a/samples/BindingTest/ViewModels/NestedCommandViewModel.cs +++ b/samples/BindingDemo/ViewModels/NestedCommandViewModel.cs @@ -6,7 +6,7 @@ using System.Text; using System.Threading.Tasks; using System.Windows.Input; -namespace BindingTest.ViewModels +namespace BindingDemo.ViewModels { public class NestedCommandViewModel : ReactiveObject { diff --git a/samples/BindingTest/ViewModels/TestItem.cs b/samples/BindingDemo/ViewModels/TestItem.cs similarity index 93% rename from samples/BindingTest/ViewModels/TestItem.cs rename to samples/BindingDemo/ViewModels/TestItem.cs index 2326a92b7d..5a9f192f58 100644 --- a/samples/BindingTest/ViewModels/TestItem.cs +++ b/samples/BindingDemo/ViewModels/TestItem.cs @@ -1,6 +1,6 @@ using ReactiveUI; -namespace BindingTest.ViewModels +namespace BindingDemo.ViewModels { public class TestItem : ReactiveObject { diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index b45a93455e..1f53dedc14 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -1,9 +1,9 @@ using System; using System.Diagnostics; using System.Linq; -using System.Runtime.InteropServices; using System.Threading; using Avalonia; +using Avalonia.Skia; namespace ControlCatalog.NetCore { @@ -37,7 +37,7 @@ namespace ControlCatalog.NetCore /// This method is needed for IDE previewer infrastructure /// public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure().UsePlatformDetect().UseReactiveUI(); + => AppBuilder.Configure().UsePlatformDetect().UseSkia().UseReactiveUI(); static void ConsoleSilencer() { diff --git a/samples/RemoteTest/Program.cs b/samples/RemoteDemo/Program.cs similarity index 98% rename from samples/RemoteTest/Program.cs rename to samples/RemoteDemo/Program.cs index f518e77143..0565b676fb 100644 --- a/samples/RemoteTest/Program.cs +++ b/samples/RemoteDemo/Program.cs @@ -9,7 +9,7 @@ using Avalonia.Remote.Protocol; using Avalonia.Threading; using ControlCatalog; -namespace RemoteTest +namespace RemoteDemo { class Program { diff --git a/samples/RemoteTest/RemoteTest.csproj b/samples/RemoteDemo/RemoteDemo.csproj similarity index 100% rename from samples/RemoteTest/RemoteTest.csproj rename to samples/RemoteDemo/RemoteDemo.csproj diff --git a/samples/RenderTest/App.config b/samples/RenderDemo/App.config similarity index 100% rename from samples/RenderTest/App.config rename to samples/RenderDemo/App.config diff --git a/samples/RenderTest/App.xaml b/samples/RenderDemo/App.xaml similarity index 81% rename from samples/RenderTest/App.xaml rename to samples/RenderDemo/App.xaml index c119f54915..aee75cb139 100644 --- a/samples/RenderTest/App.xaml +++ b/samples/RenderDemo/App.xaml @@ -2,6 +2,6 @@ - + \ No newline at end of file diff --git a/samples/RenderTest/App.xaml.cs b/samples/RenderDemo/App.xaml.cs similarity index 97% rename from samples/RenderTest/App.xaml.cs rename to samples/RenderDemo/App.xaml.cs index fd2b940f6a..0f627961e6 100644 --- a/samples/RenderTest/App.xaml.cs +++ b/samples/RenderDemo/App.xaml.cs @@ -5,7 +5,7 @@ using Avalonia; using Avalonia.Logging.Serilog; using Avalonia.Markup.Xaml; -namespace RenderTest +namespace RenderDemo { public class App : Application { diff --git a/samples/RenderTest/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml similarity index 95% rename from samples/RenderTest/MainWindow.xaml rename to samples/RenderDemo/MainWindow.xaml index da49054b77..df2b221423 100644 --- a/samples/RenderTest/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -1,6 +1,6 @@ diff --git a/samples/RenderTest/MainWindow.xaml.cs b/samples/RenderDemo/MainWindow.xaml.cs similarity index 94% rename from samples/RenderTest/MainWindow.xaml.cs rename to samples/RenderDemo/MainWindow.xaml.cs index 76a8e81aca..f1f974f7a1 100644 --- a/samples/RenderTest/MainWindow.xaml.cs +++ b/samples/RenderDemo/MainWindow.xaml.cs @@ -5,10 +5,10 @@ using System; using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; -using RenderTest.ViewModels; +using RenderDemo.ViewModels; using ReactiveUI; -namespace RenderTest +namespace RenderDemo { public class MainWindow : Window { diff --git a/samples/RenderTest/Pages/AnimationsPage.xaml b/samples/RenderDemo/Pages/AnimationsPage.xaml similarity index 100% rename from samples/RenderTest/Pages/AnimationsPage.xaml rename to samples/RenderDemo/Pages/AnimationsPage.xaml diff --git a/samples/RenderTest/Pages/AnimationsPage.xaml.cs b/samples/RenderDemo/Pages/AnimationsPage.xaml.cs similarity index 90% rename from samples/RenderTest/Pages/AnimationsPage.xaml.cs rename to samples/RenderDemo/Pages/AnimationsPage.xaml.cs index 2623721393..5b02fd9297 100644 --- a/samples/RenderTest/Pages/AnimationsPage.xaml.cs +++ b/samples/RenderDemo/Pages/AnimationsPage.xaml.cs @@ -7,9 +7,9 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Markup.Xaml; using Avalonia.Media; -using RenderTest.ViewModels; +using RenderDemo.ViewModels; -namespace RenderTest.Pages +namespace RenderDemo.Pages { public class AnimationsPage : UserControl { diff --git a/samples/RenderTest/Pages/ClippingPage.xaml b/samples/RenderDemo/Pages/ClippingPage.xaml similarity index 100% rename from samples/RenderTest/Pages/ClippingPage.xaml rename to samples/RenderDemo/Pages/ClippingPage.xaml diff --git a/samples/RenderTest/Pages/ClippingPage.xaml.cs b/samples/RenderDemo/Pages/ClippingPage.xaml.cs similarity index 96% rename from samples/RenderTest/Pages/ClippingPage.xaml.cs rename to samples/RenderDemo/Pages/ClippingPage.xaml.cs index 2a79076d4c..5357181838 100644 --- a/samples/RenderTest/Pages/ClippingPage.xaml.cs +++ b/samples/RenderDemo/Pages/ClippingPage.xaml.cs @@ -7,7 +7,7 @@ using Avalonia.Data; using Avalonia.Markup.Xaml; using Avalonia.Media; -namespace RenderTest.Pages +namespace RenderDemo.Pages { public class ClippingPage : UserControl { diff --git a/samples/RenderTest/Pages/DrawingPage.xaml b/samples/RenderDemo/Pages/DrawingPage.xaml similarity index 100% rename from samples/RenderTest/Pages/DrawingPage.xaml rename to samples/RenderDemo/Pages/DrawingPage.xaml diff --git a/samples/RenderTest/Pages/DrawingPage.xaml.cs b/samples/RenderDemo/Pages/DrawingPage.xaml.cs similarity index 91% rename from samples/RenderTest/Pages/DrawingPage.xaml.cs rename to samples/RenderDemo/Pages/DrawingPage.xaml.cs index 3bf9bd545d..3475e1aa07 100644 --- a/samples/RenderTest/Pages/DrawingPage.xaml.cs +++ b/samples/RenderDemo/Pages/DrawingPage.xaml.cs @@ -1,7 +1,7 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; -namespace RenderTest.Pages +namespace RenderDemo.Pages { public class DrawingPage : UserControl { diff --git a/samples/RenderTest/RenderTest.csproj b/samples/RenderDemo/RenderDemo.csproj similarity index 100% rename from samples/RenderTest/RenderTest.csproj rename to samples/RenderDemo/RenderDemo.csproj diff --git a/samples/RenderTest/SideBar.xaml b/samples/RenderDemo/SideBar.xaml similarity index 100% rename from samples/RenderTest/SideBar.xaml rename to samples/RenderDemo/SideBar.xaml diff --git a/samples/RenderTest/ViewModels/AnimationsPageViewModel.cs b/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs similarity index 97% rename from samples/RenderTest/ViewModels/AnimationsPageViewModel.cs rename to samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs index 17eee547a1..626a3e7c77 100644 --- a/samples/RenderTest/ViewModels/AnimationsPageViewModel.cs +++ b/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs @@ -2,7 +2,7 @@ using ReactiveUI; using Avalonia.Animation; -namespace RenderTest.ViewModels +namespace RenderDemo.ViewModels { public class AnimationsPageViewModel : ReactiveObject { diff --git a/samples/RenderTest/ViewModels/MainWindowViewModel.cs b/samples/RenderDemo/ViewModels/MainWindowViewModel.cs similarity index 96% rename from samples/RenderTest/ViewModels/MainWindowViewModel.cs rename to samples/RenderDemo/ViewModels/MainWindowViewModel.cs index 02a2abeb89..0cb5e1b87b 100644 --- a/samples/RenderTest/ViewModels/MainWindowViewModel.cs +++ b/samples/RenderDemo/ViewModels/MainWindowViewModel.cs @@ -1,7 +1,7 @@ using System; using ReactiveUI; -namespace RenderTest.ViewModels +namespace RenderDemo.ViewModels { public class MainWindowViewModel : ReactiveObject { diff --git a/samples/VirtualizationTest/App.config b/samples/VirtualizationDemo/App.config similarity index 100% rename from samples/VirtualizationTest/App.config rename to samples/VirtualizationDemo/App.config diff --git a/samples/VirtualizationTest/App.xaml b/samples/VirtualizationDemo/App.xaml similarity index 100% rename from samples/VirtualizationTest/App.xaml rename to samples/VirtualizationDemo/App.xaml diff --git a/samples/VirtualizationTest/App.xaml.cs b/samples/VirtualizationDemo/App.xaml.cs similarity index 92% rename from samples/VirtualizationTest/App.xaml.cs rename to samples/VirtualizationDemo/App.xaml.cs index 14ab5b3f84..b220807443 100644 --- a/samples/VirtualizationTest/App.xaml.cs +++ b/samples/VirtualizationDemo/App.xaml.cs @@ -4,7 +4,7 @@ using Avalonia; using Avalonia.Markup.Xaml; -namespace VirtualizationTest +namespace VirtualizationDemo { public class App : Application { diff --git a/samples/VirtualizationTest/MainWindow.xaml b/samples/VirtualizationDemo/MainWindow.xaml similarity index 100% rename from samples/VirtualizationTest/MainWindow.xaml rename to samples/VirtualizationDemo/MainWindow.xaml diff --git a/samples/VirtualizationTest/MainWindow.xaml.cs b/samples/VirtualizationDemo/MainWindow.xaml.cs similarity index 89% rename from samples/VirtualizationTest/MainWindow.xaml.cs rename to samples/VirtualizationDemo/MainWindow.xaml.cs index 952383dffb..271519b10b 100644 --- a/samples/VirtualizationTest/MainWindow.xaml.cs +++ b/samples/VirtualizationDemo/MainWindow.xaml.cs @@ -4,9 +4,9 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; -using VirtualizationTest.ViewModels; +using VirtualizationDemo.ViewModels; -namespace VirtualizationTest +namespace VirtualizationDemo { public class MainWindow : Window { diff --git a/samples/VirtualizationTest/Program.cs b/samples/VirtualizationDemo/Program.cs similarity index 94% rename from samples/VirtualizationTest/Program.cs rename to samples/VirtualizationDemo/Program.cs index 097f0cfdc7..98f1f08d6c 100644 --- a/samples/VirtualizationTest/Program.cs +++ b/samples/VirtualizationDemo/Program.cs @@ -7,7 +7,7 @@ using Avalonia.Controls; using Avalonia.Logging.Serilog; using Serilog; -namespace VirtualizationTest +namespace VirtualizationDemo { class Program { diff --git a/samples/VirtualizationTest/ViewModels/ItemViewModel.cs b/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs similarity index 92% rename from samples/VirtualizationTest/ViewModels/ItemViewModel.cs rename to samples/VirtualizationDemo/ViewModels/ItemViewModel.cs index 75777012c1..e883cdfeb9 100644 --- a/samples/VirtualizationTest/ViewModels/ItemViewModel.cs +++ b/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs @@ -4,7 +4,7 @@ using System; using ReactiveUI; -namespace VirtualizationTest.ViewModels +namespace VirtualizationDemo.ViewModels { internal class ItemViewModel : ReactiveObject { diff --git a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs similarity index 99% rename from samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs rename to samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs index 8eab91e06d..eb08ef9656 100644 --- a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs @@ -9,7 +9,7 @@ using Avalonia.Controls; using Avalonia.Controls.Primitives; using ReactiveUI; -namespace VirtualizationTest.ViewModels +namespace VirtualizationDemo.ViewModels { internal class MainWindowViewModel : ReactiveObject { diff --git a/samples/VirtualizationTest/VirtualizationTest.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj similarity index 100% rename from samples/VirtualizationTest/VirtualizationTest.csproj rename to samples/VirtualizationDemo/VirtualizationDemo.csproj diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 48e72db126..baca1494be 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -10,6 +10,7 @@ using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Diagnostics; using Avalonia.Logging; +using Avalonia.Reactive; using Avalonia.Threading; using Avalonia.Utilities; @@ -38,7 +39,7 @@ namespace Avalonia /// Maintains a list of direct property binding subscriptions so that the binding source /// doesn't get collected. /// - private List _directBindings; + private List _directBindings; /// /// Event handler for implementation. @@ -359,25 +360,12 @@ namespace Avalonia property, description); - IDisposable subscription = null; - if (_directBindings == null) { - _directBindings = new List(); + _directBindings = new List(); } - subscription = source - .Select(x => CastOrDefault(x, property.PropertyType)) - .Do(_ => { }, () => _directBindings.Remove(subscription)) - .Subscribe(x => SetDirectValue(property, x)); - - _directBindings.Add(subscription); - - return Disposable.Create(() => - { - subscription.Dispose(); - _directBindings.Remove(subscription); - }); + return new DirectBindingSubscription(this, property, source); } else { @@ -908,5 +896,38 @@ namespace Avalonia value, priority); } + + private class DirectBindingSubscription : IObserver, IDisposable + { + readonly AvaloniaObject _owner; + readonly AvaloniaProperty _property; + IDisposable _subscription; + + public DirectBindingSubscription( + AvaloniaObject owner, + AvaloniaProperty property, + IObservable source) + { + _owner = owner; + _property = property; + _owner._directBindings.Add(this); + _subscription = source.Subscribe(this); + } + + public void Dispose() + { + _subscription.Dispose(); + _owner._directBindings.Remove(this); + } + + public void OnCompleted() => Dispose(); + public void OnError(Exception error) => Dispose(); + + public void OnNext(object value) + { + var castValue = CastOrDefault(value, _property.PropertyType); + _owner.SetDirectValue(_property, castValue); + } + } } } diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 1da2ecb942..393482cccf 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -36,32 +36,15 @@ namespace Avalonia /// An observable which fires immediately with the current value of the property on the /// object and subsequently each time the property value changes. /// + /// + /// The subscription to is created using a weak reference. + /// public static IObservable GetObservable(this IAvaloniaObject o, AvaloniaProperty property) { Contract.Requires(o != null); Contract.Requires(property != null); - return new AvaloniaObservable( - observer => - { - EventHandler handler = (s, e) => - { - if (e.Property == property) - { - observer.OnNext(e.NewValue); - } - }; - - observer.OnNext(o.GetValue(property)); - - o.PropertyChanged += handler; - - return Disposable.Create(() => - { - o.PropertyChanged -= handler; - }); - }, - GetDescription(o, property)); + return new AvaloniaPropertyObservable(o, property); } /// @@ -74,51 +57,36 @@ namespace Avalonia /// An observable which fires immediately with the current value of the property on the /// object and subsequently each time the property value changes. /// + /// + /// The subscription to is created using a weak reference. + /// public static IObservable GetObservable(this IAvaloniaObject o, AvaloniaProperty property) { Contract.Requires(o != null); Contract.Requires(property != null); - return o.GetObservable((AvaloniaProperty)property).Cast(); + return new AvaloniaPropertyObservable(o, property); } /// - /// Gets an observable for a . + /// Gets an observable that listens for property changed events for an + /// . /// /// The object. - /// The type of the property. /// The property. /// - /// An observable which when subscribed pushes the old and new values of the property each - /// time it is changed. Note that the observable returned from this method does not fire - /// with the current value of the property immediately. + /// An observable which when subscribed pushes the property changed event args + /// each time a event is raised + /// for the specified property. /// - public static IObservable> GetObservableWithHistory( + public static IObservable GetPropertyChangedObservable( this IAvaloniaObject o, - AvaloniaProperty property) + AvaloniaProperty property) { Contract.Requires(o != null); Contract.Requires(property != null); - return new AvaloniaObservable>( - observer => - { - EventHandler handler = (s, e) => - { - if (e.Property == property) - { - observer.OnNext(Tuple.Create((T)e.OldValue, (T)e.NewValue)); - } - }; - - o.PropertyChanged += handler; - - return Disposable.Create(() => - { - o.PropertyChanged -= handler; - }); - }, - GetDescription(o, property)); + return new AvaloniaPropertyChangedObservable(o, property); } /// @@ -166,23 +134,6 @@ namespace Avalonia o.GetObservable(property)); } - /// - /// Gets a weak observable for a . - /// - /// The object. - /// The property. - /// An observable. - public static IObservable GetWeakObservable(this IAvaloniaObject o, AvaloniaProperty property) - { - Contract.Requires(o != null); - Contract.Requires(property != null); - - return new WeakPropertyChangedObservable( - new WeakReference(o), - property, - GetDescription(o, property)); - } - /// /// Binds a property on an to an . /// diff --git a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs index d295cb91ce..3a355bcb48 100644 --- a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs +++ b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs @@ -2,13 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections; -using System.Collections.Generic; using System.Collections.Specialized; -using System.Reactive; -using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Reactive.Subjects; +using Avalonia.Reactive; using Avalonia.Utilities; namespace Avalonia.Collections @@ -43,9 +39,8 @@ namespace Avalonia.Collections Contract.Requires(collection != null); Contract.Requires(handler != null); - return - collection.GetWeakCollectionChangedObservable() - .Subscribe(e => handler.Invoke(collection, e)); + return collection.GetWeakCollectionChangedObservable() + .Subscribe(e => handler(collection, e)); } /// @@ -63,18 +58,13 @@ namespace Avalonia.Collections Contract.Requires(collection != null); Contract.Requires(handler != null); - return - collection.GetWeakCollectionChangedObservable() - .Subscribe(handler); + return collection.GetWeakCollectionChangedObservable().Subscribe(handler); } - private class WeakCollectionChangedObservable : ObservableBase, + private class WeakCollectionChangedObservable : LightweightObservableBase, IWeakSubscriber { private WeakReference _sourceReference; - private readonly Subject _changed = new Subject(); - - private int _count; public WeakCollectionChangedObservable(WeakReference source) { @@ -83,43 +73,28 @@ namespace Avalonia.Collections public void OnEvent(object sender, NotifyCollectionChangedEventArgs e) { - _changed.OnNext(e); + PublishNext(e); } - protected override IDisposable SubscribeCore(IObserver observer) + protected override void Initialize() { if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance)) { - if (_count++ == 0) - { - WeakSubscriptionManager.Subscribe( - instance, - nameof(instance.CollectionChanged), - this); - } - - return Observable.Using(() => Disposable.Create(DecrementCount), _ => _changed) - .Subscribe(observer); - } - else - { - _changed.OnCompleted(); - observer.OnCompleted(); - return Disposable.Empty; + WeakSubscriptionManager.Subscribe( + instance, + nameof(instance.CollectionChanged), + this); } } - private void DecrementCount() + protected override void Deinitialize() { - if (--_count == 0) + if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance)) { - if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance)) - { - WeakSubscriptionManager.Unsubscribe( - instance, - nameof(instance.CollectionChanged), - this); - } + WeakSubscriptionManager.Unsubscribe( + instance, + nameof(instance.CollectionChanged), + this); } } } diff --git a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs index 3484fb5e23..f7e5de2fe2 100644 --- a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs @@ -2,11 +2,13 @@ using System.Collections.Generic; using System.Reactive.Linq; using System.Text; +using Avalonia.Reactive; namespace Avalonia.Data.Core { public class AvaloniaPropertyAccessorNode : SettableNode { + private IDisposable _subscription; private readonly bool _enableValidation; private readonly AvaloniaProperty _property; @@ -37,9 +39,22 @@ namespace Avalonia.Data.Core } } - protected override IObservable StartListeningCore(WeakReference reference) + protected override void StartListeningCore(WeakReference reference) { - return (reference.Target as IAvaloniaObject)?.GetWeakObservable(_property) ?? Observable.Empty(); + if (reference.Target is IAvaloniaObject obj) + { + _subscription = new AvaloniaPropertyObservable(obj, _property).Subscribe(ValueChanged); + } + else + { + _subscription = null; + } + } + + protected override void StopListeningCore() + { + _subscription?.Dispose(); + _subscription = null; } } } diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index 4b41d1568c..c4ffa839e0 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -7,21 +7,23 @@ using System.Reactive.Linq; using System.Reactive.Subjects; using Avalonia.Data.Converters; using Avalonia.Logging; +using Avalonia.Reactive; using Avalonia.Utilities; namespace Avalonia.Data.Core { /// /// Binds to an expression on an object using a type value converter to convert the values - /// that are send and received. + /// that are sent and received. /// - public class BindingExpression : ISubject, IDescription + public class BindingExpression : LightweightObservableBase, ISubject, IDescription { private readonly ExpressionObserver _inner; private readonly Type _targetType; private readonly object _fallbackValue; private readonly BindingPriority _priority; - private readonly Subject _errors = new Subject(); + InnerListener _innerListener; + WeakReference _value; /// /// Initializes a new instance of the class. @@ -139,7 +141,7 @@ namespace Avalonia.Data.Core "IValueConverter should not return non-errored BindingNotification."); } - _errors.OnNext(notification); + PublishNext(notification); if (_fallbackValue != AvaloniaProperty.UnsetValue) { @@ -170,12 +172,18 @@ namespace Avalonia.Data.Core } } - /// - public IDisposable Subscribe(IObserver observer) + protected override void Initialize() => _innerListener = new InnerListener(this); + protected override void Deinitialize() => _innerListener.Dispose(); + + protected override void Subscribed(IObserver observer, bool first) { - return _inner.Select(ConvertValue).Merge(_errors).Subscribe(observer); + if (!first && _value != null && _value.TryGetTarget(out var val) == true) + { + observer.OnNext(val); + } } + /// private object ConvertValue(object value) { var notification = value as BindingNotification; @@ -301,5 +309,28 @@ namespace Avalonia.Data.Core return a; } + + public class InnerListener : IObserver, IDisposable + { + private readonly BindingExpression _owner; + private readonly IDisposable _dispose; + + public InnerListener(BindingExpression owner) + { + _owner = owner; + _dispose = owner._inner.Subscribe(this); + } + + public void Dispose() => _dispose.Dispose(); + public void OnCompleted() => _owner.PublishCompleted(); + public void OnError(Exception error) => _owner.PublishError(error); + + public void OnNext(object value) + { + var converted = _owner.ConvertValue(value); + _owner._value = new WeakReference(converted); + _owner.PublishNext(converted); + } + } } } diff --git a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs index 24d4090083..0e2c3c035c 100644 --- a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs @@ -9,10 +9,5 @@ namespace Avalonia.Data.Core public class EmptyExpressionNode : ExpressionNode { public override string Description => "."; - - protected override IObservable StartListeningCore(WeakReference reference) - { - return Observable.Return(reference.Target); - } } } diff --git a/src/Avalonia.Base/Data/Core/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNode.cs index 1e54d96682..9ee4787e47 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNode.cs @@ -2,22 +2,18 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Avalonia.Data; namespace Avalonia.Data.Core { - public abstract class ExpressionNode : ISubject + public abstract class ExpressionNode { private static readonly object CacheInvalid = new object(); protected static readonly WeakReference UnsetReference = new WeakReference(AvaloniaProperty.UnsetValue); private WeakReference _target = UnsetReference; - private IDisposable _valueSubscription; - private IObserver _observer; + private Action _subscriber; + private bool _listening; protected WeakReference LastValue { get; private set; } @@ -33,92 +29,66 @@ namespace Avalonia.Data.Core var oldTarget = _target?.Target; var newTarget = value.Target; - var running = _valueSubscription != null; if (!ReferenceEquals(oldTarget, newTarget)) { - _valueSubscription?.Dispose(); - _valueSubscription = null; + if (_listening) + { + StopListening(); + } + _target = value; - if (running) + if (_subscriber != null) { - _valueSubscription = StartListening(); + StartListening(); } } } } - public IDisposable Subscribe(IObserver observer) + public void Subscribe(Action subscriber) { - if (_observer != null) + if (_subscriber != null) { throw new AvaloniaInternalException("ExpressionNode can only be subscribed once."); } - _observer = observer; - var nextSubscription = Next?.Subscribe(this); - _valueSubscription = StartListening(); - - return Disposable.Create(() => - { - _valueSubscription?.Dispose(); - _valueSubscription = null; - LastValue = null; - nextSubscription?.Dispose(); - _observer = null; - }); + _subscriber = subscriber; + Next?.Subscribe(NextValueChanged); + StartListening(); } - void IObserver.OnCompleted() + public void Unsubscribe() { - throw new AvaloniaInternalException("ExpressionNode.OnCompleted should not be called."); - } + Next?.Unsubscribe(); - void IObserver.OnError(Exception error) - { - throw new AvaloniaInternalException("ExpressionNode.OnError should not be called."); + if (_listening) + { + StopListening(); + } + + LastValue = null; + _subscriber = null; } - void IObserver.OnNext(object value) + protected virtual void StartListeningCore(WeakReference reference) { - NextValueChanged(value); + ValueChanged(reference.Target); } - protected virtual IObservable StartListeningCore(WeakReference reference) + protected virtual void StopListeningCore() { - return Observable.Return(reference.Target); } protected virtual void NextValueChanged(object value) { var bindingBroken = BindingNotification.ExtractError(value) as MarkupBindingChainException; bindingBroken?.AddNode(Description); - _observer.OnNext(value); - } - - private IDisposable StartListening() - { - var target = _target.Target; - IObservable source; - - if (target == null) - { - source = Observable.Return(TargetNullNotification()); - } - else if (target == AvaloniaProperty.UnsetValue) - { - source = Observable.Empty(); - } - else - { - source = StartListeningCore(_target); - } - - return source.Subscribe(ValueChanged); + _subscriber(value); } - private void ValueChanged(object value) + protected void ValueChanged(object value) { var notification = value as BindingNotification; @@ -131,24 +101,50 @@ namespace Avalonia.Data.Core } else { - _observer.OnNext(value); + _subscriber(value); } } else { LastValue = new WeakReference(notification.Value); + if (Next != null) { Next.Target = new WeakReference(notification.Value); } - + if (Next == null || notification.Error != null) { - _observer.OnNext(value); + _subscriber(value); } } } + private void StartListening() + { + var target = _target.Target; + + if (target == null) + { + ValueChanged(TargetNullNotification()); + _listening = false; + } + else if (target != AvaloniaProperty.UnsetValue) + { + StartListeningCore(_target); + _listening = true; + } + else + { + _listening = false; + } + } + + private void StopListening() + { + StopListeningCore(); + } + private BindingNotification TargetNullNotification() { return new BindingNotification( diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index fcc4ec4b2a..967a49924a 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -5,19 +5,18 @@ using System; using System.Collections.Generic; using System.Linq.Expressions; using System.Reactive; -using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Reactive.Subjects; using Avalonia.Data; using Avalonia.Data.Core.Parsers; using Avalonia.Data.Core.Plugins; +using Avalonia.Reactive; namespace Avalonia.Data.Core { /// /// Observes and sets the value of an expression on an object. /// - public class ExpressionObserver : ObservableBase, IDescription + public class ExpressionObserver : LightweightObservableBase, IDescription { /// /// An ordered collection of property accessor plugins that can be used to customize @@ -56,9 +55,9 @@ namespace Avalonia.Data.Core private static readonly object UninitializedValue = new object(); private readonly ExpressionNode _node; - private readonly Subject _finished; - private readonly object _root; - private IObservable _result; + private object _root; + private IDisposable _rootSubscription; + private WeakReference _value; /// /// Initializes a new instance of the class. @@ -110,7 +109,6 @@ namespace Avalonia.Data.Core _node = node; Description = description; _root = rootObservable; - _finished = new Subject(); } public static ExpressionObserver Create( @@ -124,6 +122,7 @@ namespace Avalonia.Data.Core rootObservable.Select(o => (object)o), Parse(expression, enableDataValidation), description ?? expression.ToString()); + } /// @@ -143,10 +142,8 @@ namespace Avalonia.Data.Core { Contract.Requires(rootGetter != null); Contract.Requires(update != null); - Description = description; _node = node; - _finished = new Subject(); _node.Target = new WeakReference(rootGetter()); _root = update.Select(x => rootGetter()); } @@ -229,27 +226,26 @@ namespace Avalonia.Data.Core } } - /// - protected override IDisposable SubscribeCore(IObserver observer) + protected override void Initialize() { - if (_result == null) - { - var source = (IObservable)_node; + _value = null; + _node.Subscribe(ValueChanged); + StartRoot(); + } - if (_finished != null) - { - source = source.TakeUntil(_finished); - } + protected override void Deinitialize() + { + _rootSubscription?.Dispose(); + _rootSubscription = null; + _node.Unsubscribe(); + } - _result = Observable.Using(StartRoot, _ => source) - .Select(ToWeakReference) - .Publish(UninitializedValue) - .RefCount() - .Where(x => x != UninitializedValue) - .Select(Translate); + protected override void Subscribed(IObserver observer, bool first) + { + if (!first && _value != null && _value.TryGetTarget(out var value)) + { + observer.OnNext(value); } - - return _result.Subscribe(observer); } private static ExpressionNode Parse(LambdaExpression expression, bool enableDataValidation) @@ -258,42 +254,27 @@ namespace Avalonia.Data.Core return parser.Parse(expression); } - private static object ToWeakReference(object o) - { - return o is BindingNotification ? o : new WeakReference(o); - } - - private object Translate(object o) + private void StartRoot() { - if (o is WeakReference weak) + if (_root is IObservable observable) { - return weak.Target; + _rootSubscription = observable.Subscribe( + x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null), + x => PublishCompleted(), + () => PublishCompleted()); } - else if (BindingNotification.ExtractError(o) is MarkupBindingChainException broken) + else { - broken.Commit(Description); + _node.Target = (WeakReference)_root; } - - return o; } - private IDisposable StartRoot() + private void ValueChanged(object value) { - switch (_root) - { - case IObservable observable: - return observable.Subscribe( - x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null), - _ => _finished.OnNext(Unit.Default), - () => _finished.OnNext(Unit.Default)); - case WeakReference weak: - _node.Target = weak; - break; - default: - throw new AvaloniaInternalException("The ExpressionObserver._root member should only be either an observable or WeakReference."); - } - - return Disposable.Empty; + var broken = BindingNotification.ExtractError(value) as MarkupBindingChainException; + broken?.Commit(Description); + _value = new WeakReference(value); + PublishNext(value); } } } diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs index a43d119ebb..5c3295a9d8 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs @@ -15,12 +15,16 @@ namespace Avalonia.Data.Core { public abstract class IndexerNodeBase : SettableNode { - protected override IObservable StartListeningCore(WeakReference reference) + private IDisposable _subscription; + + protected override void StartListeningCore(WeakReference reference) { var target = reference.Target; + var incc = target as INotifyCollectionChanged; + var inpc = target as INotifyPropertyChanged; var inputs = new List>(); - if (target is INotifyCollectionChanged incc) + if (incc != null) { inputs.Add(WeakObservable.FromEventPattern( incc, @@ -29,7 +33,7 @@ namespace Avalonia.Data.Core .Select(_ => GetValue(target))); } - if (target is INotifyPropertyChanged inpc) + if (inpc != null) { inputs.Add(WeakObservable.FromEventPattern( inpc, @@ -38,7 +42,12 @@ namespace Avalonia.Data.Core .Select(_ => GetValue(target))); } - return inputs.Merge().StartWith(GetValue(target)); + _subscription = Observable.Merge(inputs).StartWith(GetValue(target)).Subscribe(ValueChanged); + } + + protected override void StopListeningCore() + { + _subscription.Dispose(); } protected abstract object GetValue(object target); diff --git a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs index 7c12a0d5ae..ee91f964ff 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -117,15 +117,15 @@ namespace Avalonia.Data.Core.Plugins return false; } - protected override void Dispose(bool disposing) + protected override void SubscribeCore() { - _subscription?.Dispose(); - _subscription = null; + _subscription = Instance?.GetObservable(_property).Subscribe(PublishValue); } - protected override void SubscribeCore(IObserver observer) + protected override void UnsubscribeCore() { - _subscription = Instance?.GetWeakObservable(_property).Subscribe(observer); + _subscription?.Dispose(); + _subscription = null; } } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs b/src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs index bd429f04d6..03ab7712bd 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs @@ -55,13 +55,13 @@ namespace Avalonia.Data.Core.Plugins /// The value. void IObserver.OnNext(object value) => InnerValueChanged(value); - /// - protected override void Dispose(bool disposing) => _inner.Dispose(); - /// /// Begins listening to the inner . /// - protected override void SubscribeCore(IObserver observer) => _inner.Subscribe(this); + protected override void SubscribeCore() => _inner.Subscribe(InnerValueChanged); + + /// + protected override void UnsubscribeCore() => _inner.Dispose(); /// /// Called when the inner notifies with a new value. @@ -74,7 +74,7 @@ namespace Avalonia.Data.Core.Plugins protected virtual void InnerValueChanged(object value) { var notification = value as BindingNotification ?? new BindingNotification(value); - Observer.OnNext(notification); + PublishValue(notification); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs index 35f9f7e59a..4507b32e0c 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs @@ -1,7 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using Avalonia.Data; using System; using System.Reflection; @@ -36,11 +35,11 @@ namespace Avalonia.Data.Core.Plugins } catch (TargetInvocationException ex) { - Observer.OnNext(new BindingNotification(ex.InnerException, BindingErrorType.DataValidationError)); + PublishValue(new BindingNotification(ex.InnerException, BindingErrorType.DataValidationError)); } catch (Exception ex) { - Observer.OnNext(new BindingNotification(ex, BindingErrorType.DataValidationError)); + PublishValue(new BindingNotification(ex, BindingErrorType.DataValidationError)); } return false; diff --git a/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs b/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs index d7dda57a72..33ea5bba08 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using Avalonia.Data; namespace Avalonia.Data.Core.Plugins { @@ -10,7 +9,7 @@ namespace Avalonia.Data.Core.Plugins /// Defines an accessor to a property on an object returned by a /// /// - public interface IPropertyAccessor : IObservable, IDisposable + public interface IPropertyAccessor : IDisposable { /// /// Gets the type of the property. @@ -38,5 +37,16 @@ namespace Avalonia.Data.Core.Plugins /// True if the property was set; false if the property could not be set. /// bool SetValue(object value, BindingPriority priority); + + /// + /// Subscribes to the value of the member. + /// + /// A method that receives the values. + void Subscribe(Action listener); + + /// + /// Unsubscribes to the value of the member. + /// + void Unsubscribe(); } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs index 436046f3fa..4d6fc01229 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using Avalonia.Data; using Avalonia.Utilities; namespace Avalonia.Data.Core.Plugins @@ -40,43 +39,43 @@ namespace Avalonia.Data.Core.Plugins { if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName)) { - Observer.OnNext(CreateBindingNotification(Value)); + PublishValue(CreateBindingNotification(Value)); } } - protected override void Dispose(bool disposing) + protected override void SubscribeCore() { - base.Dispose(disposing); - var target = _reference.Target as INotifyDataErrorInfo; if (target != null) { - WeakSubscriptionManager.Unsubscribe( + WeakSubscriptionManager.Subscribe( target, nameof(target.ErrorsChanged), this); } + + base.SubscribeCore(); } - protected override void SubscribeCore(IObserver observer) + protected override void UnsubscribeCore() { var target = _reference.Target as INotifyDataErrorInfo; if (target != null) { - WeakSubscriptionManager.Subscribe( + WeakSubscriptionManager.Unsubscribe( target, nameof(target.ErrorsChanged), this); } - base.SubscribeCore(observer); + base.UnsubscribeCore(); } protected override void InnerValueChanged(object value) { - base.InnerValueChanged(CreateBindingNotification(value)); + PublishValue(CreateBindingNotification(value)); } private BindingNotification CreateBindingNotification(object value) diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index ba4e60eb74..dab32b639a 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -103,7 +103,13 @@ namespace Avalonia.Data.Core.Plugins } } - protected override void Dispose(bool disposing) + protected override void SubscribeCore() + { + SendCurrentValue(); + SubscribeToChanges(); + } + + protected override void UnsubscribeCore() { var inpc = _reference.Target as INotifyPropertyChanged; @@ -116,18 +122,12 @@ namespace Avalonia.Data.Core.Plugins } } - protected override void SubscribeCore(IObserver observer) - { - SendCurrentValue(); - SubscribeToChanges(); - } - private void SendCurrentValue() { try { var value = Value; - Observer.OnNext(value); + PublishValue(value); } catch { } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs index b2b3a107fa..cf0abc6f35 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs @@ -74,14 +74,18 @@ namespace Avalonia.Data.Core.Plugins public override bool SetValue(object value, BindingPriority priority) => false; - protected override void SubscribeCore(IObserver observer) + protected override void SubscribeCore() { try { - Observer.OnNext(Value); + PublishValue(Value); } catch { } } + + protected override void UnsubscribeCore() + { + } } } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs b/src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs index 9cc78369a7..e840b2c5c9 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs @@ -2,67 +2,75 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using Avalonia.Data; namespace Avalonia.Data.Core.Plugins { /// /// Defines a default base implementation for a . /// - /// - /// is an observable that will only be subscribed to one time. - /// In addition, the subscription can be disposed by calling on the - /// property accessor itself - this prevents needing to hold two references for a subscription. - /// public abstract class PropertyAccessorBase : IPropertyAccessor { + private Action _listener; + /// public abstract Type PropertyType { get; } /// public abstract object Value { get; } - /// - /// Stops the subscription. - /// - public void Dispose() => Dispose(true); + /// + public void Dispose() + { + if (_listener != null) + { + Unsubscribe(); + } + } /// public abstract bool SetValue(object value, BindingPriority priority); - /// - /// The currently subscribed observer. - /// - protected IObserver Observer { get; private set; } - /// - public IDisposable Subscribe(IObserver observer) + public void Subscribe(Action listener) { - Contract.Requires(observer != null); + Contract.Requires(listener != null); - if (Observer != null) + if (_listener != null) { throw new InvalidOperationException( - "A property accessor can be subscribed to only once."); + "A member accessor can be subscribed to only once."); } - Observer = observer; - SubscribeCore(observer); - return this; + _listener = listener; + SubscribeCore(); } + public void Unsubscribe() + { + if (_listener == null) + { + throw new InvalidOperationException( + "The member accessor was not subscribed."); + } + + UnsubscribeCore(); + _listener = null; + } + + /// + /// Publishes a value to the listener. + /// + /// The value. + protected void PublishValue(object value) => _listener?.Invoke(value); + /// - /// Stops listening to the property. + /// When overridden in a derived class, begins listening to the member. /// - /// - /// True if the method was called, false if the object is being - /// finalized. - /// - protected virtual void Dispose(bool disposing) => Observer = null; + protected abstract void SubscribeCore(); /// - /// When overridden in a derived class, begins listening to the property. + /// When overridden in a derived class, stops listening to the member. /// - protected abstract void SubscribeCore(IObserver observer); + protected abstract void UnsubscribeCore(); } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs b/src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs index 647adc36cb..eb2400807a 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs @@ -1,6 +1,4 @@ using System; -using System.Reactive.Disposables; -using Avalonia.Data; namespace Avalonia.Data.Core.Plugins { @@ -37,10 +35,13 @@ namespace Avalonia.Data.Core.Plugins return false; } - public IDisposable Subscribe(IObserver observer) + public void Subscribe(Action listener) + { + listener(_error); + } + + public void Unsubscribe() { - observer.OnNext(_error); - return Disposable.Empty; } } } diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index 97f419ca68..a916142675 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -3,9 +3,7 @@ using System; using System.Linq; -using System.Reactive.Disposables; using System.Reactive.Linq; -using Avalonia.Data; using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core @@ -39,7 +37,7 @@ namespace Avalonia.Data.Core return false; } - protected override IObservable StartListeningCore(WeakReference reference) + protected override void StartListeningCore(WeakReference reference) { var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference.Target, PropertyName)); var accessor = plugin?.Start(reference, PropertyName); @@ -55,17 +53,20 @@ namespace Avalonia.Data.Core } } - // Ensure that _accessor is set for the duration of the subscription. - return Observable.Using( - () => - { - _accessor = accessor; - return Disposable.Create(() => - { - _accessor = null; - }); - }, - _ => accessor); + if (accessor == null) + { + throw new NotSupportedException( + $"Could not find a matching property accessor for {PropertyName}."); + } + + accessor.Subscribe(ValueChanged); + _accessor = accessor; + } + + protected override void StopListeningCore() + { + _accessor.Dispose(); + _accessor = null; } } } diff --git a/src/Avalonia.Base/Data/Core/StreamNode.cs b/src/Avalonia.Base/Data/Core/StreamNode.cs index 8cdece9860..6fc178e7f8 100644 --- a/src/Avalonia.Base/Data/Core/StreamNode.cs +++ b/src/Avalonia.Base/Data/Core/StreamNode.cs @@ -2,30 +2,37 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Globalization; -using Avalonia.Data; using System.Reactive.Linq; namespace Avalonia.Data.Core { public class StreamNode : ExpressionNode { + private IDisposable _subscription; + public override string Description => "^"; - protected override IObservable StartListeningCore(WeakReference reference) + protected override void StartListeningCore(WeakReference reference) { foreach (var plugin in ExpressionObserver.StreamHandlers) { if (plugin.Match(reference)) { - return plugin.Start(reference); + _subscription = plugin.Start(reference).Subscribe(ValueChanged); + return; } } // TODO: Improve error. - return Observable.Return(new BindingNotification( + ValueChanged(new BindingNotification( new MarkupBindingChainException("Stream operator applied to unsupported type", Description), BindingErrorType.Error)); } + + protected override void StopListeningCore() + { + _subscription?.Dispose(); + _subscription = null; + } } } diff --git a/src/Avalonia.Base/Reactive/AvaloniaObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaObservable.cs deleted file mode 100644 index b59d8e5226..0000000000 --- a/src/Avalonia.Base/Reactive/AvaloniaObservable.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Reactive; -using System.Reactive.Disposables; - -namespace Avalonia.Reactive -{ - /// - /// An with an additional description. - /// - /// The type of the elements in the sequence. - public class AvaloniaObservable : ObservableBase, IDescription - { - private readonly Func, IDisposable> _subscribe; - - /// - /// Initializes a new instance of the class. - /// - /// The subscribe function. - /// The description of the observable. - public AvaloniaObservable(Func, IDisposable> subscribe, string description) - { - Contract.Requires(subscribe != null); - - _subscribe = subscribe; - Description = description; - } - - /// - /// Gets the description of the observable. - /// - public string Description { get; } - - /// - protected override IDisposable SubscribeCore(IObserver observer) - { - return _subscribe(observer) ?? Disposable.Empty; - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyChangedObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyChangedObservable.cs new file mode 100644 index 0000000000..5ef0d25133 --- /dev/null +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyChangedObservable.cs @@ -0,0 +1,46 @@ +using System; + +namespace Avalonia.Reactive +{ + internal class AvaloniaPropertyChangedObservable : + LightweightObservableBase, + IDescription + { + private readonly WeakReference _target; + private readonly AvaloniaProperty _property; + + public AvaloniaPropertyChangedObservable( + IAvaloniaObject target, + AvaloniaProperty property) + { + _target = new WeakReference(target); + _property = property; + } + + public string Description => $"{_target.GetType().Name}.{_property.Name}"; + + protected override void Initialize() + { + if (_target.TryGetTarget(out var target)) + { + target.PropertyChanged += PropertyChanged; + } + } + + protected override void Deinitialize() + { + if (_target.TryGetTarget(out var target)) + { + target.PropertyChanged -= PropertyChanged; + } + } + + private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + PublishNext(e); + } + } + } +} diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs new file mode 100644 index 0000000000..4385ab13ef --- /dev/null +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs @@ -0,0 +1,52 @@ +using System; + +namespace Avalonia.Reactive +{ + internal class AvaloniaPropertyObservable : LightweightObservableBase, IDescription + { + private readonly WeakReference _target; + private readonly AvaloniaProperty _property; + private T _value; + + public AvaloniaPropertyObservable( + IAvaloniaObject target, + AvaloniaProperty property) + { + _target = new WeakReference(target); + _property = property; + } + + public string Description => $"{_target.GetType().Name}.{_property.Name}"; + + protected override void Initialize() + { + if (_target.TryGetTarget(out var target)) + { + _value = (T)target.GetValue(_property); + target.PropertyChanged += PropertyChanged; + } + } + + protected override void Deinitialize() + { + if (_target.TryGetTarget(out var target)) + { + target.PropertyChanged -= PropertyChanged; + } + } + + protected override void Subscribed(IObserver observer, bool first) + { + observer.OnNext(_value); + } + + private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + _value = (T)e.NewValue; + PublishNext(_value); + } + } + } +} diff --git a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs new file mode 100644 index 0000000000..a2786d63da --- /dev/null +++ b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.Reactive; +using System.Reactive.Disposables; +using System.Threading; +using Avalonia.Threading; + +namespace Avalonia.Reactive +{ + /// + /// Lightweight base class for observable implementations. + /// + /// The observable type. + /// + /// 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. + /// + public abstract class LightweightObservableBase : IObservable + { + private Exception _error; + private List> _observers = new List>(); + + public IDisposable Subscribe(IObserver observer) + { + Contract.Requires(observer != null); + Dispatcher.UIThread.VerifyAccess(); + + var first = false; + + for (; ; ) + { + if (Volatile.Read(ref _observers) == null) + { + if (_error != null) + { + observer.OnError(_error); + } + else + { + observer.OnCompleted(); + } + + return Disposable.Empty; + } + + lock (this) + { + if (_observers == null) + { + continue; + } + + first = _observers.Count == 0; + _observers.Add(observer); + break; + } + } + + if (first) + { + Initialize(); + } + + Subscribed(observer, first); + + return new RemoveObserver(this, observer); + } + + void Remove(IObserver observer) + { + if (Volatile.Read(ref _observers) != null) + { + lock (this) + { + var observers = _observers; + + if (observers != null) + { + observers.Remove(observer); + + if (observers.Count == 0) + { + observers.TrimExcess(); + } + else + { + return; + } + } else + { + return; + } + } + + Deinitialize(); + } + } + + sealed class RemoveObserver : IDisposable + { + LightweightObservableBase _parent; + + IObserver _observer; + + public RemoveObserver(LightweightObservableBase parent, IObserver observer) + { + _parent = parent; + Volatile.Write(ref _observer, observer); + } + + public void Dispose() + { + var observer = _observer; + Interlocked.Exchange(ref _parent, null)?.Remove(observer); + _observer = null; + } + } + + protected abstract void Initialize(); + protected abstract void Deinitialize(); + + protected void PublishNext(T value) + { + if (Volatile.Read(ref _observers) != null) + { + IObserver[] observers; + + lock (this) + { + if (_observers == null) + { + return; + } + observers = _observers.ToArray(); + } + + foreach (var observer in observers) + { + observer.OnNext(value); + } + } + } + + protected void PublishCompleted() + { + if (Volatile.Read(ref _observers) != null) + { + IObserver[] observers; + + lock (this) + { + if (_observers == null) + { + return; + } + observers = _observers.ToArray(); + Volatile.Write(ref _observers, null); + } + + foreach (var observer in observers) + { + observer.OnCompleted(); + } + + Deinitialize(); + } + } + + protected void PublishError(Exception error) + { + if (Volatile.Read(ref _observers) != null) + { + + IObserver[] observers; + + lock (this) + { + if (_observers == null) + { + return; + } + + _error = error; + observers = _observers.ToArray(); + Volatile.Write(ref _observers, null); + } + + foreach (var observer in observers) + { + observer.OnError(error); + } + + Deinitialize(); + } + } + + protected virtual void Subscribed(IObserver observer, bool first) + { + } + } +} diff --git a/src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs b/src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs new file mode 100644 index 0000000000..cd8ce2cd80 --- /dev/null +++ b/src/Avalonia.Base/Reactive/SingleSubscriberObservableBase.cs @@ -0,0 +1,76 @@ +using System; +using Avalonia.Threading; + +namespace Avalonia.Reactive +{ + public abstract class SingleSubscriberObservableBase : IObservable, IDisposable + { + private Exception _error; + private IObserver _observer; + private bool _completed; + + public IDisposable Subscribe(IObserver observer) + { + Contract.Requires(observer != null); + Dispatcher.UIThread.VerifyAccess(); + + if (_observer != null) + { + throw new InvalidOperationException("The observable can only be subscribed once."); + } + + if (_error != null) + { + observer.OnError(_error); + } + else if (_completed) + { + observer.OnCompleted(); + } + else + { + _observer = observer; + Subscribed(); + } + + return this; + } + + void IDisposable.Dispose() + { + Unsubscribed(); + _observer = null; + } + + protected abstract void Unsubscribed(); + + protected void PublishNext(T value) + { + _observer?.OnNext(value); + } + + protected void PublishCompleted() + { + if (_observer != null) + { + _observer.OnCompleted(); + _completed = true; + Unsubscribed(); + _observer = null; + } + } + + protected void PublishError(Exception error) + { + if (_observer != null) + { + _observer.OnError(error); + _error = error; + Unsubscribed(); + _observer = null; + } + } + + protected abstract void Subscribed(); + } +} diff --git a/src/Avalonia.Base/Reactive/WeakPropertyChangedObservable.cs b/src/Avalonia.Base/Reactive/WeakPropertyChangedObservable.cs deleted file mode 100644 index aa5f553865..0000000000 --- a/src/Avalonia.Base/Reactive/WeakPropertyChangedObservable.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Reactive; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Avalonia.Utilities; - -namespace Avalonia.Reactive -{ - internal class WeakPropertyChangedObservable : ObservableBase, - IWeakSubscriber, IDescription - { - private WeakReference _sourceReference; - private readonly AvaloniaProperty _property; - private readonly Subject _changed = new Subject(); - - private int _count; - - public WeakPropertyChangedObservable( - WeakReference source, - AvaloniaProperty property, - string description) - { - _sourceReference = source; - _property = property; - Description = description; - } - - public string Description { get; } - - public void OnEvent(object sender, AvaloniaPropertyChangedEventArgs e) - { - if (e.Property == _property) - { - _changed.OnNext(e.NewValue); - } - } - - protected override IDisposable SubscribeCore(IObserver observer) - { - IAvaloniaObject instance; - - if (_sourceReference.TryGetTarget(out instance)) - { - if (_count++ == 0) - { - WeakSubscriptionManager.Subscribe( - instance, - nameof(instance.PropertyChanged), - this); - } - - observer.OnNext(instance.GetValue(_property)); - - return Observable.Using(() => Disposable.Create(DecrementCount), _ => _changed) - .Subscribe(observer); - } - else - { - _changed.OnCompleted(); - observer.OnCompleted(); - return Disposable.Empty; - } - } - - private void DecrementCount() - { - if (--_count == 0) - { - IAvaloniaObject instance; - - if (_sourceReference.TryGetTarget(out instance)) - { - WeakSubscriptionManager.Unsubscribe( - instance, - nameof(instance.PropertyChanged), - this); - } - } - } - } -} diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 51b690ece9..83763c0836 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -15,7 +15,7 @@ namespace Avalonia.Controls public abstract class AppBuilderBase where TAppBuilder : AppBuilderBase, new() { private static bool s_setupWasAlreadyCalled; - + /// /// Gets or sets the instance. /// @@ -92,7 +92,7 @@ namespace Avalonia.Controls }; } - protected TAppBuilder Self => (TAppBuilder) this; + protected TAppBuilder Self => (TAppBuilder)this; /// /// Registers a callback to call before Start is called on the . @@ -125,7 +125,6 @@ namespace Avalonia.Controls var window = new TMainWindow(); if (dataContextProvider != null) window.DataContext = dataContextProvider(); - window.Show(); Instance.Run(window); } @@ -143,7 +142,6 @@ namespace Avalonia.Controls if (dataContextProvider != null) mainWindow.DataContext = dataContextProvider(); - mainWindow.Show(); Instance.Run(mainWindow); } @@ -209,25 +207,36 @@ namespace Avalonia.Controls public TAppBuilder UseAvaloniaModules() => AfterSetup(builder => SetupAvaloniaModules()); + /// + /// Sets the shutdown mode of the application. + /// + /// The shutdown mode. + /// + public TAppBuilder SetExitMode(ExitMode exitMode) + { + Instance.ExitMode = exitMode; + return Self; + } + protected virtual bool CheckSetup => true; private void SetupAvaloniaModules() { var moduleInitializers = from assembly in AvaloniaLocator.Current.GetService().GetLoadedAssemblies() - from attribute in assembly.GetCustomAttributes() - where attribute.ForWindowingSubsystem == "" - || attribute.ForWindowingSubsystem == WindowingSubsystemName - where attribute.ForRenderingSubsystem == "" - || attribute.ForRenderingSubsystem == RenderingSubsystemName - group attribute by attribute.Name into exports - select (from export in exports - orderby export.ForWindowingSubsystem.Length descending - orderby export.ForRenderingSubsystem.Length descending - select export).First().ModuleType into moduleType - select (from constructor in moduleType.GetTypeInfo().DeclaredConstructors - where constructor.GetParameters().Length == 0 && !constructor.IsStatic - select constructor).Single() into constructor - select (Action)(() => constructor.Invoke(new object[0])); + from attribute in assembly.GetCustomAttributes() + where attribute.ForWindowingSubsystem == "" + || attribute.ForWindowingSubsystem == WindowingSubsystemName + where attribute.ForRenderingSubsystem == "" + || attribute.ForRenderingSubsystem == RenderingSubsystemName + group attribute by attribute.Name into exports + select (from export in exports + orderby export.ForWindowingSubsystem.Length descending + orderby export.ForRenderingSubsystem.Length descending + select export).First().ModuleType into moduleType + select (from constructor in moduleType.GetTypeInfo().DeclaredConstructors + where constructor.GetParameters().Length == 0 && !constructor.IsStatic + select constructor).Single() into constructor + select (Action)(() => constructor.Invoke(new object[0])); Delegate.Combine(moduleInitializers.ToArray()).DynamicInvoke(); } diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 6fdca557eb..de27aa94f8 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -43,11 +43,15 @@ namespace Avalonia private Styles _styles; private IResourceDictionary _resources; + private CancellationTokenSource _mainLoopCancellationTokenSource; + /// /// Initializes a new instance of the class. /// public Application() { + Windows = new WindowCollection(this); + OnExit += OnExiting; } @@ -158,6 +162,40 @@ namespace Avalonia /// IResourceNode IResourceNode.ResourceParent => null; + /// + /// Gets or sets the . This property indicates whether the application exits explicitly or implicitly. + /// If is set to OnExplicitExit the application is only closes if Exit is called. + /// The default is OnLastWindowClose + /// + /// + /// The shutdown mode. + /// + public ExitMode ExitMode { get; set; } + + /// + /// Gets or sets the main window of the application. + /// + /// + /// The main window. + /// + public Window MainWindow { get; set; } + + /// + /// Gets the open windows of the application. + /// + /// + /// The windows. + /// + public WindowCollection Windows { get; } + + /// + /// Gets or sets a value indicating whether this instance is existing. + /// + /// + /// true if this instance is existing; otherwise, false. + /// + internal bool IsExiting { get; set; } + /// /// Initializes the application by loading XAML etc. /// @@ -171,19 +209,74 @@ namespace Avalonia /// The closable to track public void Run(ICloseable closable) { - var source = new CancellationTokenSource(); - closable.Closed += OnExiting; - closable.Closed += (s, e) => source.Cancel(); - Dispatcher.UIThread.MainLoop(source.Token); + if (_mainLoopCancellationTokenSource != null) + { + throw new Exception("Run should only called once"); + } + + closable.Closed += (s, e) => Exit(); + + _mainLoopCancellationTokenSource = new CancellationTokenSource(); + + Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token); + + // Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly + if (!IsExiting) + { + OnExit?.Invoke(this, EventArgs.Empty); + } + } + + /// + /// Runs the application's main loop until some condition occurs that is specified by ExitMode. + /// + /// The main window + public void Run(Window mainWindow) + { + if (_mainLoopCancellationTokenSource != null) + { + throw new Exception("Run should only called once"); + } + + _mainLoopCancellationTokenSource = new CancellationTokenSource(); + + if (MainWindow == null) + { + if (mainWindow == null) + { + throw new ArgumentNullException(nameof(mainWindow)); + } + + if (!mainWindow.IsVisible) + { + mainWindow.Show(); + } + + MainWindow = mainWindow; + } + + Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token); + + // Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly + if (!IsExiting) + { + OnExit?.Invoke(this, EventArgs.Empty); + } } - + /// - /// Runs the application's main loop until the is cancelled. + /// Runs the application's main loop until the is canceled. /// /// The token to track public void Run(CancellationToken token) { Dispatcher.UIThread.MainLoop(token); + + // Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly + if (!IsExiting) + { + OnExit?.Invoke(this, EventArgs.Empty); + } } /// @@ -191,7 +284,13 @@ namespace Avalonia /// public void Exit() { + IsExiting = true; + + Windows.Clear(); + OnExit?.Invoke(this, EventArgs.Empty); + + _mainLoopCancellationTokenSource?.Cancel(); } /// diff --git a/src/Avalonia.Controls/ExitMode.cs b/src/Avalonia.Controls/ExitMode.cs new file mode 100644 index 0000000000..b73fe4a963 --- /dev/null +++ b/src/Avalonia.Controls/ExitMode.cs @@ -0,0 +1,26 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +namespace Avalonia +{ + /// + /// Enum for ExitMode + /// + public enum ExitMode + { + /// + /// Indicates an implicit call to Application.Exit when the last window closes. + /// + OnLastWindowClose, + + /// + /// Indicates an implicit call to Application.Exit when the main window closes. + /// + OnMainWindowClose, + + /// + /// Indicates that the application only exits on an explicit call to Application.Exit. + /// + OnExplicitExit + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 5119096965..3cb997f615 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -155,6 +155,7 @@ namespace Avalonia.Controls void IItemsPresenterHost.RegisterItemsPresenter(IItemsPresenter presenter) { Presenter = presenter; + ItemContainerGenerator.Clear(); } /// diff --git a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs index daaf3fd5cf..95193c0432 100644 --- a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs +++ b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs @@ -49,11 +49,9 @@ namespace Avalonia.Controls.Mixins Contract.Requires(content != null); Contract.Requires(logicalChildrenSelector != null); - EventHandler templateApplied = (s, ev) => + void TemplateApplied(object s, RoutedEventArgs ev) { - var sender = s as TControl; - - if (sender != null) + if (s is TControl sender) { var e = (TemplateAppliedEventArgs)ev; var presenter = (IControl)e.NameScope.Find(presenterName); @@ -64,12 +62,12 @@ namespace Avalonia.Controls.Mixins var logicalChildren = logicalChildrenSelector(sender); var subscription = presenter - .GetObservableWithHistory(ContentPresenter.ChildProperty) - .Subscribe(child => UpdateLogicalChild( + .GetPropertyChangedObservable(ContentPresenter.ChildProperty) + .Subscribe(c => UpdateLogicalChild( sender, - logicalChildren, - child.Item1, - child.Item2)); + logicalChildren, + c.OldValue, + c.NewValue)); UpdateLogicalChild( sender, @@ -80,18 +78,16 @@ namespace Avalonia.Controls.Mixins subscriptions.Value.Add(sender, subscription); } } - }; + } TemplatedControl.TemplateAppliedEvent.AddClassHandler( typeof(TControl), - templateApplied, + TemplateApplied, RoutingStrategies.Direct); content.Changed.Subscribe(e => { - var sender = e.Sender as TControl; - - if (sender != null) + if (e.Sender is TControl sender) { var logicalChildren = logicalChildrenSelector(sender); UpdateLogicalChild(sender, logicalChildren, e.OldValue, e.NewValue); @@ -100,9 +96,7 @@ namespace Avalonia.Controls.Mixins Control.TemplatedParentProperty.Changed.Subscribe(e => { - var sender = e.Sender as TControl; - - if (sender != null) + if (e.Sender is TControl sender) { var logicalChild = logicalChildrenSelector(sender).FirstOrDefault() as IControl; logicalChild?.SetValue(Control.TemplatedParentProperty, sender.TemplatedParent); @@ -111,13 +105,9 @@ namespace Avalonia.Controls.Mixins TemplatedControl.TemplateProperty.Changed.Subscribe(e => { - var sender = e.Sender as TControl; - - if (sender != null) + if (e.Sender is TControl sender) { - IDisposable subscription; - - if (subscriptions.Value.TryGetValue(sender, out subscription)) + if (subscriptions.Value.TryGetValue(sender, out IDisposable subscription)) { subscription.Dispose(); subscriptions.Value.Remove(sender); @@ -134,9 +124,7 @@ namespace Avalonia.Controls.Mixins { if (oldValue != newValue) { - var child = oldValue as IControl; - - if (child != null) + if (oldValue is IControl child) { logicalChildren.Remove(child); } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 2e668fda95..a7b8981583 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -408,12 +408,15 @@ namespace Avalonia.Controls.Primitives var panel = (InputElement)Presenter.Panel; - foreach (var container in e.Containers) + if (panel != null) { - if (KeyboardNavigation.GetTabOnceActiveElement(panel) == container.ContainerControl) + foreach (var container in e.Containers) { - KeyboardNavigation.SetTabOnceActiveElement(panel, null); - break; + if (KeyboardNavigation.GetTabOnceActiveElement(panel) == container.ContainerControl) + { + KeyboardNavigation.SetTabOnceActiveElement(panel, null); + break; + } } } } diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 8514104c91..296134ca48 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -247,6 +247,7 @@ namespace Avalonia.Controls.Primitives foreach (var child in this.GetTemplateChildren()) { child.SetValue(TemplatedParentProperty, null); + ((ISetLogicalParent)child).SetParent(null); } VisualChildren.Clear(); diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 3cbfdbd657..e0fefef0b7 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -49,14 +49,6 @@ namespace Avalonia.Controls /// public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot, INameScope { - private static List s_windows = new List(); - - /// - /// Retrieves an enumeration of all Windows in the currently running application. - /// - public static IReadOnlyList OpenWindows => s_windows; - - /// /// Defines the property. /// public static readonly StyledProperty SizeToContentProperty = @@ -75,7 +67,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(ShowInTaskbar), true); /// - /// Enables or disables the taskbar icon + /// Represents the current window state (normal, minimized, maximized) /// public static readonly StyledProperty WindowStateProperty = AvaloniaProperty.Register(nameof(WindowState)); @@ -117,7 +109,7 @@ namespace Avalonia.Controls BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White); TitleProperty.Changed.AddClassHandler((s, e) => s.PlatformImpl?.SetTitle((string)e.NewValue)); HasSystemDecorationsProperty.Changed.AddClassHandler( - (s, e) => s.PlatformImpl?.SetSystemDecorations((bool) e.NewValue)); + (s, e) => s.PlatformImpl?.SetSystemDecorations((bool)e.NewValue)); ShowInTaskbarProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue)); @@ -149,7 +141,7 @@ namespace Avalonia.Controls _maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size); Screens = new Screens(PlatformImpl?.Screen); } - + /// event EventHandler INameScope.Registered { @@ -199,7 +191,7 @@ namespace Avalonia.Controls get { return GetValue(HasSystemDecorationsProperty); } set { SetValue(HasSystemDecorationsProperty, value); } } - + /// /// Enables or disables the taskbar icon /// @@ -259,6 +251,26 @@ namespace Avalonia.Controls /// public event EventHandler Closing; + private static void AddWindow(Window window) + { + if (Application.Current == null) + { + return; + } + + Application.Current.Windows.Add(window); + } + + private static void RemoveWindow(Window window) + { + if (Application.Current == null) + { + return; + } + + Application.Current.Windows.Remove(window); + } + /// /// Closes the window. /// @@ -298,10 +310,9 @@ namespace Avalonia.Controls finally { if (ignoreCancel || !cancelClosing) - { - s_windows.Remove(this); + { PlatformImpl?.Dispose(); - IsVisible = false; + HandleClosed(); } } } @@ -359,10 +370,9 @@ namespace Avalonia.Controls return; } - s_windows.Add(this); + AddWindow(this); EnsureInitialized(); - SetWindowStartupLocation(); IsVisible = true; LayoutManager.Instance.ExecuteInitialLayoutPass(this); @@ -371,6 +381,7 @@ namespace Avalonia.Controls PlatformImpl?.Show(); Renderer?.Start(); } + SetWindowStartupLocation(); } /// @@ -400,7 +411,7 @@ namespace Avalonia.Controls throw new InvalidOperationException("The window is already being shown."); } - s_windows.Add(this); + AddWindow(this); EnsureInitialized(); SetWindowStartupLocation(); @@ -409,7 +420,7 @@ namespace Avalonia.Controls using (BeginAutoSizing()) { - var affectedWindows = s_windows.Where(w => w.IsEnabled && w != this).ToList(); + var affectedWindows = Application.Current.Windows.Where(w => w.IsEnabled && w != this).ToList(); var activated = affectedWindows.Where(w => w.IsActive).FirstOrDefault(); SetIsEnabled(affectedWindows, false); @@ -513,8 +524,8 @@ namespace Avalonia.Controls protected override void HandleClosed() { - IsVisible = false; - s_windows.Remove(this); + RemoveWindow(this); + base.HandleClosed(); } diff --git a/src/Avalonia.Controls/WindowCollection.cs b/src/Avalonia.Controls/WindowCollection.cs new file mode 100644 index 0000000000..c21a12f05b --- /dev/null +++ b/src/Avalonia.Controls/WindowCollection.cs @@ -0,0 +1,134 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections; +using System.Collections.Generic; + +using Avalonia.Controls; + +namespace Avalonia +{ + public class WindowCollection : IReadOnlyList + { + private readonly Application _application; + private readonly List _windows = new List(); + + public WindowCollection(Application application) + { + _application = application; + } + + /// + /// + /// Gets the number of elements in the collection. + /// + public int Count => _windows.Count; + + /// + /// + /// Gets the at the specified index. + /// + /// + /// The . + /// + /// The index. + /// + public Window this[int index] => _windows[index]; + + /// + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// An enumerator that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator() + { + return _windows.GetEnumerator(); + } + + /// + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Adds the specified window. + /// + /// The window. + internal void Add(Window window) + { + if (window == null) + { + return; + } + + _windows.Add(window); + } + + /// + /// Removes the specified window. + /// + /// The window. + internal void Remove(Window window) + { + if (window == null) + { + return; + } + + _windows.Remove(window); + + OnRemoveWindow(window); + } + + /// + /// Closes all windows and removes them from the underlying collection. + /// + internal void Clear() + { + while (_windows.Count > 0) + { + _windows[0].Close(); + } + } + + private void OnRemoveWindow(Window window) + { + if (window == null) + { + return; + } + + if (_application.IsExiting) + { + return; + } + + switch (_application.ExitMode) + { + case ExitMode.OnLastWindowClose: + if (Count == 0) + { + _application.Exit(); + } + + break; + case ExitMode.OnMainWindowClose: + if (window == _application.MainWindow) + { + _application.Exit(); + } + + break; + } + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs b/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs index 1f25fa132d..1586c61185 100644 --- a/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs +++ b/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Reactive; using System.Reactive.Linq; +using Avalonia.Reactive; namespace Avalonia.Controls { @@ -55,11 +56,39 @@ namespace Avalonia.Controls public static IObservable GetResourceObservable(this IResourceNode target, string key) { - return Observable.FromEventPattern( - x => target.ResourcesChanged += x, - x => target.ResourcesChanged -= x) - .StartWith((EventPattern)null) - .Select(x => target.FindResource(key)); + return new ResourceObservable(target, key); + } + + private class ResourceObservable : LightweightObservableBase + { + private readonly IResourceNode _target; + private readonly string _key; + + public ResourceObservable(IResourceNode target, string key) + { + _target = target; + _key = key; + } + + protected override void Initialize() + { + _target.ResourcesChanged += ResourcesChanged; + } + + protected override void Deinitialize() + { + _target.ResourcesChanged -= ResourcesChanged; + } + + protected override void Subscribed(IObserver observer, bool first) + { + observer.OnNext(_target.FindResource(_key)); + } + + private void ResourcesChanged(object sender, ResourcesChangedEventArgs e) + { + PublishNext(_target.FindResource(_key)); + } } } } diff --git a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs index 2858d11d9d..7186143bf9 100644 --- a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs +++ b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reactive.Linq; using System.Reflection; using Avalonia.Controls; +using Avalonia.Reactive; namespace Avalonia.LogicalTree { @@ -23,75 +24,122 @@ namespace Avalonia.LogicalTree /// The name of the control to find. public static IObservable Track(ILogical relativeTo, string name) { - var attached = Observable.FromEventPattern( - x => relativeTo.AttachedToLogicalTree += x, - x => relativeTo.AttachedToLogicalTree -= x) - .Select(x => ((ILogical)x.Sender).FindNameScope()) - .StartWith(relativeTo.FindNameScope()); - - var detached = Observable.FromEventPattern( - x => relativeTo.DetachedFromLogicalTree += x, - x => relativeTo.DetachedFromLogicalTree -= x) - .Select(x => (INameScope)null); - - return attached.Merge(detached).Select(nameScope => + return new ControlTracker(relativeTo, name); + } + + public static IObservable Track(ILogical relativeTo, int ancestorLevel, Type ancestorType = null) + { + return new ControlTracker(relativeTo, ancestorLevel, ancestorType); + } + + private class ControlTracker : LightweightObservableBase + { + private readonly ILogical _relativeTo; + private readonly string _name; + private readonly int _ancestorLevel; + private readonly Type _ancestorType; + INameScope _nameScope; + ILogical _value; + + public ControlTracker(ILogical relativeTo, string name) + { + _relativeTo = relativeTo; + _name = name; + } + + public ControlTracker(ILogical relativeTo, int ancestorLevel, Type ancestorType) { - if (nameScope != null) + _relativeTo = relativeTo; + _ancestorLevel = ancestorLevel; + _ancestorType = ancestorType; + } + + protected override void Initialize() + { + Update(); + _relativeTo.AttachedToLogicalTree += Attached; + _relativeTo.DetachedFromLogicalTree += Detached; + } + + protected override void Deinitialize() + { + _relativeTo.AttachedToLogicalTree -= Attached; + _relativeTo.DetachedFromLogicalTree -= Detached; + + if (_nameScope != null) { - var registered = Observable.FromEventPattern( - x => nameScope.Registered += x, - x => nameScope.Registered -= x) - .Where(x => x.EventArgs.Name == name) - .Select(x => x.EventArgs.Element) - .OfType(); - var unregistered = Observable.FromEventPattern( - x => nameScope.Unregistered += x, - x => nameScope.Unregistered -= x) - .Where(x => x.EventArgs.Name == name) - .Select(_ => (ILogical)null); - return registered - .StartWith(nameScope.Find(name)) - .Merge(unregistered); + _nameScope.Registered -= Registered; + _nameScope.Unregistered -= Unregistered; } - else + + _value = null; + } + + protected override void Subscribed(IObserver observer, bool first) + { + observer.OnNext(_value); + } + + private void Attached(object sender, LogicalTreeAttachmentEventArgs e) + { + Update(); + PublishNext(_value); + } + + private void Detached(object sender, LogicalTreeAttachmentEventArgs e) + { + if (_nameScope != null) { - return Observable.Return(null); + _nameScope.Registered -= Registered; + _nameScope.Unregistered -= Unregistered; } - }).Switch(); - } - public static IObservable Track(ILogical relativeTo, int ancestorLevel, Type ancestorType = null) - { - return TrackAttachmentToTree(relativeTo).Select(isAttachedToTree => + _value = null; + PublishNext(null); + } + + private void Registered(object sender, NameScopeEventArgs e) { - if (isAttachedToTree) + if (e.Name == _name && e.Element is ILogical logical) { - return relativeTo.GetLogicalAncestors() - .Where(x => ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) - .ElementAtOrDefault(ancestorLevel); + _value = logical; + PublishNext(logical); } - else + } + + private void Unregistered(object sender, NameScopeEventArgs e) + { + if (e.Name == _name) { - return null; + _value = null; + PublishNext(null); } - }); - } + } - private static IObservable TrackAttachmentToTree(ILogical relativeTo) - { - var attached = Observable.FromEventPattern( - x => relativeTo.AttachedToLogicalTree += x, - x => relativeTo.AttachedToLogicalTree -= x) - .Select(x => true) - .StartWith(relativeTo.IsAttachedToLogicalTree); - - var detached = Observable.FromEventPattern( - x => relativeTo.DetachedFromLogicalTree += x, - x => relativeTo.DetachedFromLogicalTree -= x) - .Select(x => false); - - var attachmentStatus = attached.Merge(detached); - return attachmentStatus; + private void Update() + { + if (_name != null) + { + _nameScope = _relativeTo.FindNameScope(); + + if (_nameScope != null) + { + _nameScope.Registered += Registered; + _nameScope.Unregistered += Unregistered; + _value = _nameScope.Find(_name); + } + else + { + _value = null; + } + } + else + { + _value = _relativeTo.GetLogicalAncestors() + .Where(x => _ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) + .ElementAtOrDefault(_ancestorLevel); + } + } } } } diff --git a/src/Avalonia.Styling/Styling/ActivatedObservable.cs b/src/Avalonia.Styling/Styling/ActivatedObservable.cs index 2cd324fff4..5b2774943a 100644 --- a/src/Avalonia.Styling/Styling/ActivatedObservable.cs +++ b/src/Avalonia.Styling/Styling/ActivatedObservable.cs @@ -2,8 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive; -using System.Reactive.Linq; namespace Avalonia.Styling { @@ -11,14 +9,16 @@ namespace Avalonia.Styling /// An observable which is switched on or off according to an activator observable. /// /// - /// An has two inputs: an activator observable a + /// An has two inputs: an activator observable and a /// observable which produces the activated value. When the activator /// produces true, the will produce the current activated /// value. When the activator produces false it will produce /// . /// - internal class ActivatedObservable : ObservableBase, IDescription + internal class ActivatedObservable : ActivatedValue, IDescription { + private IDisposable _sourceSubscription; + /// /// Initializes a new instance of the class. /// @@ -29,49 +29,49 @@ namespace Avalonia.Styling IObservable activator, IObservable source, string description) + : base(activator, AvaloniaProperty.UnsetValue, description) { - Contract.Requires(activator != null); Contract.Requires(source != null); - Activator = activator; - Description = description; Source = source; } - /// - /// Gets the activator observable. - /// - public IObservable Activator { get; } - - /// - /// Gets a description of the binding. - /// - public string Description { get; } - /// /// Gets an observable which produces the . /// public IObservable Source { get; } - /// - /// Notifies the provider that an observer is to receive notifications. - /// - /// The observer. - /// IDisposable object used to unsubscribe from the observable sequence. - protected override IDisposable SubscribeCore(IObserver observer) + protected override ActivatorListener CreateListener() => new ValueListener(this); + + protected override void Deinitialize() { - Contract.Requires(observer != null); + base.Deinitialize(); + _sourceSubscription.Dispose(); + _sourceSubscription = null; + } + + protected override void Initialize() + { + base.Initialize(); + _sourceSubscription = Source.Subscribe((ValueListener)Listener); + } - var sourceCompleted = Source.LastOrDefaultAsync().Select(_ => Unit.Default); - var activatorCompleted = Activator.LastOrDefaultAsync().Select(_ => Unit.Default); - var completed = sourceCompleted.Merge(activatorCompleted); + protected virtual void NotifyValue(object value) + { + Value = value; + } + + private class ValueListener : ActivatorListener, IObserver + { + public ValueListener(ActivatedObservable parent) + : base(parent) + { + } + protected new ActivatedObservable Parent => (ActivatedObservable)base.Parent; - return Activator - .CombineLatest(Source, (x, y) => new { Active = x, Value = y }) - .Select(x => x.Active ? x.Value : AvaloniaProperty.UnsetValue) - .DistinctUntilChanged() - .TakeUntil(completed) - .Subscribe(observer); + void IObserver.OnCompleted() => Parent.CompletedReceived(); + void IObserver.OnError(Exception error) => Parent.ErrorReceived(error); + void IObserver.OnNext(object value) => Parent.NotifyValue(value); } } } diff --git a/src/Avalonia.Styling/Styling/ActivatedSubject.cs b/src/Avalonia.Styling/Styling/ActivatedSubject.cs index 4c2e8dde85..a8446c4bfb 100644 --- a/src/Avalonia.Styling/Styling/ActivatedSubject.cs +++ b/src/Avalonia.Styling/Styling/ActivatedSubject.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive.Linq; using System.Reactive.Subjects; namespace Avalonia.Styling @@ -11,17 +10,14 @@ namespace Avalonia.Styling /// A subject which is switched on or off according to an activator observable. /// /// - /// An has two inputs: an activator observable and either an - /// or a observable which produces the - /// activated value. When the activator produces true, the will - /// produce the current activated value. When the activator produces false it will produce - /// . + /// An extends to + /// be an . When the object is active then values + /// received via will be passed to the source subject. /// internal class ActivatedSubject : ActivatedObservable, ISubject, IDescription { - private bool? _active; private bool _completed; - private object _value; + private object _pushValue; /// /// Initializes a new instance of the class. @@ -35,7 +31,6 @@ namespace Avalonia.Styling string description) : base(activator, source, description) { - Activator.Subscribe(ActivatorChanged, ActivatorError, ActivatorCompleted); } /// @@ -46,53 +41,57 @@ namespace Avalonia.Styling get { return (ISubject)base.Source; } } - /// - /// Notifies all subscribed observers about the end of the sequence. - /// public void OnCompleted() { - if (_active.Value && !_completed) - { - Source.OnCompleted(); - } + Source.OnCompleted(); } - /// - /// Notifies all subscribed observers with the exception. - /// - /// The exception to send to all subscribed observers. - /// is null. public void OnError(Exception error) { - if (_active.Value && !_completed) - { - Source.OnError(error); - } + Source.OnError(error); } - /// - /// Notifies all subscribed observers with the value. - /// - /// The value to send to all subscribed observers. public void OnNext(object value) { - _value = value; + _pushValue = value; - if (_active.Value && !_completed) + if (IsActive == true && !_completed) { - Source.OnNext(value); + Source.OnNext(_pushValue); } } - private void ActivatorChanged(bool active) + protected override void ActiveChanged(bool active) { - bool first = !_active.HasValue; + bool first = !IsActive.HasValue; - _active = active; + base.ActiveChanged(active); if (!first) { - Source.OnNext(active ? _value : AvaloniaProperty.UnsetValue); + Source.OnNext(active ? _pushValue : AvaloniaProperty.UnsetValue); + } + } + + protected override void CompletedReceived() + { + base.CompletedReceived(); + + if (!_completed) + { + Source.OnCompleted(); + _completed = true; + } + } + + protected override void ErrorReceived(Exception error) + { + base.ErrorReceived(error); + + if (!_completed) + { + Source.OnError(error); + _completed = true; } } diff --git a/src/Avalonia.Styling/Styling/ActivatedValue.cs b/src/Avalonia.Styling/Styling/ActivatedValue.cs index 3b9324c059..908d89b751 100644 --- a/src/Avalonia.Styling/Styling/ActivatedValue.cs +++ b/src/Avalonia.Styling/Styling/ActivatedValue.cs @@ -2,8 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive; -using System.Reactive.Linq; +using Avalonia.Reactive; namespace Avalonia.Styling { @@ -16,12 +15,12 @@ namespace Avalonia.Styling /// will produce the current value. When the activator /// produces false it will produce . /// - internal class ActivatedValue : ObservableBase, IDescription + internal class ActivatedValue : LightweightObservableBase, IDescription { - /// - /// The activator. - /// - private readonly IObservable _activator; + private static readonly object NotSent = new object(); + private IDisposable _activatorSubscription; + private object _value; + private object _last = NotSent; /// /// Initializes a new instance of the class. @@ -34,39 +33,101 @@ namespace Avalonia.Styling object value, string description) { - _activator = activator; + Contract.Requires(activator != null); + + Activator = activator; Value = value; Description = description; + Listener = CreateListener(); } /// - /// Gets the activated value. + /// Gets the activator observable. /// - public object Value - { - get; - } + public IObservable Activator { get; } /// /// Gets a description of the binding. /// - public string Description - { - get; - } + public string Description { get; } + + /// + /// Gets a value indicating whether the activator is active. + /// + public bool? IsActive { get; private set; } /// - /// Notifies the provider that an observer is to receive notifications. + /// Gets the value that will be produced when is true. /// - /// The observer. - /// IDisposable object used to unsubscribe from the observable sequence. - protected override IDisposable SubscribeCore(IObserver observer) + public object Value + { + get => _value; + protected set + { + _value = value; + PublishValue(); + } + } + + protected ActivatorListener Listener { get; } + + protected virtual void ActiveChanged(bool active) + { + IsActive = active; + PublishValue(); + } + + protected virtual void CompletedReceived() => PublishCompleted(); + + protected virtual ActivatorListener CreateListener() => new ActivatorListener(this); + + protected override void Deinitialize() + { + _activatorSubscription.Dispose(); + _activatorSubscription = null; + } + + protected virtual void ErrorReceived(Exception error) => PublishError(error); + + protected override void Initialize() + { + _activatorSubscription = Activator.Subscribe(Listener); + } + + protected override void Subscribed(IObserver observer, bool first) { - Contract.Requires(observer != null); + if (IsActive == true && !first) + { + observer.OnNext(Value); + } + } + + private void PublishValue() + { + if (IsActive.HasValue) + { + var v = IsActive.Value ? Value : AvaloniaProperty.UnsetValue; + + if (!Equals(v, _last)) + { + PublishNext(v); + _last = v; + } + } + } + + protected class ActivatorListener : IObserver + { + public ActivatorListener(ActivatedValue parent) + { + Parent = parent; + } + + protected ActivatedValue Parent { get; } - return _activator - .Select(active => active ? Value : AvaloniaProperty.UnsetValue) - .Subscribe(observer); + void IObserver.OnCompleted() => Parent.CompletedReceived(); + void IObserver.OnError(Exception error) => Parent.ErrorReceived(error); + void IObserver.OnNext(bool value) => Parent.ActiveChanged(value); } } } diff --git a/src/Avalonia.Styling/Styling/StyleActivator.cs b/src/Avalonia.Styling/Styling/StyleActivator.cs index 5bdea9f42e..63945037d8 100644 --- a/src/Avalonia.Styling/Styling/StyleActivator.cs +++ b/src/Avalonia.Styling/Styling/StyleActivator.cs @@ -48,8 +48,8 @@ namespace Avalonia.Styling else { return inputs.CombineLatest() - .Select(values => values.Any(x => x)) - .DistinctUntilChanged(); + .Select(values => values.Any(x => x)) + .DistinctUntilChanged(); } } } diff --git a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs index fb32913e7e..94c0b75c6e 100644 --- a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs @@ -4,10 +4,10 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.Reactive; -using System.Reactive.Linq; using System.Reflection; using System.Text; +using Avalonia.Collections; +using Avalonia.Reactive; namespace Avalonia.Styling { @@ -122,14 +122,7 @@ namespace Avalonia.Styling { if (subscribe) { - var observable = Observable.FromEventPattern< - NotifyCollectionChangedEventHandler, - NotifyCollectionChangedEventArgs>( - x => control.Classes.CollectionChanged += x, - x => control.Classes.CollectionChanged -= x) - .StartWith((EventPattern)null) - .Select(_ => Matches(control.Classes)) - .DistinctUntilChanged(); + var observable = new ClassObserver(control.Classes, _classes.Value); return new SelectorMatch(observable); } else @@ -204,5 +197,60 @@ namespace Avalonia.Styling return builder.ToString(); } + + private class ClassObserver : LightweightObservableBase + { + readonly IList _match; + IAvaloniaReadOnlyList _classes; + bool _value; + + public ClassObserver(IAvaloniaReadOnlyList classes, IList match) + { + _classes = classes; + _match = match; + } + + protected override void Deinitialize() => _classes.CollectionChanged -= ClassesChanged; + + protected override void Initialize() + { + _value = GetResult(); + _classes.CollectionChanged += ClassesChanged; + } + + protected override void Subscribed(IObserver observer, bool first) + { + observer.OnNext(_value); + } + + private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action != NotifyCollectionChangedAction.Move) + { + var value = GetResult(); + + if (value != _value) + { + PublishNext(GetResult()); + _value = value; + } + } + } + + private bool GetResult() + { + int remaining = _match.Count; + + foreach (var c in _classes) + { + if (_match.Contains(c)) + { + --remaining; + } + } + + return remaining == 0; + } + } } } diff --git a/src/Avalonia.Visuals/Avalonia.Visuals.csproj b/src/Avalonia.Visuals/Avalonia.Visuals.csproj index 0f003a4018..c34752a3ef 100644 --- a/src/Avalonia.Visuals/Avalonia.Visuals.csproj +++ b/src/Avalonia.Visuals/Avalonia.Visuals.csproj @@ -8,4 +8,5 @@ + \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/PathGeometry.cs b/src/Avalonia.Visuals/Media/PathGeometry.cs index ecda07ada1..e7a2c8577a 100644 --- a/src/Avalonia.Visuals/Media/PathGeometry.cs +++ b/src/Avalonia.Visuals/Media/PathGeometry.cs @@ -5,6 +5,7 @@ using System; using Avalonia.Collections; using Avalonia.Metadata; using Avalonia.Platform; +using Avalonia.Visuals.Platform; namespace Avalonia.Media { @@ -28,7 +29,7 @@ namespace Avalonia.Media static PathGeometry() { - FiguresProperty.Changed.AddClassHandler((s, e) => + FiguresProperty.Changed.AddClassHandler((s, e) => s.OnFiguresChanged(e.NewValue as PathFigures)); } @@ -40,6 +41,24 @@ namespace Avalonia.Media Figures = new PathFigures(); } + /// + /// Parses the specified path data to a . + /// + /// The s. + /// + public static new PathGeometry Parse(string pathData) + { + var pathGeometry = new PathGeometry(); + + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) + { + parser.Parse(pathData); + } + + return pathGeometry; + } + /// /// Gets or sets the figures. /// diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index 9e4a3cbeae..656526890a 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -5,50 +5,52 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Text; +using Avalonia.Platform; namespace Avalonia.Media { /// /// Parses a path markup string. /// - public class PathMarkupParser + public class PathMarkupParser : IDisposable { - private static readonly Dictionary Commands = new Dictionary - { - { 'F', Command.FillRule }, - { 'M', Command.Move }, - { 'L', Command.Line }, - { 'H', Command.HorizontalLine }, - { 'V', Command.VerticalLine }, - { 'Q', Command.QuadraticBezierCurve }, - { 'T', Command.SmoothQuadraticBezierCurve }, - { 'C', Command.CubicBezierCurve }, - { 'S', Command.SmoothCubicBezierCurve }, - { 'A', Command.Arc }, - { 'Z', Command.Close }, - }; - - private static readonly Dictionary FillRules = new Dictionary - { - {'0', FillRule.EvenOdd }, - {'1', FillRule.NonZero } - }; - - private readonly StreamGeometryContext _context; + private static readonly Dictionary s_commands = + new Dictionary + { + { 'F', Command.FillRule }, + { 'M', Command.Move }, + { 'L', Command.Line }, + { 'H', Command.HorizontalLine }, + { 'V', Command.VerticalLine }, + { 'Q', Command.QuadraticBezierCurve }, + { 'T', Command.SmoothQuadraticBezierCurve }, + { 'C', Command.CubicBezierCurve }, + { 'S', Command.SmoothCubicBezierCurve }, + { 'A', Command.Arc }, + { 'Z', Command.Close }, + }; + + private IGeometryContext _geometryContext; + private Point _currentPoint; + private Point? _previousControlPoint; + private bool _isOpen; + private bool _isDisposed; /// /// Initializes a new instance of the class. /// - /// The context for the geometry. - public PathMarkupParser(StreamGeometryContext context) + /// The geometry context. + /// geometryContext + public PathMarkupParser(IGeometryContext geometryContext) { - _context = context; + if (geometryContext == null) + { + throw new ArgumentNullException(nameof(geometryContext)); + } + + _geometryContext = geometryContext; } - /// - /// Defines the command currently being processed. - /// private enum Command { None, @@ -62,360 +64,505 @@ namespace Avalonia.Media SmoothCubicBezierCurve, SmoothQuadraticBezierCurve, Arc, - Close, + Close + } + + void IDisposable.Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (_isDisposed) + { + return; + } + + if (disposing) + { + _geometryContext = null; + } + + _isDisposed = true; + } + + private static Point MirrorControlPoint(Point controlPoint, Point center) + { + var dir = controlPoint - center; + + return center + -dir; } /// - /// Parses the specified markup string. + /// Parses the specified path data and writes the result to the geometryContext of this instance. /// - /// The markup string. - public void Parse(string s) + /// The path data. + public void Parse(string pathData) { - bool openFigure = false; + var span = pathData.AsSpan(); + _currentPoint = new Point(); - using (StringReader reader = new StringReader(s)) + while(!span.IsEmpty) { - Command command = Command.None; - Point point = new Point(); - bool relative = false; - Point? previousControlPoint = null; + if(!ReadCommand(ref span, out var command, out var relative)) + { + return; + } - while (ReadCommand(reader, ref command, ref relative)) + bool initialCommand = true; + + do { + if (!initialCommand) + { + span = ReadSeparator(span); + } + switch (command) { + case Command.None: + break; case Command.FillRule: - _context.SetFillRule(ReadFillRule(reader)); - previousControlPoint = null; + SetFillRule(ref span); break; - case Command.Move: - if (openFigure) - { - _context.EndFigure(false); - } - - point = ReadPoint(reader, point, relative); - _context.BeginFigure(point, true); - openFigure = true; - previousControlPoint = null; + AddMove(ref span, relative); break; - case Command.Line: - point = ReadPoint(reader, point, relative); - _context.LineTo(point); - previousControlPoint = null; + AddLine(ref span, relative); break; - case Command.HorizontalLine: - if (!relative) - { - point = point.WithX(ReadDouble(reader)); - } - else - { - point = new Point(point.X + ReadDouble(reader), point.Y); - } - - _context.LineTo(point); - previousControlPoint = null; + AddHorizontalLine(ref span, relative); break; - case Command.VerticalLine: - if (!relative) - { - point = point.WithY(ReadDouble(reader)); - } - else - { - point = new Point(point.X, point.Y + ReadDouble(reader)); - } - - _context.LineTo(point); - previousControlPoint = null; + AddVerticalLine(ref span, relative); break; - - case Command.QuadraticBezierCurve: - { - Point handle = ReadPoint(reader, point, relative); - previousControlPoint = handle; - ReadSeparator(reader); - point = ReadPoint(reader, point, relative); - _context.QuadraticBezierTo(handle, point); - break; - } - - case Command.SmoothQuadraticBezierCurve: - { - Point end = ReadPoint(reader, point, relative); - - if(previousControlPoint != null) - previousControlPoint = MirrorControlPoint((Point)previousControlPoint, point); - - _context.QuadraticBezierTo(previousControlPoint ?? point, end); - point = end; - break; - } - case Command.CubicBezierCurve: - { - Point point1 = ReadPoint(reader, point, relative); - ReadSeparator(reader); - Point point2 = ReadPoint(reader, point, relative); - previousControlPoint = point2; - ReadSeparator(reader); - point = ReadPoint(reader, point, relative); - _context.CubicBezierTo(point1, point2, point); - break; - } - + AddCubicBezierCurve(ref span, relative); + break; + case Command.QuadraticBezierCurve: + AddQuadraticBezierCurve(ref span, relative); + break; case Command.SmoothCubicBezierCurve: - { - Point point2 = ReadPoint(reader, point, relative); - ReadSeparator(reader); - Point end = ReadPoint(reader, point, relative); - - if(previousControlPoint != null) - previousControlPoint = MirrorControlPoint((Point)previousControlPoint, point); - - _context.CubicBezierTo(previousControlPoint ?? point, point2, end); - previousControlPoint = point2; - point = end; - break; - } - + AddSmoothCubicBezierCurve(ref span, relative); + break; + case Command.SmoothQuadraticBezierCurve: + AddSmoothQuadraticBezierCurve(ref span, relative); + break; case Command.Arc: - { - Size size = ReadSize(reader); - ReadSeparator(reader); - double rotationAngle = ReadDouble(reader); - ReadSeparator(reader); - bool isLargeArc = ReadBool(reader); - ReadSeparator(reader); - SweepDirection sweepDirection = ReadBool(reader) ? SweepDirection.Clockwise : SweepDirection.CounterClockwise; - ReadSeparator(reader); - point = ReadPoint(reader, point, relative); - - _context.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection); - previousControlPoint = null; - break; - } - + AddArc(ref span, relative); + break; case Command.Close: - _context.EndFigure(true); - openFigure = false; - previousControlPoint = null; + CloseFigure(); break; - default: throw new NotSupportedException("Unsupported command"); } - } - if (openFigure) - { - _context.EndFigure(false); - } + initialCommand = false; + } while (PeekArgument(span)); + + } + + if (_isOpen) + { + _geometryContext.EndFigure(false); } } - private Point MirrorControlPoint(Point controlPoint, Point center) + private void CreateFigure() { - Point dir = (controlPoint - center); - return center + -dir; + if (_isOpen) + { + _geometryContext.EndFigure(false); + } + + _geometryContext.BeginFigure(_currentPoint); + + _isOpen = true; } - private static bool ReadCommand( - StringReader reader, - ref Command command, - ref bool relative) + private void SetFillRule(ref ReadOnlySpan span) { - ReadWhitespace(reader); + if (!ReadArgument(ref span, out var fillRule) || fillRule.Length != 1) + { + throw new InvalidDataException("Invalid fill rule."); + } - int i = reader.Peek(); + FillRule rule; - if (i == -1) + switch (fillRule[0]) { - return false; + case '0': + rule = FillRule.EvenOdd; + break; + case '1': + rule = FillRule.NonZero; + break; + default: + throw new InvalidDataException("Invalid fill rule"); } - else + + _geometryContext.SetFillRule(rule); + } + + private void CloseFigure() + { + if (_isOpen) { - char c = (char)i; - Command next = Command.None; + _geometryContext.EndFigure(true); + } + + _previousControlPoint = null; + + _isOpen = false; + } + + private void AddMove(ref ReadOnlySpan span, bool relative) + { + var currentPoint = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); - if (!Commands.TryGetValue(char.ToUpperInvariant(c), out next)) + _currentPoint = currentPoint; + + CreateFigure(); + + while (PeekArgument(span)) + { + span = ReadSeparator(span); + AddLine(ref span, relative); + + if (!relative) { - if ((char.IsDigit(c) || c == '.' || c == '+' || c == '-') && - (command != Command.None)) - { - return true; - } - else - { - throw new InvalidDataException("Unexpected path command '" + c + "'."); - } + _currentPoint = currentPoint; + CreateFigure(); } + } + } - command = next; - relative = char.IsLower(c); - reader.Read(); - return true; + private void AddLine(ref ReadOnlySpan span, bool relative) + { + _currentPoint = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); + + if (!_isOpen) + { + CreateFigure(); } + + _geometryContext.LineTo(_currentPoint); } - private static FillRule ReadFillRule(StringReader reader) + private void AddHorizontalLine(ref ReadOnlySpan span, bool relative) { - int i = reader.Read(); - if (i == -1) + _currentPoint = relative + ? new Point(_currentPoint.X + ReadDouble(ref span), _currentPoint.Y) + : _currentPoint.WithX(ReadDouble(ref span)); + + if (!_isOpen) { - throw new InvalidDataException("Invalid fill rule"); + CreateFigure(); } - char c = (char)i; - FillRule rule; - if (!FillRules.TryGetValue(c, out rule)) + _geometryContext.LineTo(_currentPoint); + } + + private void AddVerticalLine(ref ReadOnlySpan span, bool relative) + { + _currentPoint = relative + ? new Point(_currentPoint.X, _currentPoint.Y + ReadDouble(ref span)) + : _currentPoint.WithY(ReadDouble(ref span)); + + if (!_isOpen) { - throw new InvalidDataException("Invalid fill rule"); + CreateFigure(); } - return rule; + _geometryContext.LineTo(_currentPoint); } - private static double ReadDouble(StringReader reader) + private void AddCubicBezierCurve(ref ReadOnlySpan span, bool relative) { - ReadWhitespace(reader); + var point1 = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); + + span = ReadSeparator(span); + + var point2 = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); - // TODO: Handle Infinity, NaN and scientific notation. - StringBuilder b = new StringBuilder(); - bool readSign = false; - bool readPoint = false; - bool readExponent = false; - int i; + _previousControlPoint = point2; - while ((i = reader.Peek()) != -1) + span = ReadSeparator(span); + + var point3 = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); + + if (!_isOpen) { - char c = char.ToUpperInvariant((char)i); + CreateFigure(); + } - if (((c == '+' || c == '-') && !readSign) || - (c == '.' && !readPoint) || - (c == 'E' && !readExponent) || - char.IsDigit(c)) - { - if (b.Length != 0 && !readExponent && c == '-') - break; - - b.Append(c); - reader.Read(); + _geometryContext.CubicBezierTo(point1, point2, point3); - if (!readSign) - { - readSign = c == '+' || c == '-'; - } + _currentPoint = point3; + } - if (!readPoint) - { - readPoint = c == '.'; - } + private void AddQuadraticBezierCurve(ref ReadOnlySpan span, bool relative) + { + var start = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); - if (c == 'E') - { - readSign = false; - readExponent = true; - } - } - else - { - break; - } + _previousControlPoint = start; + + span = ReadSeparator(span); + + var end = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); + + if (!_isOpen) + { + CreateFigure(); } - return double.Parse(b.ToString(), CultureInfo.InvariantCulture); + _geometryContext.QuadraticBezierTo(start, end); + + _currentPoint = end; } - private static Point ReadPoint(StringReader reader, Point current, bool relative) + private void AddSmoothCubicBezierCurve(ref ReadOnlySpan span, bool relative) { - if (!relative) + var point2 = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); + + span = ReadSeparator(span); + + var end = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); + + if (_previousControlPoint != null) { - current = new Point(); + _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); } - ReadWhitespace(reader); - double x = current.X + ReadDouble(reader); - ReadSeparator(reader); - double y = current.Y + ReadDouble(reader); - return new Point(x, y); + if (!_isOpen) + { + CreateFigure(); + } + + _geometryContext.CubicBezierTo(_previousControlPoint ?? _currentPoint, point2, end); + + _previousControlPoint = point2; + + _currentPoint = end; } - private static Size ReadSize(StringReader reader) + private void AddSmoothQuadraticBezierCurve(ref ReadOnlySpan span, bool relative) { - ReadWhitespace(reader); - double x = ReadDouble(reader); - ReadSeparator(reader); - double y = ReadDouble(reader); - return new Size(x, y); + var end = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); + + if (_previousControlPoint != null) + { + _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); + } + + if (!_isOpen) + { + CreateFigure(); + } + + _geometryContext.QuadraticBezierTo(_previousControlPoint ?? _currentPoint, end); + + _currentPoint = end; } - private static bool ReadBool(StringReader reader) + private void AddArc(ref ReadOnlySpan span, bool relative) { - return ReadDouble(reader) != 0; + var size = ReadSize(ref span); + + span = ReadSeparator(span); + + var rotationAngle = ReadDouble(ref span); + span = ReadSeparator(span); + var isLargeArc = ReadBool(ref span); + + span = ReadSeparator(span); + + var sweepDirection = ReadBool(ref span) ? SweepDirection.Clockwise : SweepDirection.CounterClockwise; + + span = ReadSeparator(span); + + var end = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); + + if (!_isOpen) + { + CreateFigure(); + } + + _geometryContext.ArcTo(end, size, rotationAngle, isLargeArc, sweepDirection); + + _currentPoint = end; + + _previousControlPoint = null; } - private static Point ReadRelativePoint(StringReader reader, Point lastPoint) + private static bool PeekArgument(ReadOnlySpan span) { - ReadWhitespace(reader); - double x = ReadDouble(reader); - ReadSeparator(reader); - double y = ReadDouble(reader); - return new Point(lastPoint.X + x, lastPoint.Y + y); + span = SkipWhitespace(span); + + return !span.IsEmpty && (span[0] == ',' || span[0] == '-' || span[0] == '.' || char.IsDigit(span[0])); } - private static void ReadSeparator(StringReader reader) + private static bool ReadArgument(ref ReadOnlySpan remaining, out ReadOnlySpan argument) { - int i; - bool readComma = false; + remaining = SkipWhitespace(remaining); + if (remaining.IsEmpty) + { + argument = ReadOnlySpan.Empty; + return false; + } + + var valid = false; + int i = 0; + if (remaining[i] == '-') + { + i++; + } + for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true; - while ((i = reader.Peek()) != -1) + if (i < remaining.Length && remaining[i] == '.') { - char c = (char)i; + valid = false; + i++; + } + for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true; - if (char.IsWhiteSpace(c)) - { - reader.Read(); - } - else if (c == ',') + if (i < remaining.Length) + { + // scientific notation + if (remaining[i] == 'E' || remaining[i] == 'e') { - if (readComma) + valid = false; + i++; + if (remaining[i] == '-' || remaining[i] == '+') { - throw new InvalidDataException("Unexpected ','."); - } + i++; + for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true; + } + } + } - readComma = true; - reader.Read(); - } - else - { - break; - } + if (!valid) + { + argument = ReadOnlySpan.Empty; + return false; } + argument = remaining.Slice(0, i); + remaining = remaining.Slice(i); + return true; } - private static void ReadWhitespace(StringReader reader) + + private static ReadOnlySpan ReadSeparator(ReadOnlySpan span) { - int i; + span = SkipWhitespace(span); + if (!span.IsEmpty && span[0] == ',') + { + span = span.Slice(1); + } + return span; + } + + private static ReadOnlySpan SkipWhitespace(ReadOnlySpan span) + { + int i = 0; + for (; i < span.Length && char.IsWhiteSpace(span[i]); i++) ; + return span.Slice(i); + } - while ((i = reader.Peek()) != -1) + private bool ReadBool(ref ReadOnlySpan span) + { + if (!ReadArgument(ref span, out var boolValue) || boolValue.Length != 1) + { + throw new InvalidDataException("Invalid bool rule."); + } + + switch (boolValue[0]) { - char c = (char)i; + case '0': + return false; + case '1': + return true; + default: + throw new InvalidDataException("Invalid bool rule"); + } + } - if (char.IsWhiteSpace(c)) - { - reader.Read(); - } - else - { - break; - } + private double ReadDouble(ref ReadOnlySpan span) + { + if (!ReadArgument(ref span, out var doubleValue)) + { + throw new InvalidDataException("Invalid double value"); + } + + return double.Parse(doubleValue.ToString(), CultureInfo.InvariantCulture); + } + + private Size ReadSize(ref ReadOnlySpan span) + { + var width = ReadDouble(ref span); + span = ReadSeparator(span); + var height = ReadDouble(ref span); + return new Size(width, height); + } + + private Point ReadPoint(ref ReadOnlySpan span) + { + var x = ReadDouble(ref span); + span = ReadSeparator(span); + var y = ReadDouble(ref span); + return new Point(x, y); + } + + private Point ReadRelativePoint(ref ReadOnlySpan span, Point origin) + { + var x = ReadDouble(ref span); + span = ReadSeparator(span); + var y = ReadDouble(ref span); + return new Point(origin.X + x, origin.Y + y); + } + + private bool ReadCommand(ref ReadOnlySpan span, out Command command, out bool relative) + { + span = SkipWhitespace(span); + if (span.IsEmpty) + { + command = default; + relative = false; + return false; + } + var c = span[0]; + if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out command)) + { + throw new InvalidDataException("Unexpected path command '" + c + "'."); } + relative = char.IsLower(c); + span = span.Slice(1); + return true; } } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/StreamGeometry.cs b/src/Avalonia.Visuals/Media/StreamGeometry.cs index 9848a649aa..d3cb788486 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometry.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometry.cs @@ -35,14 +35,15 @@ namespace Avalonia.Media /// A . public static new StreamGeometry Parse(string s) { - StreamGeometry result = new StreamGeometry(); + var streamGeometry = new StreamGeometry(); - using (StreamGeometryContext ctx = result.Open()) - { - PathMarkupParser parser = new PathMarkupParser(ctx); + using (var context = streamGeometry.Open()) + using (var parser = new PathMarkupParser(context)) + { parser.Parse(s); - return result; } + + return streamGeometry; } /// diff --git a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs index 70b889f817..7521582067 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs @@ -15,7 +15,7 @@ namespace Avalonia.Media /// . /// /// TODO: This class is just a wrapper around IStreamGeometryContextImpl: is it needed? - public class StreamGeometryContext : IDisposable + public class StreamGeometryContext : IGeometryContext { private readonly IStreamGeometryContextImpl _impl; diff --git a/src/Avalonia.Visuals/Platform/IGeometryContext.cs b/src/Avalonia.Visuals/Platform/IGeometryContext.cs new file mode 100644 index 0000000000..ac63837428 --- /dev/null +++ b/src/Avalonia.Visuals/Platform/IGeometryContext.cs @@ -0,0 +1,66 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Media; + +namespace Avalonia.Platform +{ + /// + /// Describes a geometry using drawing commands. + /// + public interface IGeometryContext : IDisposable + { + /// + /// Draws an arc to the specified point. + /// + /// The destination point. + /// The radii of an oval whose perimeter is used to draw the angle. + /// The rotation angle of the oval that specifies the curve. + /// true to draw the arc greater than 180 degrees; otherwise, false. + /// + /// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction. + /// + void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection); + + /// + /// Begins a new figure. + /// + /// The starting point for the figure. + /// Whether the figure is filled. + void BeginFigure(Point startPoint, bool isFilled = true); + + /// + /// Draws a Bezier curve to the specified point. + /// + /// The first control point used to specify the shape of the curve. + /// The second control point used to specify the shape of the curve. + /// The destination point for the end of the curve. + void CubicBezierTo(Point point1, Point point2, Point point3); + + /// + /// Draws a quadratic Bezier curve to the specified point + /// + /// Control point + /// DestinationPoint + void QuadraticBezierTo(Point control, Point endPoint); + + /// + /// Draws a line to the specified point. + /// + /// The destination point. + void LineTo(Point point); + + /// + /// Ends the figure started by . + /// + /// Whether the figure is closed. + void EndFigure(bool isClosed); + + /// + /// Sets the fill rule. + /// + /// The fill rule. + void SetFillRule(FillRule fillRule); + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs b/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs index 386560c6b6..da9505cd2d 100644 --- a/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs @@ -1,62 +1,12 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using Avalonia.Media; - namespace Avalonia.Platform { /// /// Describes a geometry using drawing commands. /// - public interface IStreamGeometryContextImpl : IDisposable - { - /// - /// Draws an arc to the specified point. - /// - /// The destination point. - /// The radii of an oval whose perimeter is used to draw the angle. - /// The rotation angle of the oval that specifies the curve. - /// true to draw the arc greater than 180 degrees; otherwise, false. - /// - /// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction. - /// - void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection); - - /// - /// Begins a new figure. - /// - /// The starting point for the figure. - /// Whether the figure is filled. - void BeginFigure(Point startPoint, bool isFilled); - - /// - /// Draws a Bezier curve to the specified point. - /// - /// The first control point used to specify the shape of the curve. - /// The second control point used to specify the shape of the curve. - /// The destination point for the end of the curve. - void CubicBezierTo(Point point1, Point point2, Point point3); - - /// - /// Draws a quadratic Bezier curve to the specified point - /// - /// Control point - /// DestinationPoint - void QuadraticBezierTo(Point control, Point endPoint); - - /// - /// Draws a line to the specified point. - /// - /// The destination point. - void LineTo(Point point); - - /// - /// Ends the figure started by . - /// - /// Whether the figure is closed. - void EndFigure(bool isClosed); - - void SetFillRule(FillRule fillRule); + public interface IStreamGeometryContextImpl : IGeometryContext + { } } diff --git a/src/Avalonia.Visuals/Platform/PathGeometryContext.cs b/src/Avalonia.Visuals/Platform/PathGeometryContext.cs new file mode 100644 index 0000000000..cc881094fd --- /dev/null +++ b/src/Avalonia.Visuals/Platform/PathGeometryContext.cs @@ -0,0 +1,85 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Media; +using Avalonia.Platform; +using System; + +namespace Avalonia.Visuals.Platform +{ + public class PathGeometryContext : IGeometryContext + { + private PathFigure _currentFigure; + private PathGeometry _pathGeometry; + + public PathGeometryContext(PathGeometry pathGeometry) + { + _pathGeometry = pathGeometry ?? throw new ArgumentNullException(nameof(pathGeometry)); + } + + public void Dispose() + { + _pathGeometry = null; + } + + public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) + { + var arcSegment = new ArcSegment + { + Size = size, + RotationAngle = rotationAngle, + IsLargeArc = isLargeArc, + SweepDirection = sweepDirection, + Point = point + }; + + _currentFigure.Segments.Add(arcSegment); + } + + public void BeginFigure(Point startPoint, bool isFilled) + { + _currentFigure = new PathFigure { StartPoint = startPoint, IsClosed = false, IsFilled = isFilled }; + + _pathGeometry.Figures.Add(_currentFigure); + } + + public void CubicBezierTo(Point point1, Point point2, Point point3) + { + var bezierSegment = new BezierSegment { Point1 = point1, Point2 = point2, Point3 = point3 }; + + _currentFigure.Segments.Add(bezierSegment); + } + + public void QuadraticBezierTo(Point control, Point endPoint) + { + var quadraticBezierSegment = new QuadraticBezierSegment { Point1 = control, Point2 = endPoint }; + + _currentFigure.Segments.Add(quadraticBezierSegment); + } + + public void LineTo(Point point) + { + var lineSegment = new LineSegment + { + Point = point + }; + + _currentFigure.Segments.Add(lineSegment); + } + + public void EndFigure(bool isClosed) + { + if (_currentFigure != null) + { + _currentFigure.IsClosed = isClosed; + } + + _currentFigure = null; + } + + public void SetFillRule(FillRule fillRule) + { + _pathGeometry.FillRule = fillRule; + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index fd6b149837..a14923b410 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -117,6 +117,9 @@ namespace Avalonia.Rendering var scene = Interlocked.Exchange(ref _scene, null); scene?.Dispose(); Stop(); + + Layers.Clear(); + RenderTarget?.Dispose(); } /// diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index e830d5c313..2118b66de2 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs @@ -133,6 +133,7 @@ namespace Avalonia.Rendering /// public void Dispose() { + _renderTarget?.Dispose(); } /// diff --git a/src/Avalonia.Visuals/Rendering/RenderLayers.cs b/src/Avalonia.Visuals/Rendering/RenderLayers.cs index bafd644603..6a45ecd912 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLayers.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLayers.cs @@ -11,11 +11,7 @@ namespace Avalonia.Rendering { private List _inner = new List(); private Dictionary _index = new Dictionary(); - - public RenderLayers() - { - } - + public int Count => _inner.Count; public RenderLayer this[IVisual layerRoot] => _index[layerRoot]; @@ -51,6 +47,16 @@ namespace Avalonia.Rendering } } + public void Clear() + { + foreach (var layer in _index.Values) + { + layer.Bitmap.Dispose(); + } + + _index.Clear(); + } + public bool TryGetValue(IVisual layerRoot, out RenderLayer value) { return _index.TryGetValue(layerRoot, out value); diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index acde49a84a..c2db17cd86 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -3,7 +3,7 @@ using System; using System.Globalization; -using System.Xml.Linq; +using JetBrains.Annotations; namespace Avalonia { @@ -122,6 +122,56 @@ namespace Avalonia return new Vector(a._x - b._x, a._y - b._y); } + /// + /// Check if two vectors are equal (bitwise). + /// + /// + /// + public bool Equals(Vector other) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + return _x == other._x && _y == other._y; + // ReSharper restore CompareOfFloatsByEqualityOperator + } + + /// + /// Check if two vectors are nearly equal (numerically). + /// + /// The other vector. + /// True if vectors are nearly equal. + [Pure] + public bool NearlyEquals(Vector other) + { + const float tolerance = float.Epsilon; + + return Math.Abs(_x - other._x) < tolerance && Math.Abs(_y - other._y) < tolerance; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + + return obj is Vector vector && Equals(vector); + } + + public override int GetHashCode() + { + unchecked + { + return (_x.GetHashCode() * 397) ^ _y.GetHashCode(); + } + } + + public static bool operator ==(Vector left, Vector right) + { + return left.Equals(right); + } + + public static bool operator !=(Vector left, Vector right) + { + return !left.Equals(right); + } + /// /// Returns the string representation of the point. /// diff --git a/src/Avalonia.Visuals/VisualTree/VisualLocator.cs b/src/Avalonia.Visuals/VisualTree/VisualLocator.cs index 6fcb6c0db1..eaca9ed9d7 100644 --- a/src/Avalonia.Visuals/VisualTree/VisualLocator.cs +++ b/src/Avalonia.Visuals/VisualTree/VisualLocator.cs @@ -1,9 +1,8 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; using System.Reflection; -using System.Text; +using Avalonia.Reactive; namespace Avalonia.VisualTree { @@ -11,36 +10,54 @@ namespace Avalonia.VisualTree { public static IObservable Track(IVisual relativeTo, int ancestorLevel, Type ancestorType = null) { - return TrackAttachmentToTree(relativeTo).Select(isAttachedToTree => + return new VisualTracker(relativeTo, ancestorLevel, ancestorType); + } + + private class VisualTracker : LightweightObservableBase + { + private readonly IVisual _relativeTo; + private readonly int _ancestorLevel; + private readonly Type _ancestorType; + + public VisualTracker(IVisual relativeTo, int ancestorLevel, Type ancestorType) + { + _relativeTo = relativeTo; + _ancestorLevel = ancestorLevel; + _ancestorType = ancestorType; + } + + protected override void Initialize() + { + _relativeTo.AttachedToVisualTree += AttachedDetached; + _relativeTo.DetachedFromVisualTree += AttachedDetached; + } + + protected override void Deinitialize() { - if (isAttachedToTree) + _relativeTo.AttachedToVisualTree -= AttachedDetached; + _relativeTo.DetachedFromVisualTree -= AttachedDetached; + } + + protected override void Subscribed(IObserver observer, bool first) + { + observer.OnNext(GetResult()); + } + + private void AttachedDetached(object sender, VisualTreeAttachmentEventArgs e) => PublishNext(GetResult()); + + private IVisual GetResult() + { + if (_relativeTo.IsAttachedToVisualTree) { - return relativeTo.GetVisualAncestors() - .Where(x => ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) - .ElementAtOrDefault(ancestorLevel); + return _relativeTo.GetVisualAncestors() + .Where(x => _ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) + .ElementAtOrDefault(_ancestorLevel); } else { return null; } - }); - } - - private static IObservable TrackAttachmentToTree(IVisual relativeTo) - { - var attached = Observable.FromEventPattern( - x => relativeTo.AttachedToVisualTree += x, - x => relativeTo.AttachedToVisualTree -= x) - .Select(x => true) - .StartWith(relativeTo.IsAttachedToVisualTree); - - var detached = Observable.FromEventPattern( - x => relativeTo.DetachedFromVisualTree += x, - x => relativeTo.DetachedFromVisualTree -= x) - .Select(x => false); - - var attachmentStatus = attached.Merge(detached); - return attachmentStatus; + } } } } diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index 48f74f4d45..6f3d736be4 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -5,12 +5,11 @@ using System; using System.Linq; using System.Reactive; using System.Reactive.Linq; -using System.Reflection; -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 @@ -196,15 +195,11 @@ namespace Avalonia.Data if (!targetIsDataContext) { - var update = target.GetObservable(StyledElement.DataContextProperty) - .Skip(1) - .Select(_ => Unit.Default); var result = ExpressionObserverBuilder.Build( () => target.GetValue(StyledElement.DataContextProperty), path, - update, - enableDataValidation, - typeResolver: TypeResolver); + new UpdateSignal(target, StyledElement.DataContextProperty), + enableDataValidation); return result; } @@ -287,17 +282,12 @@ namespace Avalonia.Data bool enableDataValidation) { Contract.Requires(target != null); - - var update = target.GetObservable(StyledElement.TemplatedParentProperty) - .Skip(1) - .Select(_ => Unit.Default); - + var result = ExpressionObserverBuilder.Build( () => target.GetValue(StyledElement.TemplatedParentProperty), path, - update, - enableDataValidation, - typeResolver: TypeResolver); + new UpdateSignal(target, StyledElement.TemplatedParentProperty), + enableDataValidation); return result; } @@ -317,5 +307,35 @@ namespace Avalonia.Data Observable.Return((object)null); }).Switch(); } + + private class UpdateSignal : SingleSubscriberObservableBase + { + private readonly IAvaloniaObject _target; + private readonly AvaloniaProperty _property; + + public UpdateSignal(IAvaloniaObject target, AvaloniaProperty property) + { + _target = target; + _property = property; + } + + protected override void Subscribed() + { + _target.PropertyChanged += PropertyChanged; + } + + protected override void Unsubscribed() + { + _target.PropertyChanged -= PropertyChanged; + } + + private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + PublishNext(Unit.Default); + } + } + } } } diff --git a/src/OSX/Avalonia.MonoMac/KeyTransform.cs b/src/OSX/Avalonia.MonoMac/KeyTransform.cs index 6d4b58031e..4abd5a24f1 100644 --- a/src/OSX/Avalonia.MonoMac/KeyTransform.cs +++ b/src/OSX/Avalonia.MonoMac/KeyTransform.cs @@ -200,7 +200,7 @@ namespace Avalonia.MonoMac [kVK_Return] = Key.Return, [kVK_Tab] = Key.Tab, [kVK_Space] = Key.Space, - [kVK_Delete] = Key.Delete, + [kVK_Delete] = Key.Back, [kVK_Escape] = Key.Escape, [kVK_Command] = Key.LWin, [kVK_Shift] = Key.LeftShift, diff --git a/src/Skia/Avalonia.Skia/BitmapImpl.cs b/src/Skia/Avalonia.Skia/BitmapImpl.cs deleted file mode 100644 index ccc5a37105..0000000000 --- a/src/Skia/Avalonia.Skia/BitmapImpl.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.IO; -using Avalonia.Platform; -using Avalonia.Rendering; -using SkiaSharp; - -namespace Avalonia.Skia -{ - class BitmapImpl : IRenderTargetBitmapImpl, IWriteableBitmapImpl - { - private Vector _dpi; - - public SKBitmap Bitmap { get; private set; } - - public BitmapImpl(SKBitmap bm) - { - Bitmap = bm; - PixelHeight = bm.Height; - PixelWidth = bm.Width; - _dpi = new Vector(96, 96); - } - - static void ReleaseProc(IntPtr address, object ctx) - { - ((IUnmanagedBlob) ctx).Dispose(); - } - - private static readonly SKBitmapReleaseDelegate ReleaseDelegate = ReleaseProc; - - public BitmapImpl(int width, int height, Vector dpi, PixelFormat? fmt = null) - { - PixelHeight = height; - PixelWidth = width; - _dpi = dpi; - var colorType = fmt?.ToSkColorType() ?? SKImageInfo.PlatformColorType; - var runtimePlatform = AvaloniaLocator.Current?.GetService(); - var runtime = runtimePlatform?.GetRuntimeInfo(); - if (runtime?.IsDesktop == true && runtime?.OperatingSystem == OperatingSystemType.Linux) - colorType = SKColorType.Bgra8888; - - if (runtimePlatform != null) - { - Bitmap = new SKBitmap(); - var nfo = new SKImageInfo(width, height, colorType, SKAlphaType.Premul); - var plat = AvaloniaLocator.Current.GetService(); - var blob = plat.AllocBlob(nfo.BytesSize); - Bitmap.InstallPixels(nfo, blob.Address, nfo.RowBytes, null, ReleaseDelegate, blob); - - } - else - Bitmap = new SKBitmap(width, height, colorType, SKAlphaType.Premul); - Bitmap.Erase(SKColor.Empty); - } - - public void Dispose() - { - Bitmap.Dispose(); - } - - public int PixelWidth { get; private set; } - public int PixelHeight { get; private set; } - - class BitmapDrawingContext : DrawingContextImpl - { - private readonly SKSurface _surface; - - public BitmapDrawingContext(SKBitmap bitmap, Vector dpi, IVisualBrushRenderer visualBrushRenderer) - : this(CreateSurface(bitmap), dpi, visualBrushRenderer) - { - CanUseLcdRendering = false; - } - - private static SKSurface CreateSurface(SKBitmap bitmap) - { - IntPtr length; - var rv = SKSurface.Create(bitmap.Info, bitmap.GetPixels(out length), bitmap.RowBytes); - if (rv == null) - throw new Exception("Unable to create Skia surface"); - return rv; - } - - public BitmapDrawingContext(SKSurface surface, Vector dpi, IVisualBrushRenderer visualBrushRenderer) - : base(surface.Canvas, dpi, visualBrushRenderer) - { - _surface = surface; - } - - public override void Dispose() - { - base.Dispose(); - _surface.Dispose(); - } - } - - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) - { - return new BitmapDrawingContext(Bitmap, _dpi, visualBrushRenderer); - } - - public void Save(Stream stream) - { - IntPtr length; - using (var image = SKImage.FromPixels(Bitmap.Info, Bitmap.GetPixels(out length), Bitmap.RowBytes)) - using (var data = image.Encode()) - { - data.SaveTo(stream); - } - } - - public void Save(string fileName) - { - using (var stream = File.Create(fileName)) - Save(stream); - } - - class BitmapFramebuffer : ILockedFramebuffer - { - private SKBitmap _bmp; - - public BitmapFramebuffer(SKBitmap bmp) - { - _bmp = bmp; - _bmp.LockPixels(); - } - - public void Dispose() - { - _bmp.UnlockPixels(); - _bmp = null; - } - - public IntPtr Address => _bmp.GetPixels(); - public int Width => _bmp.Width; - public int Height => _bmp.Height; - public int RowBytes => _bmp.RowBytes; - public Vector Dpi { get; } = new Vector(96, 96); - public PixelFormat Format => _bmp.ColorType.ToPixelFormat(); - } - - public ILockedFramebuffer Lock() => new BitmapFramebuffer(Bitmap); - } -} diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 22e5652cfb..b7ce6eedc4 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -1,57 +1,114 @@ -using Avalonia.Media; -using SkiaSharp; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Utilities; using Avalonia.Utilities; +using SkiaSharp; namespace Avalonia.Skia { - internal class DrawingContextImpl : IDrawingContextImpl + /// + /// Skia based drawing context. + /// + public class DrawingContextImpl : IDrawingContextImpl { + private readonly IDisposable[] _disposables; private readonly Vector _dpi; + private readonly Stack _maskStack = new Stack(); + private readonly Stack _opacityStack = new Stack(); private readonly Matrix? _postTransform; - private readonly IDisposable[] _disposables; private readonly IVisualBrushRenderer _visualBrushRenderer; - private Stack maskStack = new Stack(); - protected bool CanUseLcdRendering = true; - public SKCanvas Canvas { get; private set; } - - public DrawingContextImpl( - SKCanvas canvas, - Vector dpi, - IVisualBrushRenderer visualBrushRenderer, - params IDisposable[] disposables) + private double _currentOpacity = 1.0f; + private readonly bool _canTextUseLcdRendering; + private Matrix _currentTransform; + + /// + /// Context create info. + /// + public struct CreateInfo + { + /// + /// Canvas to draw to. + /// + public SKCanvas Canvas; + + /// + /// Dpi of drawings. + /// + public Vector Dpi; + + /// + /// Visual brush renderer. + /// + public IVisualBrushRenderer VisualBrushRenderer; + + /// + /// Render text without Lcd rendering. + /// + public bool DisableTextLcdRendering; + } + + /// + /// Create new drawing context. + /// + /// Create info. + /// Array of elements to dispose after drawing has finished. + public DrawingContextImpl(CreateInfo createInfo, params IDisposable[] disposables) { - _dpi = dpi; - if (dpi.X != 96 || dpi.Y != 96) - _postTransform = Matrix.CreateScale(dpi.X / 96, dpi.Y / 96); - _visualBrushRenderer = visualBrushRenderer; + _dpi = createInfo.Dpi; + _visualBrushRenderer = createInfo.VisualBrushRenderer; _disposables = disposables; - Canvas = canvas; + _canTextUseLcdRendering = !createInfo.DisableTextLcdRendering; + + Canvas = createInfo.Canvas; + + if (Canvas == null) + { + throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo)); + } + + if (!_dpi.NearlyEquals(SkiaPlatform.DefaultDpi)) + { + _postTransform = + Matrix.CreateScale(_dpi.X / SkiaPlatform.DefaultDpi.X, _dpi.Y / SkiaPlatform.DefaultDpi.Y); + } + Transform = Matrix.Identity; } + + /// + /// Skia canvas. + /// + public SKCanvas Canvas { get; } + /// public void Clear(Color color) { Canvas.Clear(color.ToSKColor()); } + /// public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect) { - var impl = (BitmapImpl)source.Item; + var drawableImage = (IDrawableBitmapImpl) source.Item; var s = sourceRect.ToSKRect(); var d = destRect.ToSKRect(); - using (var paint = new SKPaint() - { Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity)) }) + + using (var paint = + new SKPaint {Color = new SKColor(255, 255, 255, (byte) (255 * opacity * _currentOpacity))}) { - Canvas.DrawBitmap(impl.Bitmap, s, d, paint); + drawableImage.Draw(this, s, d, paint); } } + /// public void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { PushOpacityMask(opacityMask, opacityMaskRect); @@ -59,17 +116,19 @@ namespace Avalonia.Skia PopOpacityMask(); } + /// public void DrawLine(Pen pen, Point p1, Point p2) { using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) { - Canvas.DrawLine((float)p1.X, (float)p1.Y, (float)p2.X, (float)p2.Y, paint.Paint); + Canvas.DrawLine((float) p1.X, (float) p1.Y, (float) p2.X, (float) p2.Y, paint.Paint); } } + /// public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) { - var impl = (GeometryImpl)geometry; + var impl = (GeometryImpl) geometry; var size = geometry.Bounds.Size; using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper)) @@ -79,6 +138,7 @@ namespace Avalonia.Skia { Canvas.DrawPath(impl.EffectivePath, fill.Paint); } + if (stroke.Paint != null) { Canvas.DrawPath(impl.EffectivePath, stroke.Paint); @@ -86,227 +146,424 @@ namespace Avalonia.Skia } } - private struct PaintState : IDisposable + /// + public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0) { - private readonly SKColor _color; - private readonly SKShader _shader; - private readonly SKPaint _paint; + using (var paint = CreatePaint(pen, rect.Size)) + { + var rc = rect.ToSKRect(); - public PaintState(SKPaint paint, SKColor color, SKShader shader) + if (Math.Abs(cornerRadius) < float.Epsilon) + { + Canvas.DrawRect(rc, paint.Paint); + } + else + { + Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); + } + } + } + + /// + public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0) + { + using (var paint = CreatePaint(brush, rect.Size)) { - _paint = paint; - _color = color; - _shader = shader; + var rc = rect.ToSKRect(); + + if (Math.Abs(cornerRadius) < float.Epsilon) + { + Canvas.DrawRect(rc, paint.Paint); + } + else + { + Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); + } } + } - public void Dispose() + /// + public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) + { + using (var paint = CreatePaint(foreground, text.Size)) { - _paint.Color = _color; - _paint.Shader = _shader; + var textImpl = (FormattedTextImpl) text; + textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering); } } - internal struct PaintWrapper : IDisposable + /// + public IRenderTargetBitmapImpl CreateLayer(Size size) { - //We are saving memory allocations there - //TODO: add more disposable fields if needed - public readonly SKPaint Paint; + var normalizedDpi = new Vector(_dpi.X / SkiaPlatform.DefaultDpi.X, _dpi.Y / SkiaPlatform.DefaultDpi.Y); + var pixelSize = size * normalizedDpi; - private IDisposable _disposable1; - private IDisposable _disposable2; + return CreateRenderTarget((int) pixelSize.Width, (int) pixelSize.Height, _dpi); + } - public IDisposable ApplyTo(SKPaint paint) - { - var state = new PaintState(paint, paint.Color, paint.Shader); + /// + public void PushClip(Rect clip) + { + Canvas.Save(); + Canvas.ClipRect(clip.ToSKRect()); + } - paint.Color = Paint.Color; - paint.Shader = Paint.Shader; + /// + public void PopClip() + { + Canvas.Restore(); + } - return state; - } + /// + public void PushOpacity(double opacity) + { + _opacityStack.Push(_currentOpacity); + _currentOpacity *= opacity; + } - public void AddDisposable(IDisposable disposable) - { - if (_disposable1 == null) - _disposable1 = disposable; - else if (_disposable2 == null) - _disposable2 = disposable; - else - throw new InvalidOperationException(); - } + /// + public void PopOpacity() + { + _currentOpacity = _opacityStack.Pop(); + } - public PaintWrapper(SKPaint paint) + /// + public virtual void Dispose() + { + if (_disposables == null) { - Paint = paint; - _disposable1 = null; - _disposable2 = null; + return; } - public void Dispose() + foreach (var disposable in _disposables) { - Paint?.Dispose(); - _disposable1?.Dispose(); - _disposable2?.Dispose(); + disposable?.Dispose(); } } - internal PaintWrapper CreatePaint(IBrush brush, Size targetSize) + /// + public void PushGeometryClip(IGeometryImpl clip) { - SKPaint paint = new SKPaint(); - var rv = new PaintWrapper(paint); - paint.IsStroke = false; + Canvas.Save(); + Canvas.ClipPath(((GeometryImpl)clip).EffectivePath); + } - - double opacity = brush.Opacity * _currentOpacity; - paint.IsAntialias = true; + /// + public void PopGeometryClip() + { + Canvas.Restore(); + } + + /// + public void PushOpacityMask(IBrush mask, Rect bounds) + { + // TODO: This should be disposed + var paint = new SKPaint(); + + Canvas.SaveLayer(paint); + _maskStack.Push(CreatePaint(mask, bounds.Size)); + } - var solid = brush as ISolidColorBrush; - if (solid != null) + /// + public void PopOpacityMask() + { + using (var paint = new SKPaint { BlendMode = SKBlendMode.DstIn }) { - paint.Color = new SKColor(solid.Color.R, solid.Color.G, solid.Color.B, (byte) (solid.Color.A * opacity)); - return rv; + Canvas.SaveLayer(paint); + using (var paintWrapper = _maskStack.Pop()) + { + Canvas.DrawPaint(paintWrapper.Paint); + } + Canvas.Restore(); } - paint.Color = (new SKColor(255, 255, 255, (byte)(255 * opacity))); - var gradient = brush as IGradientBrush; - if (gradient != null) + Canvas.Restore(); + } + + /// + public Matrix Transform + { + get { return _currentTransform; } + set { - var tileMode = gradient.SpreadMethod.ToSKShaderTileMode(); - var stopColors = gradient.GradientStops.Select(s => s.Color.ToSKColor()).ToArray(); - var stopOffsets = gradient.GradientStops.Select(s => (float)s.Offset).ToArray(); + if (_currentTransform == value) + return; + + _currentTransform = value; + + var transform = value; - var linearGradient = brush as ILinearGradientBrush; - if (linearGradient != null) + if (_postTransform.HasValue) + { + transform *= _postTransform.Value; + } + + Canvas.SetMatrix(transform.ToSKMatrix()); + } + } + + /// + /// Configure paint wrapper for using gradient brush. + /// + /// Paint wrapper. + /// Target size. + /// Gradient brush. + private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Size targetSize, IGradientBrush gradientBrush) + { + var tileMode = gradientBrush.SpreadMethod.ToSKShaderTileMode(); + var stopColors = gradientBrush.GradientStops.Select(s => s.Color.ToSKColor()).ToArray(); + var stopOffsets = gradientBrush.GradientStops.Select(s => (float)s.Offset).ToArray(); + + switch (gradientBrush) + { + case ILinearGradientBrush linearGradient: { var start = linearGradient.StartPoint.ToPixels(targetSize).ToSKPoint(); var end = linearGradient.EndPoint.ToPixels(targetSize).ToSKPoint(); // would be nice to cache these shaders possibly? - using (var shader = SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode)) - paint.Shader = shader; + using (var shader = + SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode)) + { + paintWrapper.Paint.Shader = shader; + } + break; } - else + case IRadialGradientBrush radialGradient: { - var radialGradient = brush as IRadialGradientBrush; - if (radialGradient != null) - { - var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint(); - var radius = (float)radialGradient.Radius; + var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint(); + var radius = (float)(radialGradient.Radius * targetSize.Width); - // TODO: There is no SetAlpha in SkiaSharp - //paint.setAlpha(128); - - // would be nice to cache these shaders possibly? - using (var shader = SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode)) - paint.Shader = shader; + // TODO: There is no SetAlpha in SkiaSharp + //paint.setAlpha(128); + // would be nice to cache these shaders possibly? + using (var shader = + SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode)) + { + paintWrapper.Paint.Shader = shader; } + + break; } + } + } + + /// + /// Configure paint wrapper for using tile brush. + /// + /// Paint wrapper. + /// Target size. + /// Tile brush to use. + /// Tile brush image. + private void ConfigureTileBrush(ref PaintWrapper paintWrapper, Size targetSize, ITileBrush tileBrush, IDrawableBitmapImpl tileBrushImage) + { + var calc = new TileBrushCalculator(tileBrush, + new Size(tileBrushImage.PixelWidth, tileBrushImage.PixelHeight), targetSize); + + var intermediate = CreateRenderTarget( + (int)calc.IntermediateSize.Width, + (int)calc.IntermediateSize.Height, _dpi); + + paintWrapper.AddDisposable(intermediate); + + using (var context = intermediate.CreateDrawingContext(null)) + { + var rect = new Rect(0, 0, tileBrushImage.PixelWidth, tileBrushImage.PixelHeight); - return rv; + context.Clear(Colors.Transparent); + context.PushClip(calc.IntermediateClip); + context.Transform = calc.IntermediateTransform; + context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect); + context.PopClip(); } - var tileBrush = brush as ITileBrush; - var visualBrush = brush as IVisualBrush; - var tileBrushImage = default(BitmapImpl); + var tileTransform = + tileBrush.TileMode != TileMode.None + ? SKMatrix.MakeTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y) + : SKMatrix.MakeIdentity(); - if (visualBrush != null) + SKShaderTileMode tileX = + tileBrush.TileMode == TileMode.None + ? SKShaderTileMode.Clamp + : tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY + ? SKShaderTileMode.Mirror + : SKShaderTileMode.Repeat; + + SKShaderTileMode tileY = + tileBrush.TileMode == TileMode.None + ? SKShaderTileMode.Clamp + : tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY + ? SKShaderTileMode.Mirror + : SKShaderTileMode.Repeat; + + + var image = intermediate.SnapshotImage(); + paintWrapper.AddDisposable(image); + + using (var shader = image.ToShader(tileX, tileY, tileTransform)) { - if (_visualBrushRenderer != null) - { - var intermediateSize = _visualBrushRenderer.GetRenderTargetSize(visualBrush); + paintWrapper.Paint.Shader = shader; + } + } - if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) - { - var intermediate = new BitmapImpl((int)intermediateSize.Width, (int)intermediateSize.Height, _dpi); + /// + /// Configure paint wrapper to use visual brush. + /// + /// Paint wrapper. + /// Visual brush. + /// Visual brush renderer. + /// Tile brush image. + private void ConfigureVisualBrush(ref PaintWrapper paintWrapper, IVisualBrush visualBrush, IVisualBrushRenderer visualBrushRenderer, ref IDrawableBitmapImpl tileBrushImage) + { + if (_visualBrushRenderer == null) + { + throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl."); + } - using (var ctx = intermediate.CreateDrawingContext(_visualBrushRenderer)) - { - ctx.Clear(Colors.Transparent); - _visualBrushRenderer.RenderVisualBrush(ctx, visualBrush); - } + var intermediateSize = visualBrushRenderer.GetRenderTargetSize(visualBrush); - tileBrushImage = intermediate; - rv.AddDisposable(tileBrushImage); - } - } - else + if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) + { + var intermediate = CreateRenderTarget((int)intermediateSize.Width, (int)intermediateSize.Height, _dpi); + + using (var ctx = intermediate.CreateDrawingContext(visualBrushRenderer)) { - throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl."); + ctx.Clear(Colors.Transparent); + + visualBrushRenderer.RenderVisualBrush(ctx, visualBrush); } + + tileBrushImage = intermediate; + paintWrapper.AddDisposable(tileBrushImage); } - else + } + + /// + /// Creates paint wrapper for given brush. + /// + /// Source brush. + /// Target size. + /// Paint wrapper for given brush. + internal PaintWrapper CreatePaint(IBrush brush, Size targetSize) + { + var paint = new SKPaint + { + IsStroke = false, + IsAntialias = true + }; + + var paintWrapper = new PaintWrapper(paint); + + double opacity = brush.Opacity * _currentOpacity; + + if (brush is ISolidColorBrush solid) { - tileBrushImage = (BitmapImpl)((tileBrush as IImageBrush)?.Source?.PlatformImpl.Item); + paint.Color = new SKColor(solid.Color.R, solid.Color.G, solid.Color.B, (byte) (solid.Color.A * opacity)); + + return paintWrapper; } - if (tileBrush != null && tileBrushImage != null) + paint.Color = new SKColor(255, 255, 255, (byte) (255 * opacity)); + + if (brush is IGradientBrush gradient) { - var calc = new TileBrushCalculator(tileBrush, new Size(tileBrushImage.PixelWidth, tileBrushImage.PixelHeight), targetSize); - var bitmap = new BitmapImpl((int)calc.IntermediateSize.Width, (int)calc.IntermediateSize.Height, _dpi); - rv.AddDisposable(bitmap); - using (var context = bitmap.CreateDrawingContext(null)) - { - var rect = new Rect(0, 0, tileBrushImage.PixelWidth, tileBrushImage.PixelHeight); + ConfigureGradientBrush(ref paintWrapper, targetSize, gradient); - context.Clear(Colors.Transparent); - context.PushClip(calc.IntermediateClip); - context.Transform = calc.IntermediateTransform; - context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect); - context.PopClip(); - } + return paintWrapper; + } + + var tileBrush = brush as ITileBrush; + var visualBrush = brush as IVisualBrush; + var tileBrushImage = default(IDrawableBitmapImpl); - SKMatrix translation = SKMatrix.MakeTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y); - SKShaderTileMode tileX = - tileBrush.TileMode == TileMode.None - ? SKShaderTileMode.Clamp - : tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY - ? SKShaderTileMode.Mirror - : SKShaderTileMode.Repeat; - - SKShaderTileMode tileY = - tileBrush.TileMode == TileMode.None - ? SKShaderTileMode.Clamp - : tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY - ? SKShaderTileMode.Mirror - : SKShaderTileMode.Repeat; - using (var shader = SKShader.CreateBitmap(bitmap.Bitmap, tileX, tileY, translation)) - paint.Shader = shader; + if (visualBrush != null) + { + ConfigureVisualBrush(ref paintWrapper, visualBrush, _visualBrushRenderer, ref tileBrushImage); + } + else + { + tileBrushImage = (IDrawableBitmapImpl) (tileBrush as IImageBrush)?.Source?.PlatformImpl.Item; } - return rv; + if (tileBrush != null && tileBrushImage != null) + { + ConfigureTileBrush(ref paintWrapper, targetSize, tileBrush, tileBrushImage); + } + + return paintWrapper; } + /// + /// Creates paint wrapper for given pen. + /// + /// Source pen. + /// Target size. + /// private PaintWrapper CreatePaint(Pen pen, Size targetSize) { var rv = CreatePaint(pen.Brush, targetSize); var paint = rv.Paint; paint.IsStroke = true; - paint.StrokeWidth = (float)pen.Thickness; + paint.StrokeWidth = (float) pen.Thickness; - if (pen.StartLineCap == PenLineCap.Round) - paint.StrokeCap = SKStrokeCap.Round; - else if (pen.StartLineCap == PenLineCap.Square) - paint.StrokeCap = SKStrokeCap.Square; - else - paint.StrokeCap = SKStrokeCap.Butt; + // Need to modify dashes due to Skia modifying their lengths + // https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/paths/dots + // TODO: Still something is off, dashes are now present, but don't look the same as D2D ones. + float dashLengthModifier; + float gapLengthModifier; - if (pen.LineJoin == PenLineJoin.Miter) - paint.StrokeJoin = SKStrokeJoin.Miter; - else if (pen.LineJoin == PenLineJoin.Round) - paint.StrokeJoin = SKStrokeJoin.Round; - else - paint.StrokeJoin = SKStrokeJoin.Bevel; + switch (pen.StartLineCap) + { + case PenLineCap.Round: + paint.StrokeCap = SKStrokeCap.Round; + dashLengthModifier = -paint.StrokeWidth; + gapLengthModifier = paint.StrokeWidth; + break; + case PenLineCap.Square: + paint.StrokeCap = SKStrokeCap.Square; + dashLengthModifier = -paint.StrokeWidth; + gapLengthModifier = paint.StrokeWidth; + break; + default: + paint.StrokeCap = SKStrokeCap.Butt; + dashLengthModifier = 0.0f; + gapLengthModifier = 0.0f; + break; + } + + switch (pen.LineJoin) + { + case PenLineJoin.Miter: + paint.StrokeJoin = SKStrokeJoin.Miter; + break; + case PenLineJoin.Round: + paint.StrokeJoin = SKStrokeJoin.Round; + break; + default: + paint.StrokeJoin = SKStrokeJoin.Bevel; + break; + } - paint.StrokeMiter = (float)pen.MiterLimit; + paint.StrokeMiter = (float) pen.MiterLimit; if (pen.DashStyle?.Dashes != null && pen.DashStyle.Dashes.Count > 0) { - var pe = SKPathEffect.CreateDash( - pen.DashStyle?.Dashes.Select(x => (float)x).ToArray(), - (float)pen.DashStyle.Offset); + var srcDashes = pen.DashStyle.Dashes; + var dashesArray = new float[srcDashes.Count]; + + for (var i = 0; i < srcDashes.Count; ++i) + { + var lengthModifier = i % 2 == 0 ? dashLengthModifier : gapLengthModifier; + + // Avalonia dash lengths are relative, but Skia takes absolute sizes - need to scale + dashesArray[i] = (float) srcDashes[i] * paint.StrokeWidth + lengthModifier; + } + + var pe = SKPathEffect.CreateDash(dashesArray, (float) pen.DashStyle.Offset); + paint.PathEffect = pe; rv.AddDisposable(pe); } @@ -314,128 +571,118 @@ namespace Avalonia.Skia return rv; } - public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0) + /// + /// Create new render target compatible with this drawing context. + /// + /// Width. + /// Height. + /// Drawing dpi. + /// Pixel format. + /// + private SurfaceRenderTarget CreateRenderTarget(int width, int height, Vector dpi, PixelFormat? format = null) { - using (var paint = CreatePaint(pen, rect.Size)) + var createInfo = new SurfaceRenderTarget.CreateInfo { - var rc = rect.ToSKRect(); - if (cornerRadius == 0) - { - Canvas.DrawRect(rc, paint.Paint); - } - else - { - Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); - } - } + Width = width, + Height = height, + Dpi = dpi, + Format = format, + DisableTextLcdRendering = !_canTextUseLcdRendering + }; + + return new SurfaceRenderTarget(createInfo); } - public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0) + /// + /// Skia cached paint state. + /// + private struct PaintState : IDisposable { - using (var paint = CreatePaint(brush, rect.Size)) + private readonly SKColor _color; + private readonly SKShader _shader; + private readonly SKPaint _paint; + + public PaintState(SKPaint paint, SKColor color, SKShader shader) { - var rc = rect.ToSKRect(); - if (cornerRadius == 0) - { - Canvas.DrawRect(rc, paint.Paint); - } - else - { - Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); - } + _paint = paint; + _color = color; + _shader = shader; } - } - public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) - { - using (var paint = CreatePaint(foreground, text.Size)) + /// + public void Dispose() { - var textImpl = (FormattedTextImpl)text; - textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, CanUseLcdRendering); + _paint.Color = _color; + _paint.Shader = _shader; } } - public IRenderTargetBitmapImpl CreateLayer(Size size) - { - var pixelSize = size * (_dpi / 96); - return new BitmapImpl((int)pixelSize.Width, (int)pixelSize.Height, _dpi); - } - - public void PushClip(Rect clip) - { - Canvas.Save(); - Canvas.ClipRect(clip.ToSKRect()); - } - - public void PopClip() - { - Canvas.Restore(); - } - - private double _currentOpacity = 1.0f; - private readonly Stack _opacityStack = new Stack(); - - public void PushOpacity(double opacity) + /// + /// Skia paint wrapper. + /// + internal struct PaintWrapper : IDisposable { - _opacityStack.Push(_currentOpacity); - _currentOpacity *= opacity; - } + //We are saving memory allocations there + public readonly SKPaint Paint; - public void PopOpacity() - { - _currentOpacity = _opacityStack.Pop(); - } + private IDisposable _disposable1; + private IDisposable _disposable2; + private IDisposable _disposable3; - public virtual void Dispose() - { - if(_disposables!=null) - foreach (var disposable in _disposables) - disposable?.Dispose(); - } + public PaintWrapper(SKPaint paint) + { + Paint = paint; - public void PushGeometryClip(IGeometryImpl clip) - { - Canvas.Save(); - Canvas.ClipPath(((StreamGeometryImpl)clip).EffectivePath); - } + _disposable1 = null; + _disposable2 = null; + _disposable3 = null; + } - public void PopGeometryClip() - { - Canvas.Restore(); - } + public IDisposable ApplyTo(SKPaint paint) + { + var state = new PaintState(paint, paint.Color, paint.Shader); - public void PushOpacityMask(IBrush mask, Rect bounds) - { - Canvas.SaveLayer(new SKPaint()); - maskStack.Push(CreatePaint(mask, bounds.Size)); - } + paint.Color = Paint.Color; + paint.Shader = Paint.Shader; - public void PopOpacityMask() - { - Canvas.SaveLayer(new SKPaint { BlendMode = SKBlendMode.DstIn }); - using (var paintWrapper = maskStack.Pop()) - { - Canvas.DrawPaint(paintWrapper.Paint); + return state; } - Canvas.Restore(); - Canvas.Restore(); - } - private Matrix _currentTransform; - - public Matrix Transform - { - get { return _currentTransform; } - set + /// + /// Add new disposable to a wrapper. + /// + /// Disposable to add. + public void AddDisposable(IDisposable disposable) { - if (_currentTransform == value) - return; + if (_disposable1 == null) + { + _disposable1 = disposable; + } + else if (_disposable2 == null) + { + _disposable2 = disposable; + } + else if (_disposable3 == null) + { + _disposable3 = disposable; + } + else + { + Debug.Assert(false); - _currentTransform = value; - var transform = value; - if (_postTransform.HasValue) - transform *= _postTransform.Value; - Canvas.SetMatrix(transform.ToSKMatrix()); + // ReSharper disable once HeuristicUnreachableCode + throw new InvalidOperationException( + "PaintWrapper disposable object limit reached. You need to add extra struct fields to support more disposables."); + } + } + + /// + public void Dispose() + { + Paint?.Dispose(); + _disposable1?.Dispose(); + _disposable2?.Dispose(); + _disposable3?.Dispose(); } } } diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 9c18d52cea..d835c83aa6 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -10,6 +10,9 @@ using System.Linq; namespace Avalonia.Skia { + /// + /// Skia formatted text implementation. + /// public class FormattedTextImpl : IFormattedTextImpl { public FormattedTextImpl( @@ -21,7 +24,7 @@ namespace Avalonia.Skia IReadOnlyList spans) { Text = text ?? string.Empty; - + // Replace 0 characters with zero-width spaces (200B) Text = Text.Replace((char)0, (char)0x200B); @@ -352,7 +355,7 @@ namespace Avalonia.Skia { float measuredWidth; string subText = textInput.Substring(textIndex, stop - textIndex); - lengthBreak = (int)paint.BreakText(subText, maxWidth, out measuredWidth) / 2; + lengthBreak = (int)paint.BreakText(subText, maxWidth, out measuredWidth); } //Check for white space or line breakers before the lengthBreak @@ -451,7 +454,6 @@ namespace Avalonia.Skia private void BuildRects() { // Build character rects - var fm = _paint.FontMetrics; SKTextAlign align = _paint.TextAlign; for (int li = 0; li < _skiaLines.Count; li++) @@ -559,18 +561,16 @@ namespace Avalonia.Skia string subString; - float widthConstraint = (_constraint.Width != double.PositiveInfinity) - ? (float)_constraint.Width - : -1; - - for (int c = 0; curOff < length; c++) + float widthConstraint = double.IsPositiveInfinity(_constraint.Width) + ? -1 + : (float)_constraint.Width; + + while(curOff < length) { float lineWidth = -1; int measured; int trailingnumber = 0; - - subString = Text.Substring(curOff); - + float constraint = -1; if (_wrapping == TextWrapping.Wrap) diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index 1956f02d1b..99dbbefd4d 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -1,82 +1,199 @@ -using System; -using System.Collections.Generic; -using System.Text; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Disposables; using Avalonia.Controls.Platform.Surfaces; -using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Skia.Helpers; using SkiaSharp; namespace Avalonia.Skia { + /// + /// Skia render target that renders to a framebuffer surface. No gpu acceleration available. + /// public class FramebufferRenderTarget : IRenderTarget { - private readonly IFramebufferPlatformSurface _surface; + private readonly IFramebufferPlatformSurface _platformSurface; + private SKImageInfo _currentImageInfo; + private IntPtr _currentFramebufferAddress; + private SKSurface _framebufferSurface; + private PixelFormatConversionShim _conversionShim; + private IDisposable _preFramebufferCopyHandler; - public FramebufferRenderTarget(IFramebufferPlatformSurface surface) + /// + /// Create new framebuffer render target using a target surface. + /// + /// Target surface. + public FramebufferRenderTarget(IFramebufferPlatformSurface platformSurface) { - _surface = surface; + _platformSurface = platformSurface; } + /// public void Dispose() { - //Nothing to do here, since we don't own framebuffer + FreeSurface(); + } + + /// + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + { + var framebuffer = _platformSurface.Lock(); + var framebufferImageInfo = new SKImageInfo(framebuffer.Width, framebuffer.Height, + framebuffer.Format.ToSkColorType(), SKAlphaType.Premul); + + CreateSurface(framebufferImageInfo, framebuffer); + + var canvas = _framebufferSurface.Canvas; + + canvas.RestoreToCount(-1); + canvas.Save(); + canvas.ResetMatrix(); + + var createInfo = new DrawingContextImpl.CreateInfo + { + Canvas = canvas, + Dpi = framebuffer.Dpi, + VisualBrushRenderer = visualBrushRenderer, + DisableTextLcdRendering = true + }; + + return new DrawingContextImpl(createInfo, _preFramebufferCopyHandler, framebuffer); + } + + /// + /// Check if two images info are compatible. + /// + /// Current. + /// Desired. + /// True, if images are compatible. + private static bool AreImageInfosCompatible(SKImageInfo currentImageInfo, SKImageInfo desiredImageInfo) + { + return currentImageInfo.Width == desiredImageInfo.Width && + currentImageInfo.Height == desiredImageInfo.Height && + currentImageInfo.ColorType == desiredImageInfo.ColorType; + } + + /// + /// Create Skia surface backed by given framebuffer. + /// + /// Desired image info. + /// Backing framebuffer. + private void CreateSurface(SKImageInfo desiredImageInfo, ILockedFramebuffer framebuffer) + { + if (_framebufferSurface != null && AreImageInfosCompatible(_currentImageInfo, desiredImageInfo) && _currentFramebufferAddress == framebuffer.Address) + { + return; + } + + FreeSurface(); + + _currentFramebufferAddress = framebuffer.Address; + + var surface = SKSurface.Create(desiredImageInfo, _currentFramebufferAddress, framebuffer.RowBytes); + + // If surface cannot be created - try to create a compatibilty shim first + if (surface == null) + { + _conversionShim = new PixelFormatConversionShim(desiredImageInfo, framebuffer.Address); + _preFramebufferCopyHandler = _conversionShim.SurfaceCopyHandler; + + surface = _conversionShim.Surface; + } + + _framebufferSurface = surface ?? throw new Exception("Unable to create a surface for pixel format " + + framebuffer.Format + + " or pixel format translator"); + _currentImageInfo = desiredImageInfo; + } + + /// + /// Free Skia surface. + /// + private void FreeSurface() + { + _conversionShim?.Dispose(); + _conversionShim = null; + _preFramebufferCopyHandler = null; + + if (_conversionShim != null) + { + _framebufferSurface?.Dispose(); + } + + _framebufferSurface = null; + _currentFramebufferAddress = IntPtr.Zero; } - class PixelFormatShim : IDisposable + /// + /// Converts non-compatible pixel formats using bitmap copies. + /// + private class PixelFormatConversionShim : IDisposable { - private readonly SKImageInfo _nfo; - private readonly IntPtr _fb; - private readonly int _rowBytes; - private SKBitmap _bitmap; + private readonly SKBitmap _bitmap; + private readonly SKImageInfo _destinationInfo; + private readonly IntPtr _framebufferAddress; - public PixelFormatShim(SKImageInfo nfo, IntPtr fb, int rowBytes) + public PixelFormatConversionShim(SKImageInfo destinationInfo, IntPtr framebufferAddress) { - _nfo = nfo; - _fb = fb; - _rowBytes = rowBytes; + _destinationInfo = destinationInfo; + _framebufferAddress = framebufferAddress; + + // Create bitmap using default platform settings + _bitmap = new SKBitmap(destinationInfo.Width, destinationInfo.Height); + + if (!_bitmap.CanCopyTo(destinationInfo.ColorType)) + { + _bitmap.Dispose(); + + throw new Exception( + $"Unable to create pixel format shim for conversion from {_bitmap.ColorType} to {destinationInfo.ColorType}"); + } + + Surface = SKSurface.Create(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes); - - _bitmap = new SKBitmap(nfo.Width, nfo.Height); - if (!_bitmap.CanCopyTo(nfo.ColorType)) + if (Surface == null) { _bitmap.Dispose(); + throw new Exception( - $"Unable to create pixel format shim for conversion from {_bitmap.ColorType} to {nfo.ColorType}"); + $"Unable to create pixel format shim surface for conversion from {_bitmap.ColorType} to {destinationInfo.ColorType}"); } + + SurfaceCopyHandler = Disposable.Create(CopySurface); } - public SKSurface CreateSurface() => SKSurface.Create(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes); + /// + /// Skia surface. + /// + public SKSurface Surface { get; } + /// + /// Handler to start conversion via surface copy. + /// + public IDisposable SurfaceCopyHandler { get; } + + /// public void Dispose() { - using (var tmp = _bitmap.Copy(_nfo.ColorType)) - tmp.CopyPixelsTo(_fb, _nfo.BytesPerPixel * _nfo.Height * _rowBytes, _rowBytes); + Surface.Dispose(); _bitmap.Dispose(); } - - } - - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) - { - var fb = _surface.Lock(); - PixelFormatShim shim = null; - SKImageInfo framebuffer = new SKImageInfo(fb.Width, fb.Height, fb.Format.ToSkColorType(), - SKAlphaType.Premul); - var surface = SKSurface.Create(framebuffer, fb.Address, fb.RowBytes) ?? - (shim = new PixelFormatShim(framebuffer, fb.Address, fb.RowBytes)) - .CreateSurface(); - if (surface == null) - throw new Exception("Unable to create a surface for pixel format " + fb.Format + - " or pixel format translator"); - var canvas = surface.Canvas; - - - canvas.RestoreToCount(0); - canvas.Save(); - canvas.ResetMatrix(); - return new DrawingContextImpl(canvas, fb.Dpi, visualBrushRenderer, canvas, surface, shim, fb); + /// + /// Convert and copy surface to a framebuffer. + /// + private void CopySurface() + { + using (var snapshot = Surface.Snapshot()) + { + snapshot.ReadPixels(_destinationInfo, _framebufferAddress, _destinationInfo.RowBytes, 0, 0, + SKImageCachingHint.Disallow); + } + } } } -} +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index fb134b728c..af4cdb8056 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using Avalonia.Media; using Avalonia.Platform; @@ -5,14 +8,169 @@ using SkiaSharp; namespace Avalonia.Skia { - abstract class GeometryImpl : IGeometryImpl + /// + /// A Skia implementation of . + /// + public abstract class GeometryImpl : IGeometryImpl { + private PathCache _pathCache; + + /// public abstract Rect Bounds { get; } public abstract SKPath EffectivePath { get; } - public abstract bool FillContains(Point point); - public abstract Rect GetRenderBounds(Pen pen); - public abstract IGeometryImpl Intersect(IGeometryImpl geometry); - public abstract bool StrokeContains(Pen pen, Point point); - public abstract ITransformedGeometryImpl WithTransform(Matrix transform); + + /// + public bool FillContains(Point point) + { + return PathContainsCore(EffectivePath, point); + } + + /// + public bool StrokeContains(Pen pen, Point point) + { + // Skia requires to compute stroke path to check for point containment. + // Due to that we are caching using stroke width. + // Usually this function is being called with same stroke width per path, so this saves a lot of Skia traffic. + + var strokeWidth = (float)(pen?.Thickness ?? 0); + + if (!_pathCache.HasCacheFor(strokeWidth)) + { + UpdatePathCache(strokeWidth); + } + + return PathContainsCore(_pathCache.CachedStrokePath, point); + } + + /// + /// Update path cache for given stroke width. + /// + /// Stroke width. + private void UpdatePathCache(float strokeWidth) + { + var strokePath = new SKPath(); + + // For stroke widths close to 0 simply use empty path. Render bounds are cached from fill path. + if (Math.Abs(strokeWidth) < float.Epsilon) + { + _pathCache.Cache(strokePath, strokeWidth, Bounds); + } + else + { + using (var paint = new SKPaint()) + { + paint.IsStroke = true; + paint.StrokeWidth = strokeWidth; + + paint.GetFillPath(EffectivePath, strokePath); + + _pathCache.Cache(strokePath, strokeWidth, strokePath.TightBounds.ToAvaloniaRect()); + } + } + } + + /// + /// Check Skia path if it contains a point. + /// + /// Path to check. + /// Point. + /// True, if point is contained in a path. + private static bool PathContainsCore(SKPath path, Point point) + { + return path.Contains((float)point.X, (float)point.Y); + } + + /// + public IGeometryImpl Intersect(IGeometryImpl geometry) + { + var result = EffectivePath.Op(((GeometryImpl) geometry).EffectivePath, SKPathOp.Intersect); + + return result == null ? null : new StreamGeometryImpl(result); + } + + /// + public Rect GetRenderBounds(Pen pen) + { + var strokeWidth = (float)(pen?.Thickness ?? 0); + + if (!_pathCache.HasCacheFor(strokeWidth)) + { + UpdatePathCache(strokeWidth); + } + + return _pathCache.CachedGeometryRenderBounds.Inflate(strokeWidth / 2.0); + } + + /// + public ITransformedGeometryImpl WithTransform(Matrix transform) + { + return new TransformedGeometryImpl(this, transform); + } + + /// + /// Invalidate all caches. Call after chaning path contents. + /// + protected void InvalidateCaches() + { + _pathCache.Invalidate(); + } + + private struct PathCache + { + private float _cachedStrokeWidth; + + /// + /// Tolerance for two stroke widths to be deemed equal + /// + public const float Tolerance = float.Epsilon; + + /// + /// Cached contour path. + /// + public SKPath CachedStrokePath { get; private set; } + + /// + /// Cached geometry render bounds. + /// + public Rect CachedGeometryRenderBounds { get; private set; } + + /// + /// Is cached valid for given stroke width. + /// + /// Stroke width to check. + /// True, if CachedStrokePath can be used for given stroke width. + public bool HasCacheFor(float strokeWidth) + { + return CachedStrokePath != null && Math.Abs(_cachedStrokeWidth - strokeWidth) < Tolerance; + } + + /// + /// Cache path for given stroke width. Takes ownership of a passed path. + /// + /// Path to cache. + /// Stroke width to cache. + /// Render bounds to use. + public void Cache(SKPath path, float strokeWidth, Rect geometryRenderBounds) + { + if (CachedStrokePath != path) + { + CachedStrokePath?.Dispose(); + } + + CachedStrokePath = path; + CachedGeometryRenderBounds = geometryRenderBounds; + _cachedStrokeWidth = strokeWidth; + } + + /// + /// Invalidate cache state. + /// + public void Invalidate() + { + CachedStrokePath?.Dispose(); + CachedGeometryRenderBounds = Rect.Empty; + _cachedStrokeWidth = default(float); + } + } } } diff --git a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs new file mode 100644 index 0000000000..d587a989cc --- /dev/null +++ b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs @@ -0,0 +1,47 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.IO; +using SkiaSharp; + +namespace Avalonia.Skia.Helpers +{ + /// + /// Helps with saving images to stream/file. + /// + public static class ImageSavingHelper + { + /// + /// Save Skia image to a file. + /// + /// Image to save + /// Target file. + public static void SaveImage(SKImage image, string fileName) + { + if (image == null) throw new ArgumentNullException(nameof(image)); + if (fileName == null) throw new ArgumentNullException(nameof(fileName)); + + using (var stream = File.Create(fileName)) + { + SaveImage(image, stream); + } + } + + /// + /// Save Skia image to a stream. + /// + /// Image to save + /// Target stream. + public static void SaveImage(SKImage image, Stream stream) + { + if (image == null) throw new ArgumentNullException(nameof(image)); + if (stream == null) throw new ArgumentNullException(nameof(stream)); + + using (var data = image.Encode()) + { + data.SaveTo(stream); + } + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs b/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs new file mode 100644 index 0000000000..307af708af --- /dev/null +++ b/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs @@ -0,0 +1,35 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Platform; +using SkiaSharp; + +namespace Avalonia.Skia.Helpers +{ + /// + /// Helps with resolving pixel formats to Skia color types. + /// + public static class PixelFormatHelper + { + /// + /// Resolve given format to Skia color type. + /// + /// Format to resolve. + /// Resolved color type. + public static SKColorType ResolveColorType(PixelFormat? format) + { + 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) + { + colorType = SKColorType.Bgra8888; + } + + return colorType; + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs b/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs new file mode 100644 index 0000000000..5aa5de2abc --- /dev/null +++ b/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs @@ -0,0 +1,23 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Platform; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Extended bitmap implementation that allows for drawing it's contents. + /// + internal interface IDrawableBitmapImpl : IBitmapImpl + { + /// + /// Draw bitmap to a drawing context. + /// + /// Drawing context. + /// Source rect. + /// Destination rect. + /// Paint to use. + void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint); + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs new file mode 100644 index 0000000000..332b8547bf --- /dev/null +++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs @@ -0,0 +1,92 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.IO; +using Avalonia.Platform; +using Avalonia.Skia.Helpers; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Immutable Skia bitmap. + /// + public class ImmutableBitmap : IDrawableBitmapImpl + { + private readonly SKImage _image; + + /// + /// Create immutable bitmap from given stream. + /// + /// Stream containing encoded data. + public ImmutableBitmap(Stream stream) + { + using (var skiaStream = new SKManagedStream(stream)) + { + _image = SKImage.FromEncodedData(SKData.Create(skiaStream)); + + if (_image == null) + { + throw new ArgumentException("Unable to load bitmap from provided data"); + } + + PixelWidth = _image.Width; + PixelHeight = _image.Height; + } + } + + /// + /// Create immutable bitmap from given pixel data copy. + /// + /// Width of data pixels. + /// Height of data pixels. + /// Stride of data pixels. + /// Format of data pixels. + /// Data pixels. + public ImmutableBitmap(int width, int height, int stride, PixelFormat format, IntPtr data) + { + var imageInfo = new SKImageInfo(width, height, format.ToSkColorType(), SKAlphaType.Premul); + + _image = SKImage.FromPixelCopy(imageInfo, data, stride); + + if (_image == null) + { + throw new ArgumentException("Unable to create bitmap from provided data"); + } + + PixelWidth = width; + PixelHeight = height; + } + + /// + public int PixelWidth { get; } + + /// + public int PixelHeight { get; } + + /// + public void Dispose() + { + _image.Dispose(); + } + + /// + public void Save(string fileName) + { + ImageSavingHelper.SaveImage(_image, fileName); + } + + /// + public void Save(Stream stream) + { + ImageSavingHelper.SaveImage(_image, stream); + } + + /// + public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) + { + context.Canvas.DrawImage(_image, sourceRect, destRect, paint); + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 50e65f45dc..d4e6403dc9 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -1,21 +1,21 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using System.Collections.Generic; using System.IO; -using System.Linq; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Media; using Avalonia.Platform; -using SkiaSharp; namespace Avalonia.Skia { - public partial class PlatformRenderInterface : IPlatformRenderInterface + /// + /// Skia platform render interface. + /// + public class PlatformRenderInterface : IPlatformRenderInterface { - public IBitmapImpl CreateBitmap(int width, int height) - { - return CreateRenderTargetBitmap(width, height, 96, 96); - } - + /// public IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, @@ -27,27 +27,19 @@ namespace Avalonia.Skia return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans); } + /// public IStreamGeometryImpl CreateStreamGeometry() { return new StreamGeometryImpl(); } - public IBitmapImpl LoadBitmap(System.IO.Stream stream) + /// + public IBitmapImpl LoadBitmap(Stream stream) { - using (var s = new SKManagedStream(stream)) - { - var bitmap = SKBitmap.Decode(s); - if (bitmap != null) - { - return new BitmapImpl(bitmap); - } - else - { - throw new ArgumentException("Unable to load bitmap from provided data"); - } - } + return new ImmutableBitmap(stream); } + /// public IBitmapImpl LoadBitmap(string fileName) { using (var stream = File.OpenRead(fileName)) @@ -56,16 +48,13 @@ namespace Avalonia.Skia } } + /// public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride) { - using (var tmp = new SKBitmap()) - { - tmp.InstallPixels(new SKImageInfo(width, height, format.ToSkColorType(), SKAlphaType.Premul) - , data, stride); - return new BitmapImpl(tmp.Copy()); - } + return new ImmutableBitmap(width, height, stride, format, data); } + /// public IRenderTargetBitmapImpl CreateRenderTargetBitmap( int width, int height, @@ -73,24 +62,47 @@ namespace Avalonia.Skia double dpiY) { if (width < 1) + { throw new ArgumentException("Width can't be less than 1", nameof(width)); + } + if (height < 1) + { throw new ArgumentException("Height can't be less than 1", nameof(height)); + } + + var dpi = new Vector(dpiX, dpiY); - return new BitmapImpl(width, height, new Vector(dpiX, dpiY)); + var createInfo = new SurfaceRenderTarget.CreateInfo + { + Width = width, + Height = height, + Dpi = dpi, + DisableTextLcdRendering = false + }; + + return new SurfaceRenderTarget(createInfo); } + /// public virtual IRenderTarget CreateRenderTarget(IEnumerable surfaces) { - var fb = surfaces?.OfType().FirstOrDefault(); - if (fb == null) - throw new Exception("Skia backend currently only supports framebuffer render target"); - return new FramebufferRenderTarget(fb); + 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(int width, int height, PixelFormat? format = null) { - return new BitmapImpl(width, height, new Vector(96, 96), format); + return new WriteableBitmapImpl(width, height, format); } } -} +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs b/src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs new file mode 100644 index 0000000000..0086671880 --- /dev/null +++ b/src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs @@ -0,0 +1,28 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Controls; +using Avalonia.Skia; + +// ReSharper disable once CheckNamespace +namespace Avalonia +{ + /// + /// Skia appication extensions. + /// + public static class SkiaApplicationExtensions + { + /// + /// Enable Skia renderer. + /// + /// Builder type. + /// Builder. + /// Preferred backend type. + /// Configure builder. + public static T UseSkia(this T builder) where T : AppBuilderBase, new() + { + builder.UseRenderingSubsystem(() => SkiaPlatform.Initialize(), "Skia"); + return builder; + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/SkiaPlatform.cs b/src/Skia/Avalonia.Skia/SkiaPlatform.cs index d3083d3d33..06679478cb 100644 --- a/src/Skia/Avalonia.Skia/SkiaPlatform.cs +++ b/src/Skia/Avalonia.Skia/SkiaPlatform.cs @@ -1,47 +1,31 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; -using System.Collections.Generic; -using System.Text; -using Avalonia.Controls; +using Avalonia.Logging; using Avalonia.Platform; -using Avalonia.Rendering; - -namespace Avalonia -{ - public static class SkiaApplicationExtensions - { - public static T UseSkia(this T builder) where T : AppBuilderBase, new() - { - builder.UseRenderingSubsystem(Skia.SkiaPlatform.Initialize, "Skia"); - return builder; - } - } -} namespace Avalonia.Skia { + /// + /// Skia platform initializer. + /// public static class SkiaPlatform { - private static bool s_forceSoftwareRendering; - + /// + /// Initialize Skia platform. + /// public static void Initialize() { var renderInterface = new PlatformRenderInterface(); + AvaloniaLocator.CurrentMutable .Bind().ToConstant(renderInterface); } - public static bool ForceSoftwareRendering - { - get { return s_forceSoftwareRendering; } - set - { - s_forceSoftwareRendering = value; - - // TODO: I left this property here as place holder. Do we still need the ability to Force software rendering? - // Is it even possible with SkiaSharp? Perhaps kekekes can answer as part of the HW accel work. - // - throw new NotImplementedException(); - } - } + /// + /// Default DPI. + /// + public static Vector DefaultDpi => new Vector(96.0f, 96.0f); } } diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs index 6a1aed8d79..9196ace4d8 100644 --- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs +++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs @@ -1,10 +1,12 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using Avalonia.Media; using Avalonia.Platform; using SkiaSharp; - -namespace Avalonia +namespace Avalonia.Skia { public static class SkiaSharpExtensions { diff --git a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs index 935d6d5e5b..c19ff79d87 100644 --- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs @@ -1,90 +1,103 @@ -using System; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using Avalonia.Media; using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia { - class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl + /// + /// A Skia implementation of a . + /// + public class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl { - Rect _bounds; - SKPath _path; - - public override SKPath EffectivePath => _path; - - public override Rect GetRenderBounds(Pen pen) + private Rect _bounds; + private readonly SKPath _effectivePath; + + /// + /// Initializes a new instance of the class. + /// + /// An existing Skia . + /// Precomputed path bounds. + public StreamGeometryImpl(SKPath path, Rect bounds) { - return GetRenderBounds(pen?.Thickness ?? 0); + _effectivePath = path; + _bounds = bounds; } - public override Rect Bounds => _bounds; - - public IStreamGeometryImpl Clone() + /// + /// Initializes a new instance of the class. + /// + /// An existing Skia . + public StreamGeometryImpl(SKPath path) : this(path, path.TightBounds.ToAvaloniaRect()) { - return new StreamGeometryImpl - { - _path = _path?.Clone(), - _bounds = Bounds - }; } - public IStreamGeometryContextImpl Open() + /// + /// Initializes a new instance of the class. + /// + public StreamGeometryImpl() : this(CreateEmptyPath(), Rect.Empty) { - _path = new SKPath(); - _path.FillType = SKPathFillType.EvenOdd; - - return new StreamContext(this); } + + /// + public override SKPath EffectivePath => _effectivePath; - public override bool FillContains(Point point) - { - // TODO: Not supported by SkiaSharp yet, so use expanded Rect - // return EffectivePath.Contains(point.X, point.Y); - return GetRenderBounds(0).Contains(point); - } - - public override bool StrokeContains(Pen pen, Point point) - { - // TODO: Not supported by SkiaSharp yet, so use expanded Rect - // return EffectivePath.Contains(point.X, point.Y); - return GetRenderBounds(0).Contains(point); - } + /// + public override Rect Bounds => _bounds; - public override IGeometryImpl Intersect(IGeometryImpl geometry) + /// + public IStreamGeometryImpl Clone() { - throw new NotImplementedException(); + return new StreamGeometryImpl(_effectivePath?.Clone(), Bounds); } - public override ITransformedGeometryImpl WithTransform(Matrix transform) + /// + public IStreamGeometryContextImpl Open() { - return new TransformedGeometryImpl(this, transform); + return new StreamContext(this); } - private Rect GetRenderBounds(double strokeThickness) + /// + /// Create new empty . + /// + /// Empty + private static SKPath CreateEmptyPath() { - // TODO: Calculate properly. - return Bounds.Inflate(strokeThickness); + return new SKPath + { + FillType = SKPathFillType.EvenOdd + }; } - class StreamContext : IStreamGeometryContextImpl + /// + /// A Skia implementation of a . + /// + private class StreamContext : IStreamGeometryContextImpl { private readonly StreamGeometryImpl _geometryImpl; - private SKPath _path; + private readonly SKPath _path; - Point _currentPoint; + /// + /// Initializes a new instance of the class. + /// Geometry to operate on. + /// public StreamContext(StreamGeometryImpl geometryImpl) { _geometryImpl = geometryImpl; - _path = _geometryImpl._path; + _path = _geometryImpl._effectivePath; } - + + /// + /// Will update bounds of passed geometry. public void Dispose() { - SKRect rc; - _path.GetBounds(out rc); - _geometryImpl._bounds = rc.ToAvaloniaRect(); + _geometryImpl._bounds = _path.TightBounds.ToAvaloniaRect(); + _geometryImpl.InvalidateCaches(); } + /// public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) { _path.ArcTo( @@ -95,33 +108,33 @@ namespace Avalonia.Skia sweepDirection == SweepDirection.Clockwise ? SKPathDirection.Clockwise : SKPathDirection.CounterClockwise, (float)point.X, (float)point.Y); - _currentPoint = point; } + /// public void BeginFigure(Point startPoint, bool isFilled) { _path.MoveTo((float)startPoint.X, (float)startPoint.Y); - _currentPoint = startPoint; } + /// public void CubicBezierTo(Point point1, Point point2, Point point3) { _path.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (float)point3.X, (float)point3.Y); - _currentPoint = point3; } + /// public void QuadraticBezierTo(Point point1, Point point2) { _path.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y); - _currentPoint = point2; } + /// public void LineTo(Point point) { _path.LineTo((float)point.X, (float)point.Y); - _currentPoint = point; } + /// public void EndFigure(bool isClosed) { if (isClosed) @@ -130,6 +143,7 @@ namespace Avalonia.Skia } } + /// public void SetFillRule(FillRule fillRule) { _path.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding; diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs new file mode 100644 index 0000000000..88200dcfbe --- /dev/null +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -0,0 +1,169 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.IO; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Skia.Helpers; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Skia render target that writes to a surface. + /// + public class SurfaceRenderTarget : IRenderTargetBitmapImpl, IDrawableBitmapImpl + { + private readonly Vector _dpi; + private readonly SKSurface _surface; + private readonly SKCanvas _canvas; + private readonly bool _disableLcdRendering; + + /// + /// Create new surface render target. + /// + /// Create info. + public SurfaceRenderTarget(CreateInfo createInfo) + { + PixelWidth = createInfo.Width; + PixelHeight = createInfo.Height; + _dpi = createInfo.Dpi; + _disableLcdRendering = createInfo.DisableTextLcdRendering; + + _surface = CreateSurface(PixelWidth, PixelHeight, createInfo.Format); + + _canvas = _surface?.Canvas; + + if (_surface == null || _canvas == null) + { + throw new InvalidOperationException("Failed to create Skia render target surface"); + } + } + + /// + /// Create backing Skia surface. + /// + /// Width. + /// Height. + /// Format. + /// + private static SKSurface CreateSurface(int width, int height, PixelFormat? format) + { + var imageInfo = MakeImageInfo(width, height, format); + + return SKSurface.Create(imageInfo); + } + + /// + public void Dispose() + { + _canvas.Dispose(); + _surface.Dispose(); + } + + /// + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + { + _canvas.RestoreToCount(-1); + _canvas.ResetMatrix(); + + var createInfo = new DrawingContextImpl.CreateInfo + { + Canvas = _canvas, + Dpi = _dpi, + VisualBrushRenderer = visualBrushRenderer, + DisableTextLcdRendering = _disableLcdRendering + }; + + return new DrawingContextImpl(createInfo); + } + + /// + public int PixelWidth { get; } + + /// + public int PixelHeight { get; } + + /// + public void Save(string fileName) + { + using (var image = SnapshotImage()) + { + ImageSavingHelper.SaveImage(image, fileName); + } + } + + /// + public void Save(Stream stream) + { + using (var image = SnapshotImage()) + { + ImageSavingHelper.SaveImage(image, stream); + } + } + + /// + public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) + { + using (var image = SnapshotImage()) + { + context.Canvas.DrawImage(image, sourceRect, destRect, paint); + } + } + + /// + /// Create Skia image snapshot from a surface. + /// + /// Image snapshot. + public SKImage SnapshotImage() + { + return _surface.Snapshot(); + } + + /// + /// Create image info for given parameters. + /// + /// Width. + /// Height. + /// Format. + /// + private static SKImageInfo MakeImageInfo(int width, int height, PixelFormat? format) + { + var colorType = PixelFormatHelper.ResolveColorType(format); + + return new SKImageInfo(width, height, colorType, SKAlphaType.Premul); + } + + /// + /// Create info of a surface render target. + /// + public struct CreateInfo + { + /// + /// Width of a render target. + /// + public int Width; + + /// + /// Height of a render target. + /// + public int Height; + + /// + /// Dpi used when rendering to a surface. + /// + public Vector Dpi; + + /// + /// Pixel format of a render target. + /// + public PixelFormat? Format; + + /// + /// Render text without Lcd rendering. + /// + public bool DisableTextLcdRendering; + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs index e14d3f04be..e95069eef3 100644 --- a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs @@ -1,59 +1,43 @@ -using System; -using Avalonia.Media; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia { - class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl + /// + /// A Skia implementation of a . + /// + public class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl { + /// + /// Initializes a new instance of the class. + /// + /// Source geometry. + /// Transform of new geometry. public TransformedGeometryImpl(GeometryImpl source, Matrix transform) { SourceGeometry = source; Transform = transform; - EffectivePath = source.EffectivePath.Clone(); - EffectivePath.Transform(transform.ToSKMatrix()); + + var transformedPath = source.EffectivePath.Clone(); + transformedPath.Transform(transform.ToSKMatrix()); + + EffectivePath = transformedPath; + Bounds = transformedPath.TightBounds.ToAvaloniaRect(); } + /// public override SKPath EffectivePath { get; } + /// public IGeometryImpl SourceGeometry { get; } + /// public Matrix Transform { get; } - public override Rect Bounds => SourceGeometry.Bounds.TransformToAABB(Transform); - - public override bool FillContains(Point point) - { - // TODO: Not supported by SkiaSharp yet, so use expanded Rect - return GetRenderBounds(0).Contains(point); - } - - public override Rect GetRenderBounds(Pen pen) - { - return GetRenderBounds(pen.Thickness); - } - - public override IGeometryImpl Intersect(IGeometryImpl geometry) - { - throw new NotImplementedException(); - } - - public override bool StrokeContains(Pen pen, Point point) - { - // TODO: Not supported by SkiaSharp yet, so use expanded Rect - return GetRenderBounds(0).Contains(point); - } - - public override ITransformedGeometryImpl WithTransform(Matrix transform) - { - return new TransformedGeometryImpl(this, transform); - } - - public Rect GetRenderBounds(double strokeThickness) - { - // TODO: Calculate properly. - return Bounds.Inflate(strokeThickness); - } + /// + public override Rect Bounds { get; } } } diff --git a/src/Skia/Avalonia.Skia/TypefaceCache.cs b/src/Skia/Avalonia.Skia/TypefaceCache.cs index 24674f3b22..f85dd84055 100644 --- a/src/Skia/Avalonia.Skia/TypefaceCache.cs +++ b/src/Skia/Avalonia.Skia/TypefaceCache.cs @@ -1,3 +1,7 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; using System.Collections.Generic; using System.Linq; using Avalonia.Media; @@ -5,7 +9,10 @@ using SkiaSharp; namespace Avalonia.Skia { - static class TypefaceCache + /// + /// Cache for Skia typefaces. + /// + internal static class TypefaceCache { public static SKTypeface Default = SKTypeface.FromFamilyName(FontFamily.Default.Name); static readonly Dictionary> Cache = new Dictionary>(); diff --git a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs new file mode 100644 index 0000000000..ab6c399ff4 --- /dev/null +++ b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs @@ -0,0 +1,151 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.IO; +using Avalonia.Platform; +using Avalonia.Skia.Helpers; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Skia based writeable bitmap. + /// + public class WriteableBitmapImpl : IWriteableBitmapImpl, IDrawableBitmapImpl + { + private static readonly SKBitmapReleaseDelegate s_releaseDelegate = ReleaseProc; + private readonly SKBitmap _bitmap; + + /// + /// Create new writeable bitmap. + /// + /// Width. + /// Height. + /// Format. + public WriteableBitmapImpl(int width, int height, PixelFormat? format = null) + { + PixelHeight = height; + PixelWidth = width; + + var colorType = PixelFormatHelper.ResolveColorType(format); + + var runtimePlatform = AvaloniaLocator.Current?.GetService(); + + if (runtimePlatform != null) + { + _bitmap = new SKBitmap(); + + var nfo = new SKImageInfo(width, height, colorType, SKAlphaType.Premul); + var blob = runtimePlatform.AllocBlob(nfo.BytesSize); + + _bitmap.InstallPixels(nfo, blob.Address, nfo.RowBytes, null, s_releaseDelegate, blob); + } + else + { + _bitmap = new SKBitmap(width, height, colorType, SKAlphaType.Premul); + } + + _bitmap.Erase(SKColor.Empty); + } + + /// + public int PixelWidth { get; } + + /// + public int PixelHeight { get; } + + /// + public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) + { + context.Canvas.DrawBitmap(_bitmap, sourceRect, destRect, paint); + } + + /// + public void Dispose() + { + _bitmap.Dispose(); + } + + /// + public void Save(Stream stream) + { + using (var image = GetSnapshot()) + { + ImageSavingHelper.SaveImage(image, stream); + } + } + + /// + public void Save(string fileName) + { + using (var image = GetSnapshot()) + { + ImageSavingHelper.SaveImage(image, fileName); + } + } + + /// + public ILockedFramebuffer Lock() => new BitmapFramebuffer(_bitmap); + + /// + /// Get snapshot as image. + /// + /// Image snapshot. + public SKImage GetSnapshot() + { + return SKImage.FromPixels(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes); + } + + /// + /// Release given unmanaged blob. + /// + /// Blob address. + /// Blob. + private static void ReleaseProc(IntPtr address, object ctx) + { + ((IUnmanagedBlob)ctx).Dispose(); + } + + /// + /// Framebuffer for bitmap. + /// + private class BitmapFramebuffer : ILockedFramebuffer + { + private SKBitmap _bitmap; + + /// + /// Create framebuffer from given bitmap. + /// + /// Bitmap. + public BitmapFramebuffer(SKBitmap bitmap) + { + _bitmap = bitmap; + } + + /// + public void Dispose() + { + _bitmap = null; + } + + /// + public IntPtr Address => _bitmap.GetPixels(); + + /// + public int Width => _bitmap.Width; + + /// + public int Height => _bitmap.Height; + + /// + public int RowBytes => _bitmap.RowBytes; + + /// + public Vector Dpi { get; } = SkiaPlatform.DefaultDpi; + + /// + public PixelFormat Format => _bitmap.ColorType.ToPixelFormat(); + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/readme.md b/src/Skia/Avalonia.Skia/readme.md index 3defe9c58c..7ed92c5453 100644 --- a/src/Skia/Avalonia.Skia/readme.md +++ b/src/Skia/Avalonia.Skia/readme.md @@ -1,42 +1,22 @@ -TODO: - -BitmapImpl -- constructor from Width/Height -- Save - -StreamGeometryImpl -- Hit testing in Geometry missing as SkiaSharp does not expose this - DrawingContextImpl - Alpha support missing as SkiaSharp does not expose this - Gradient Shader caching? -- TileBrushes - Pen Dash styles Formatted Text Rendering -- minor polish +- Minor polish -RenderTarget -- Figure out a cleaner implementation across all platforms -- HW acceleration +Linux +- Need gpu platform implementation -App Bootstrapping -- Cleanup the testapplications across all platforms -- Add a cleaner Fluent API for the subsystems - - ie. app.UseDirect2D() (via platform specific extension methods) +macOS +- Need gpu platform implementation Android - Not tested at all yet iOS -- Get GLView working again. See HW above - -Win32 -- Cleanup the unmanaged methods (BITMAPINFO) if possible +- Not tested at all yet General -- Cleanup/eliminate obsolete files -- Finish cleanup of the many Test Applications -- Get Skia Unit Tests passing - - +- Get Skia Unit Tests passing (most of the issues are related to antialiasing) \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/ClipboardImpl.cs b/src/Windows/Avalonia.Win32/ClipboardImpl.cs index 3dae8e37d9..a908c9e1e2 100644 --- a/src/Windows/Avalonia.Win32/ClipboardImpl.cs +++ b/src/Windows/Avalonia.Win32/ClipboardImpl.cs @@ -57,6 +57,9 @@ namespace Avalonia.Win32 } await OpenClipboard(); + + UnmanagedMethods.EmptyClipboard(); + try { var hGlobal = Marshal.StringToHGlobalUni(text); diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 9cb22c68cd..f89086ccb7 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -321,6 +321,24 @@ namespace Avalonia.Win32.Interop WS_EX_NOACTIVATE = 0x08000000 } + [Flags] + public enum ClassStyles : uint + { + CS_VREDRAW = 0x0001, + CS_HREDRAW = 0x0002, + CS_DBLCLKS = 0x0008, + CS_OWNDC = 0x0020, + CS_CLASSDC = 0x0040, + CS_PARENTDC = 0x0080, + CS_NOCLOSE = 0x0200, + CS_SAVEBITS = 0x0800, + CS_BYTEALIGNCLIENT = 0x1000, + CS_BYTEALIGNWINDOW = 0x2000, + CS_GLOBALCLASS = 0x4000, + CS_IME = 0x00010000, + CS_DROPSHADOW = 0x00020000 + } + public enum WindowsMessage : uint { WM_NULL = 0x0000, @@ -1203,7 +1221,7 @@ namespace Avalonia.Win32.Interop OFN_NOREADONLYRETURN = 0x00008000, OFN_OVERWRITEPROMPT = 0x00000002 } - + public enum HRESULT : uint { S_FALSE = 0x0001, @@ -1223,7 +1241,7 @@ namespace Avalonia.Win32.Interop public const uint SIGDN_FILESYSPATH = 0x80058000; [Flags] - internal enum FOS : uint + public enum FOS : uint { FOS_OVERWRITEPROMPT = 0x00000002, FOS_STRICTFILETYPES = 0x00000004, @@ -1247,135 +1265,246 @@ namespace Avalonia.Win32.Interop FOS_DEFAULTNOMINIMODE = 0x20000000 } - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public struct OpenFileName + public static class ShellIds { - public int lStructSize; - public IntPtr hwndOwner; - public IntPtr hInstance; - public IntPtr lpstrFilter; - public IntPtr lpstrCustomFilter; - public int nMaxCustFilter; - public int nFilterIndex; - public IntPtr lpstrFile; - public int nMaxFile; - public IntPtr lpstrFileTitle; - public int nMaxFileTitle; - public IntPtr lpstrInitialDir; - public IntPtr lpstrTitle; - public OpenFileNameFlags Flags; - private readonly ushort Unused; - private readonly ushort Unused2; - public IntPtr lpstrDefExt; - public IntPtr lCustData; - public IntPtr lpfnHook; - public IntPtr lpTemplateName; - public IntPtr reservedPtr; - public int reservedInt; - public int flagsEx; - } - } + public static readonly Guid OpenFileDialog = Guid.Parse("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7"); + public static readonly Guid SaveFileDialog = Guid.Parse("C0B4E2F3-BA21-4773-8DBA-335EC946EB8B"); + public static readonly Guid IFileDialog = Guid.Parse("42F85136-DB7E-439C-85F1-E4075D135FC8"); + public static readonly Guid IShellItem = Guid.Parse("43826D1E-E718-42EE-BC55-A1E261C37BFE"); + } - [ComImport(), Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - internal interface IFileDialog - { - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - [PreserveSig()] - uint Show([In, Optional] IntPtr hwndOwner); //IModalWindow + [ComImport(), Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IFileDialog + { + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig()] + uint Show([In, Optional] IntPtr hwndOwner); //IModalWindow - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFileTypes([In] uint cFileTypes, [In, MarshalAs(UnmanagedType.LPArray)] IntPtr rgFilterSpec); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileTypes(uint cFileTypes, [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] COMDLG_FILTERSPEC[] rgFilterSpec); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFileTypeIndex([In] uint iFileType); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileTypeIndex([In] uint iFileType); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetFileTypeIndex(out uint piFileType); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetFileTypeIndex(out uint piFileType); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint Advise([In, MarshalAs(UnmanagedType.Interface)] IntPtr pfde, out uint pdwCookie); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Advise([In, MarshalAs(UnmanagedType.Interface)] IntPtr pfde, out uint pdwCookie); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint Unadvise([In] uint dwCookie); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Unadvise([In] uint dwCookie); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetOptions([In] uint fos); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetOptions([In] uint fos); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetOptions(out uint fos); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetOptions(out uint fos); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, uint fdap); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, uint fdap); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint Close([MarshalAs(UnmanagedType.Error)] uint hr); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Close([MarshalAs(UnmanagedType.Error)] uint hr); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetClientGuid([In] ref Guid guid); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetClientGuid([In] ref Guid guid); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint ClearClientData(); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint ClearClientData(); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter); - } + } + [ComImport, Guid("d57c7288-d4ad-4768-be02-9d969532d960"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IFileOpenDialog + { + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig()] + uint Show([In, Optional] IntPtr hwndOwner); //IModalWindow - [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - internal interface IShellItem - { - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint BindToHandler([In] IntPtr pbc, [In] ref Guid rbhid, [In] ref Guid riid, [Out, MarshalAs(UnmanagedType.Interface)] out IntPtr ppvOut); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileTypes([In] uint cFileTypes, [In, MarshalAs(UnmanagedType.LPArray)] IntPtr rgFilterSpec); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetParent([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFileTypeIndex([In] uint iFileType); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetDisplayName([In] uint sigdnName, out IntPtr ppszName); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetFileTypeIndex(out uint piFileType); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetAttributes([In] uint sfgaoMask, out uint psfgaoAttribs); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Advise([In, MarshalAs(UnmanagedType.Interface)] IntPtr pfde, out uint pdwCookie); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint Compare([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] uint hint, out int piOrder); - + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void Unadvise([In] uint dwCookie); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetOptions([In] uint fos); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetOptions(out uint fos); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, uint fdap); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void Close([MarshalAs(UnmanagedType.Error)] int hr); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetClientGuid([In] ref Guid guid); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void ClearClientData(); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetResults([MarshalAs(UnmanagedType.Interface)] out IShellItemArray ppenum); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetSelectedItems([MarshalAs(UnmanagedType.Interface)] out IShellItemArray ppsai); + } + + [ComImport, Guid("B63EA76D-1F85-456F-A19C-48159EFA858B"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IShellItemArray + { + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void BindToHandler([In, MarshalAs(UnmanagedType.Interface)] IntPtr pbc, [In] ref Guid rbhid, + [In] ref Guid riid, out IntPtr ppvOut); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetPropertyStore([In] int Flags, [In] ref Guid riid, out IntPtr ppv); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetPropertyDescriptionList([In] ref PROPERTYKEY keyType, [In] ref Guid riid, out IntPtr ppv); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetAttributes([In] SIATTRIBFLAGS dwAttribFlags, [In] uint sfgaoMask, out uint psfgaoAttribs); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetCount(out uint pdwNumItems); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetItemAt([In] uint dwIndex, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void EnumItems([MarshalAs(UnmanagedType.Interface)] out IntPtr ppenumShellItems); + } + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + public struct PROPERTYKEY + { + public Guid fmtid; + public uint pid; + } + + public enum SIATTRIBFLAGS + { + SIATTRIBFLAGS_AND = 1, + SIATTRIBFLAGS_APPCOMPAT = 3, + SIATTRIBFLAGS_OR = 2 + } + + [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IShellItem + { + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint BindToHandler([In] IntPtr pbc, [In] ref Guid rbhid, [In] ref Guid riid, [Out, MarshalAs(UnmanagedType.Interface)] out IntPtr ppvOut); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetParent([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetDisplayName([In] uint sigdnName, out IntPtr ppszName); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetAttributes([In] uint sfgaoMask, out uint psfgaoAttribs); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Compare([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] uint hint, out int piOrder); + + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct COMDLG_FILTERSPEC + { + [MarshalAs(UnmanagedType.LPWStr)] + public string pszName; + [MarshalAs(UnmanagedType.LPWStr)] + public string pszSpec; + } } - + [Flags] internal enum DropEffect : int { diff --git a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs index 9b6bf57e92..d555e93a88 100644 --- a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs +++ b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs @@ -1,152 +1,94 @@ +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Platform; +using Avalonia.Win32.Interop; using System; using System.Collections.Generic; -using System.Diagnostics; -using System.IO; using System.Linq; using System.Runtime.InteropServices; -using System.Text; using System.Threading.Tasks; -using Avalonia.Controls; -using Avalonia.Controls.Platform; -using Avalonia.Platform; -using Avalonia.Win32.Interop; namespace Avalonia.Win32 { class SystemDialogImpl : ISystemDialogImpl { - static char[] ToChars(string s) - { - if (s == null) - return null; - var chars = new char[s.Length]; - for (int c = 0; c < s.Length; c++) - chars[c] = s[c]; - return chars; - } - public unsafe Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) { var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero; return Task.Factory.StartNew(() => { - var filters = new StringBuilder(); - foreach (var filter in dialog.Filters) - { - var extMask = string.Join(";", filter.Extensions.Select(e => "*." + e)); - filters.Append(filter.Name); - filters.Append(" ("); - filters.Append(extMask); - filters.Append(")"); - filters.Append('\0'); - filters.Append(extMask); - filters.Append('\0'); - } - if (filters.Length == 0) - filters.Append("All files\0*.*\0"); - filters.Append('\0'); + var result = new string[0]; - var filterBuffer = new char[filters.Length]; - filters.CopyTo(0, filterBuffer, 0, filterBuffer.Length); - - var defExt = ToChars((dialog as SaveFileDialog)?.DefaultExtension); - var fileBuffer = new char[256]; - dialog.InitialFileName?.CopyTo(0, fileBuffer, 0, dialog.InitialFileName.Length); + Guid clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog; + Guid iid = UnmanagedMethods.ShellIds.IFileDialog; + UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk); + var frm = (UnmanagedMethods.IFileDialog)unk; - string userSelectedExt = string.Empty; + var openDialog = dialog as OpenFileDialog; + uint options; + frm.GetOptions(out options); + options |= (uint)(UnmanagedMethods.FOS.FOS_NOVALIDATE | UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT); + if (openDialog?.AllowMultiple == true) + options |= (uint)UnmanagedMethods.FOS.FOS_ALLOWMULTISELECT; + frm.SetOptions(options); - var title = ToChars(dialog.Title); - var initialDir = ToChars(dialog.InitialDirectory); + var defaultExtension = (dialog as SaveFileDialog)?.DefaultExtension ?? ""; + frm.SetDefaultExtension(defaultExtension); + frm.SetFileName(dialog.InitialFileName ?? ""); + frm.SetTitle(dialog.Title); - fixed (char* pFileBuffer = fileBuffer) - fixed (char* pFilterBuffer = filterBuffer) - fixed (char* pDefExt = defExt) - fixed (char* pInitDir = initialDir) - fixed (char* pTitle = title) + var filters = new List(); + foreach (var filter in dialog.Filters) { - var ofn = new UnmanagedMethods.OpenFileName() - { - hwndOwner = hWnd, - hInstance = IntPtr.Zero, - lCustData = IntPtr.Zero, - nFilterIndex = 0, - Flags = - UnmanagedMethods.OpenFileNameFlags.OFN_EXPLORER | - UnmanagedMethods.OpenFileNameFlags.OFN_HIDEREADONLY, - nMaxCustFilter = 0, - nMaxFile = fileBuffer.Length - 1, - nMaxFileTitle = 0, - lpTemplateName = IntPtr.Zero, - lpfnHook = IntPtr.Zero, - lpstrCustomFilter = IntPtr.Zero, - lpstrDefExt = new IntPtr(pDefExt), - lpstrFile = new IntPtr(pFileBuffer), - lpstrFileTitle = IntPtr.Zero, - lpstrFilter = new IntPtr(pFilterBuffer), - lpstrInitialDir = new IntPtr(pInitDir), - lpstrTitle = new IntPtr(pTitle), - - }; - ofn.lStructSize = Marshal.SizeOf(ofn); - if ((dialog as OpenFileDialog)?.AllowMultiple == true) - ofn.Flags |= UnmanagedMethods.OpenFileNameFlags.OFN_ALLOWMULTISELECT; - - if (dialog is SaveFileDialog) - ofn.Flags |= UnmanagedMethods.OpenFileNameFlags.OFN_NOREADONLYRETURN | - UnmanagedMethods.OpenFileNameFlags.OFN_OVERWRITEPROMPT; - - var pofn = &ofn; - - // We should save the current directory to restore it later. - var currentDirectory = Environment.CurrentDirectory; - - var res = dialog is OpenFileDialog - ? UnmanagedMethods.GetOpenFileName(new IntPtr(pofn)) - : UnmanagedMethods.GetSaveFileName(new IntPtr(pofn)); - - // Restore the old current directory, since GetOpenFileName and GetSaveFileName change it after they're called - Environment.CurrentDirectory = currentDirectory; - - if (!res) - return null; - if (dialog?.Filters.Count > 0) - userSelectedExt = dialog.Filters[ofn.nFilterIndex - 1].Extensions.FirstOrDefault(); + var extMask = string.Join(";", filter.Extensions.Select(e => "*." + e)); + filters.Add(new UnmanagedMethods.COMDLG_FILTERSPEC { pszName = filter.Name, pszSpec = extMask }); } - var cStart = 0; - string dir = null; - var files = new List(); - for (var c = 0; c < fileBuffer.Length; c++) + if (filters.Count == 0) + filters.Add(new UnmanagedMethods.COMDLG_FILTERSPEC { pszName = "All files", pszSpec = "*.*" }); + + frm.SetFileTypes((uint)filters.Count, filters.ToArray()); + frm.SetFileTypeIndex(0); + + if (dialog.InitialDirectory != null) { - if (fileBuffer[c] == 0) + UnmanagedMethods.IShellItem directoryShellItem; + Guid riid = UnmanagedMethods.ShellIds.IShellItem; + if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.InitialDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) { - //Encountered double zero char - if (cStart == c) - break; - - var s = new string(fileBuffer, cStart, c - cStart); - if (dir == null) - dir = s; - else - files.Add(s); - cStart = c + 1; + frm.SetFolder(directoryShellItem); + frm.SetDefaultFolder(directoryShellItem); } } - if (files.Count == 0) + + if (frm.Show(hWnd) == (uint)UnmanagedMethods.HRESULT.S_OK) { - if (dialog is SaveFileDialog) + if (openDialog?.AllowMultiple == true) { - if (string.IsNullOrWhiteSpace(Path.GetExtension(dir)) && - !string.IsNullOrWhiteSpace(userSelectedExt) && - !userSelectedExt.Contains("*")) - dir = Path.ChangeExtension(dir, userSelectedExt); + UnmanagedMethods.IShellItemArray shellItemArray; + ((UnmanagedMethods.IFileOpenDialog)frm).GetResults(out shellItemArray); + uint count; + shellItemArray.GetCount(out count); + result = new string[count]; + for (uint i = 0; i < count; i++) + { + UnmanagedMethods.IShellItem shellItem; + shellItemArray.GetItemAt(i, out shellItem); + result[i] = GetAbsoluteFilePath(shellItem); + } + } + else + { + UnmanagedMethods.IShellItem shellItem; + if (frm.GetResult(out shellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) + { + result = new string[] { GetAbsoluteFilePath(shellItem) }; + } } - - return new[] { dir }; } - return files.Select(f => Path.Combine(dir, f)).ToArray(); + return result; }); } @@ -157,11 +99,11 @@ namespace Avalonia.Win32 string result = string.Empty; var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero; - var clsid = Guid.Parse("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7"); - var iid = Guid.Parse("42F85136-DB7E-439C-85F1-E4075D135FC8"); + Guid clsid = UnmanagedMethods.ShellIds.OpenFileDialog; + Guid iid = UnmanagedMethods.ShellIds.IFileDialog; UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk); - var frm = (IFileDialog)unk; + var frm = (UnmanagedMethods.IFileDialog)unk; uint options; frm.GetOptions(out options); options |= (uint)(UnmanagedMethods.FOS.FOS_PICKFOLDERS | UnmanagedMethods.FOS.FOS_FORCEFILESYSTEM | UnmanagedMethods.FOS.FOS_NOVALIDATE | UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT); @@ -169,8 +111,8 @@ namespace Avalonia.Win32 if (dialog.InitialDirectory != null) { - IShellItem directoryShellItem; - var riid = new Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"); //IShellItem + UnmanagedMethods.IShellItem directoryShellItem; + Guid riid = UnmanagedMethods.ShellIds.IShellItem; if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.InitialDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) { frm.SetFolder(directoryShellItem); @@ -179,8 +121,8 @@ namespace Avalonia.Win32 if (dialog.DefaultDirectory != null) { - IShellItem directoryShellItem; - var riid = new Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"); //IShellItem + UnmanagedMethods.IShellItem directoryShellItem; + Guid riid = UnmanagedMethods.ShellIds.IShellItem; if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.DefaultDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) { frm.SetDefaultFolder(directoryShellItem); @@ -189,29 +131,35 @@ namespace Avalonia.Win32 if (frm.Show(hWnd) == (uint)UnmanagedMethods.HRESULT.S_OK) { - IShellItem shellItem; + UnmanagedMethods.IShellItem shellItem; if (frm.GetResult(out shellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) { - IntPtr pszString; - if (shellItem.GetDisplayName(UnmanagedMethods.SIGDN_FILESYSPATH, out pszString) == (uint)UnmanagedMethods.HRESULT.S_OK) - { - if (pszString != IntPtr.Zero) - { - try - { - result = Marshal.PtrToStringAuto(pszString); - } - finally - { - Marshal.FreeCoTaskMem(pszString); - } - } - } + result = GetAbsoluteFilePath(shellItem); } } return result; }); } + + private string GetAbsoluteFilePath(UnmanagedMethods.IShellItem shellItem) + { + IntPtr pszString; + if (shellItem.GetDisplayName(UnmanagedMethods.SIGDN_FILESYSPATH, out pszString) == (uint)UnmanagedMethods.HRESULT.S_OK) + { + if (pszString != IntPtr.Zero) + { + try + { + return Marshal.PtrToStringAuto(pszString); + } + finally + { + Marshal.FreeCoTaskMem(pszString); + } + } + } + return ""; + } } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 9f67f97252..292d8a8138 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -724,7 +724,7 @@ namespace Avalonia.Win32 UnmanagedMethods.WNDCLASSEX wndClassEx = new UnmanagedMethods.WNDCLASSEX { cbSize = Marshal.SizeOf(), - style = 0, + style = (int)(ClassStyles.CS_OWNDC | ClassStyles.CS_HREDRAW | ClassStyles.CS_VREDRAW), // Unique DC helps with performance when using Gpu based rendering lpfnWndProc = _wndProcDelegate, hInstance = UnmanagedMethods.GetModuleHandle(null), hCursor = DefaultCursor, diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs index f8e050df85..d51f56f558 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs @@ -338,6 +338,21 @@ namespace Avalonia.Base.UnitTests.Data.Core GC.KeepAlive(data); } + [Fact] + public void Second_Subscription_Should_Fire_Immediately() + { + var data = new Class1 { StringValue = "foo" }; + var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(string)); + object result = null; + + target.Subscribe(); + target.Subscribe(x => result = x); + + Assert.Equal("foo", result); + + GC.KeepAlive(data); + } + private class Class1 : NotifyingBase { private string _stringValue; diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs index 45c084014b..383030cb6c 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Data.Core.Plugins; using Xunit; @@ -58,9 +57,9 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor); Assert.Equal(0, data.ErrorsChangedSubscriptionCount); - var sub = validator.Subscribe(_ => { }); + validator.Subscribe(_ => { }); Assert.Equal(1, data.ErrorsChangedSubscriptionCount); - sub.Dispose(); + validator.Unsubscribe(); Assert.Equal(0, data.ErrorsChangedSubscriptionCount); } diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index 9e92baf0ff..a460ab5bf3 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -1,6 +1,7 @@  - netcoreapp2.0 + netcoreapp2.0 + Exe @@ -15,7 +16,7 @@ - + diff --git a/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs b/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs new file mode 100644 index 0000000000..c4106340b2 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs @@ -0,0 +1,45 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Media; +using Avalonia.UnitTests; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Visuals.Media +{ + [MemoryDiagnoser] + public class PathMarkupParserTests : IDisposable + { + private IDisposable _app; + + public PathMarkupParserTests() + { + _app = UnitTestApplication.Start(TestServices.StyledWindow); + } + + public void Dispose() + { + _app.Dispose(); + } + + [Benchmark] + public void Parse_Large_Path() + { + const string PathData = "F1 M 16.6309 18.6563C 17.1309 8.15625 29.8809 14.1563 29.8809 14.1563C 30.8809 11.1563 34.1308 11.4063" + + " 34.1308 11.4063C 33.5 12 34.6309 13.1563 34.6309 13.1563C 32.1309 13.1562 31.1309 14.9062 31.1309 14.9" + + "062C 41.1309 23.9062 32.6309 27.9063 32.6309 27.9062C 24.6309 24.9063 21.1309 22.1562 16.6309 18.6563 Z" + + " M 16.6309 19.9063C 21.6309 24.1563 25.1309 26.1562 31.6309 28.6562C 31.6309 28.6562 26.3809 39.1562 18" + + ".3809 36.1563C 18.3809 36.1563 18 38 16.3809 36.9063C 15 36 16.3809 34.9063 16.3809 34.9063C 16.3809 34" + + ".9063 10.1309 30.9062 16.6309 19.9063 Z "; + + var streamGeometry = new StreamGeometry(); + + using (var context = streamGeometry.Open()) + using (var parser = new PathMarkupParser(context)) + { + parser.Parse(PathData); + } + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs new file mode 100644 index 0000000000..694a6d2278 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs @@ -0,0 +1,117 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class ApplicationTests + { + [Fact] + public void Should_Exit_After_MainWindow_Closed() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + Application.Current.ExitMode = ExitMode.OnMainWindowClose; + + var mainWindow = new Window(); + + mainWindow.Show(); + + Application.Current.MainWindow = mainWindow; + + var window = new Window(); + + window.Show(); + + mainWindow.Close(); + + Assert.True(Application.Current.IsExiting); + } + } + + [Fact] + public void Should_Exit_After_Last_Window_Closed() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + Application.Current.ExitMode = ExitMode.OnLastWindowClose; + + var windowA = new Window(); + + windowA.Show(); + + var windowB = new Window(); + + windowB.Show(); + + windowA.Close(); + + Assert.False(Application.Current.IsExiting); + + windowB.Close(); + + Assert.True(Application.Current.IsExiting); + } + } + + [Fact] + public void Should_Only_Exit_On_Explicit_Exit() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + Application.Current.ExitMode = ExitMode.OnExplicitExit; + + var windowA = new Window(); + + windowA.Show(); + + var windowB = new Window(); + + windowB.Show(); + + windowA.Close(); + + Assert.False(Application.Current.IsExiting); + + windowB.Close(); + + Assert.False(Application.Current.IsExiting); + + Application.Current.Exit(); + + Assert.True(Application.Current.IsExiting); + } + } + + [Fact] + public void Should_Close_All_Remaining_Open_Windows_After_Explicit_Exit_Call() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var windows = new List { new Window(), new Window(), new Window(), new Window() }; + + foreach (var window in windows) + { + window.Show(); + } + + Application.Current.Exit(); + + Assert.Empty(Application.Current.Windows); + } + } + + [Fact] + public void Throws_ArgumentNullException_On_Run_If_MainWindow_Is_Null() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + Assert.Throws(() => { Application.Current.Run(null); }); + } + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 4da803353e..9ef1e9f0d2 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -315,6 +315,26 @@ namespace Avalonia.Controls.UnitTests Assert.Same(before, after); } + [Fact] + public void Should_Clear_Containers_When_ItemsPresenter_Changes() + { + var target = new ItemsControl + { + Items = new[] { "foo", "bar" }, + Template = GetTemplate(), + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + Assert.Equal(2, target.ItemContainerGenerator.Containers.Count()); + + target.Template = GetTemplate(); + target.ApplyTemplate(); + + Assert.Empty(target.ItemContainerGenerator.Containers); + } + [Fact] public void Empty_Class_Should_Initially_Be_Applied() { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs index cd71717619..166586ace1 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs @@ -160,6 +160,24 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(target, child.GetLogicalParent()); } + [Fact] + public void Changing_Template_Should_Clear_Old_Templated_Childs_Parent() + { + var target = new TemplatedControl + { + Template = new FuncControlTemplate(_ => new Decorator()) + }; + + target.ApplyTemplate(); + + var child = (Decorator)target.GetVisualChildren().Single(); + + target.Template = new FuncControlTemplate(_ => new Canvas()); + target.ApplyTemplate(); + + Assert.Null(child.Parent); + } + [Fact] public void Nested_Templated_Control_Should_Not_Have_Template_Applied() { diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index a85c4df8af..e80ffd97cd 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -129,7 +129,7 @@ namespace Avalonia.Controls.UnitTests window.Show(); - Assert.Equal(new[] { window }, Window.OpenWindows); + Assert.Equal(new[] { window }, Application.Current.Windows); } } @@ -145,7 +145,7 @@ namespace Avalonia.Controls.UnitTests window.Show(); window.IsVisible = true; - Assert.Equal(new[] { window }, Window.OpenWindows); + Assert.Equal(new[] { window }, Application.Current.Windows); window.Close(); } @@ -162,7 +162,7 @@ namespace Avalonia.Controls.UnitTests window.Show(); window.Close(); - Assert.Empty(Window.OpenWindows); + Assert.Empty(Application.Current.Windows); } } @@ -184,7 +184,7 @@ namespace Avalonia.Controls.UnitTests window.Show(); windowImpl.Object.Closed(); - Assert.Empty(Window.OpenWindows); + Assert.Empty(Application.Current.Windows); } } @@ -339,7 +339,7 @@ namespace Avalonia.Controls.UnitTests { // HACK: We really need a decent way to have "statics" that can be scoped to // AvaloniaLocator scopes. - ((IList)Window.OpenWindows).Clear(); + Application.Current.Windows.Clear(); } } } diff --git a/tests/Avalonia.RenderTests/Media/BitmapTests.cs b/tests/Avalonia.RenderTests/Media/BitmapTests.cs index 089579a0a0..4cee05a0d9 100644 --- a/tests/Avalonia.RenderTests/Media/BitmapTests.cs +++ b/tests/Avalonia.RenderTests/Media/BitmapTests.cs @@ -67,7 +67,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media [Theory] [InlineData(PixelFormat.Rgba8888), InlineData(PixelFormat.Bgra8888), -#if SKIA +#if AVALONIA_SKIA InlineData(PixelFormat.Rgb565) #endif ] diff --git a/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs b/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs index 6381bceadc..0107002274 100644 --- a/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs @@ -281,12 +281,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media await RenderToFile(target); CompareImages(); } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task ImageBrush_NoStretch_NoTile_BottomRightQuarterDest() { Decorator target = new Decorator @@ -309,12 +305,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media await RenderToFile(target); CompareImages(); } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task ImageBrush_NoStretch_NoTile_BottomRightQuarterSource_BottomRightQuarterDest() { Decorator target = new Decorator diff --git a/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs index 062c7d88f5..656e77fc31 100644 --- a/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs @@ -21,12 +21,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media public LinearGradientBrushTests() : base(@"Media\LinearGradientBrush") { } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task LinearGradientBrush_RedBlue_Horizontal_Fill() { Decorator target = new Decorator @@ -52,12 +48,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media await RenderToFile(target); CompareImages(); } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task LinearGradientBrush_RedBlue_Vertical_Fill() { Decorator target = new Decorator diff --git a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs index b3e214f863..0017feb106 100644 --- a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs @@ -21,12 +21,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media public RadialGradientBrushTests() : base(@"Media\RadialGradientBrush") { } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task RadialGradientBrush_RedBlue() { Decorator target = new Decorator diff --git a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs index f69d336271..3a1f7adb2d 100644 --- a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs @@ -270,12 +270,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media await RenderToFile(target); CompareImages(); } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task VisualBrush_NoStretch_NoTile_BottomRightQuarterSource_BottomRightQuarterDest() { Decorator target = new Decorator diff --git a/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj new file mode 100644 index 0000000000..0473355fcd --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj @@ -0,0 +1,22 @@ + + + netcoreapp2.0 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Avalonia.Skia.UnitTests/HitTesting.cs b/tests/Avalonia.Skia.UnitTests/HitTesting.cs new file mode 100644 index 0000000000..544a09e50f --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/HitTesting.cs @@ -0,0 +1,79 @@ +using Avalonia.Controls.Shapes; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Rendering; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Skia.UnitTests +{ + public class HitTesting + { + [Fact] + public void Hit_Test_Should_Respect_Fill() + { + using (AvaloniaLocator.EnterScope()) + { + SkiaPlatform.Initialize(); + + var root = new TestRoot + { + Width = 100, + Height = 100, + Child = new Ellipse + { + Width = 100, + Height = 100, + Fill = Brushes.Red, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }; + + root.Renderer = new DeferredRenderer(root, null); + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + + var outsideResult = root.Renderer.HitTest(new Point(10, 10), root, null); + var insideResult = root.Renderer.HitTest(new Point(50, 50), root, null); + + Assert.Empty(outsideResult); + Assert.Equal(new[] {root.Child}, insideResult); + } + } + + [Fact] + public void Hit_Test_Should_Respect_Stroke() + { + using (AvaloniaLocator.EnterScope()) + { + SkiaPlatform.Initialize(); + + var root = new TestRoot + { + Width = 100, + Height = 100, + Child = new Ellipse + { + Width = 100, + Height = 100, + Stroke = Brushes.Red, + StrokeThickness = 5, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }; + + root.Renderer = new DeferredRenderer(root, null); + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + + var outsideResult = root.Renderer.HitTest(new Point(50, 50), root, null); + var insideResult = root.Renderer.HitTest(new Point(1, 50), root, null); + + Assert.Empty(outsideResult); + Assert.Equal(new[] { root.Child }, insideResult); + } + } + } +} diff --git a/tests/Avalonia.Skia.UnitTests/Properties/AssemblyInfo.cs b/tests/Avalonia.Skia.UnitTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..a462e5b079 --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Reflection; +using Xunit; + +// Don't run tests in parallel. +[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs b/tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs index d5d1d3b01c..7773d4767a 100644 --- a/tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs +++ b/tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs @@ -54,15 +54,16 @@ namespace Avalonia.Styling.UnitTests } [Fact] - public void Should_Complete_When_Activator_Completes() + public void Should_Error_When_Source_Errors() { var activator = new BehaviorSubject(false); var source = new BehaviorSubject(1); var target = new ActivatedObservable(activator, source, string.Empty); + var error = new Exception(); var completed = false; - target.Subscribe(_ => { }, () => completed = true); - activator.OnCompleted(); + target.Subscribe(_ => { }, x => completed = true); + source.OnError(error); Assert.True(completed); } diff --git a/tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs b/tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs index cc42e3f840..03f91d97a1 100644 --- a/tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs +++ b/tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs @@ -17,6 +17,7 @@ namespace Avalonia.Styling.UnitTests var source = new TestSubject(); var target = new ActivatedSubject(activator, source, string.Empty); + target.Subscribe(); target.OnNext("bar"); Assert.Equal(AvaloniaProperty.UnsetValue, source.Value); activator.OnNext(true); @@ -36,6 +37,7 @@ namespace Avalonia.Styling.UnitTests var source = new TestSubject(); var target = new ActivatedSubject(activator, source, string.Empty); + target.Subscribe(); activator.OnCompleted(); Assert.True(source.Completed); @@ -47,10 +49,14 @@ namespace Avalonia.Styling.UnitTests var activator = new BehaviorSubject(false); var source = new TestSubject(); var target = new ActivatedSubject(activator, source, string.Empty); + var targetError = default(Exception); + var error = new Exception(); - activator.OnError(new Exception()); + target.Subscribe(_ => { }, e => targetError = e); + activator.OnError(error); - Assert.NotNull(source.Error); + Assert.Same(error, source.Error); + Assert.Same(error, targetError); } private class TestSubject : ISubject diff --git a/tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs b/tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs index e2cc470a1e..92a7c1bd1f 100644 --- a/tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs +++ b/tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs @@ -40,6 +40,20 @@ namespace Avalonia.Styling.UnitTests Assert.True(completed); } + [Fact] + public void Should_Error_When_Activator_Errors() + { + var activator = new BehaviorSubject(false); + var target = new ActivatedValue(activator, 1, string.Empty); + var error = new Exception(); + var completed = false; + + target.Subscribe(_ => { }, x => completed = true); + activator.OnError(error); + + Assert.True(completed); + } + [Fact] public void Should_Unsubscribe_From_Activator_When_All_Subscriptions_Disposed() { diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs index b41c21fbf4..75599925b7 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; @@ -8,6 +9,7 @@ using Moq; using Avalonia.Controls; using Avalonia.Styling; using Xunit; +using System.Collections.Generic; namespace Avalonia.Styling.UnitTests { @@ -117,6 +119,28 @@ namespace Avalonia.Styling.UnitTests Assert.False(await activator.Take(1)); } + [Fact] + public void Only_Notifies_When_Result_Changes() + { + // Test for #1698 + var control = new Control1 + { + Classes = new Classes { "foo" }, + }; + + var target = default(Selector).Class("foo"); + var activator = target.Match(control).ObservableResult; + var result = new List(); + + using (activator.Subscribe(x => result.Add(x))) + { + control.Classes.Add("bar"); + control.Classes.Remove("foo"); + } + + Assert.Equal(new[] { true, false }, result); + } + public class Control1 : TestControlBase { } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs index e63d23283c..8dca52e6a7 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs @@ -2,60 +2,172 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using Avalonia.Media; -using Avalonia.Platform; -using Moq; +using Avalonia.Visuals.Platform; using Xunit; namespace Avalonia.Visuals.UnitTests.Media { + using System.Globalization; + using System.IO; + public class PathMarkupParserTests { [Fact] public void Parses_Move() { - using (AvaloniaLocator.EnterScope()) - { - var result = new Mock(); - - var parser = PrepareParser(result); - + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) + { parser.Parse("M10 10"); - result.Verify(x => x.BeginFigure(new Point(10, 10), true)); + var figure = pathGeometry.Figures[0]; + + Assert.Equal(new Point(10, 10), figure.StartPoint); } } [Fact] public void Parses_Line() { - using (AvaloniaLocator.EnterScope()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var result = new Mock(); + parser.Parse("M0 0L10 10"); - var parser = PrepareParser(result); + var figure = pathGeometry.Figures[0]; - parser.Parse("M0 0L10 10"); + var segment = figure.Segments[0]; + + Assert.IsType(segment); - result.Verify(x => x.LineTo(new Point(10, 10))); + var lineSegment = (LineSegment)segment; + + Assert.Equal(new Point(10, 10), lineSegment.Point); } } [Fact] public void Parses_Close() { - using (AvaloniaLocator.EnterScope()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var result = new Mock(); + parser.Parse("M0 0L10 10z"); - var parser = PrepareParser(result); + var figure = pathGeometry.Figures[0]; - parser.Parse("M0 0L10 10z"); + Assert.True(figure.IsClosed); + } + } + + [Fact] + public void Parses_FillMode_Before_Move() + { + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) + { + parser.Parse("F 1M0,0"); - result.Verify(x => x.EndFigure(true)); + Assert.Equal(FillRule.NonZero, pathGeometry.FillRule); } } [Theory] + [InlineData("M0 0 10 10 20 20")] + [InlineData("M0,0 10,10 20,20")] + [InlineData("M0,0,10,10,20,20")] + public void Parses_Implicit_Line_Command_After_Move(string pathData) + { + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) + { + parser.Parse(pathData); + + var figure = pathGeometry.Figures[0]; + + var segment = figure.Segments[0]; + + Assert.IsType(segment); + + var lineSegment = (LineSegment)segment; + + Assert.Equal(new Point(10, 10), lineSegment.Point); + + figure = pathGeometry.Figures[1]; + + segment = figure.Segments[0]; + + Assert.IsType(segment); + + lineSegment = (LineSegment)segment; + + Assert.Equal(new Point(20, 20), lineSegment.Point); + } + } + + [Theory] + [InlineData("m0 0 10 10 20 20")] + [InlineData("m0,0 10,10 20,20")] + [InlineData("m0,0,10,10,20,20")] + public void Parses_Implicit_Line_Command_After_Relative_Move(string pathData) + { + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) + { + parser.Parse(pathData); + + var figure = pathGeometry.Figures[0]; + + var segment = figure.Segments[0]; + + Assert.IsType(segment); + + var lineSegment = (LineSegment)segment; + + Assert.Equal(new Point(10, 10), lineSegment.Point); + + segment = figure.Segments[1]; + + Assert.IsType(segment); + + lineSegment = (LineSegment)segment; + + Assert.Equal(new Point(30, 30), lineSegment.Point); + } + } + + [Fact] + public void Parses_Scientific_Notation_Double() + { + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) + { + parser.Parse("M -1.01725E-005 -1.01725e-005"); + + var figure = pathGeometry.Figures[0]; + + Assert.Equal( + new Point( + double.Parse("-1.01725E-005", NumberStyles.Float, CultureInfo.InvariantCulture), + double.Parse("-1.01725E-005", NumberStyles.Float, CultureInfo.InvariantCulture)), + figure.StartPoint); + } + } + + [Theory] + [InlineData("M5.5.5 5.5.5 5.5.5")] + [InlineData("F1M9.0771,11C9.1161,10.701,9.1801,10.352,9.3031,10L9.0001,10 9.0001,6.166 3.0001,9.767 3.0001,10 " + + "9.99999999997669E-05,10 9.99999999997669E-05,0 3.0001,0 3.0001,0.234 9.0001,3.834 9.0001,0 " + + "12.0001,0 12.0001,8.062C12.1861,8.043 12.3821,8.031 12.5941,8.031 15.3481,8.031 15.7961,9.826 " + + "15.9201,11L16.0001,16 9.0001,16 9.0001,12.562 9.0001,11z")] // issue #1708 + [InlineData(" M0 0")] [InlineData("F1 M24,14 A2,2,0,1,1,20,14 A2,2,0,1,1,24,14 z")] // issue #1107 [InlineData("M0 0L10 10z")] [InlineData("M50 50 L100 100 L150 50")] @@ -75,29 +187,32 @@ namespace Avalonia.Visuals.UnitTests.Media ".3809 36.1563C 18.3809 36.1563 18 38 16.3809 36.9063C 15 36 16.3809 34.9063 16.3809 34.9063C 16.3809 34" + ".9063 10.1309 30.9062 16.6309 19.9063 Z ")] [InlineData( - "F1M16,12C16,14.209 14.209,16 12,16 9.791,16 8,14.209 8,12 8,11.817 8.03,11.644 8.054,11.467L6.585,10 4,10 " + - "4,6.414 2.5,7.914 0,5.414 0,3.586 3.586,0 4.414,0 7.414,3 7.586,3 9,1.586 11.914,4.5 10.414,6 " + + "F1M16,12C16,14.209 14.209,16 12,16 9.791,16 8,14.209 8,12 8,11.817 8.03,11.644 8.054,11.467L6.585,10 4,10 " + + "4,6.414 2.5,7.914 0,5.414 0,3.586 3.586,0 4.414,0 7.414,3 7.586,3 9,1.586 11.914,4.5 10.414,6 " + "12.461,8.046C14.45,8.278,16,9.949,16,12")] public void Should_Parse(string pathData) { - using (AvaloniaLocator.EnterScope()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var parser = PrepareParser(); - parser.Parse(pathData); Assert.True(true); } } - private static PathMarkupParser PrepareParser(Mock implMock = null) + [Theory] + [InlineData("0 0")] + [InlineData("j")] + public void Throws_InvalidDataException_On_None_Defined_Command(string pathData) { - AvaloniaLocator.CurrentMutable - .Bind() - .ToConstant(Mock.Of()); - - return new PathMarkupParser( - new StreamGeometryContext(implMock != null ? implMock.Object : Mock.Of())); + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) + { + Assert.Throws(() => parser.Parse(pathData)); + } } } } \ No newline at end of file diff --git a/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Horizontal_Fill.expected.png b/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Horizontal_Fill.expected.png index 744a314256..89668d19f4 100644 Binary files a/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Horizontal_Fill.expected.png and b/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Horizontal_Fill.expected.png differ diff --git a/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Vertical_Fill.expected.png b/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Vertical_Fill.expected.png index 3a3d7df5d4..2b882874dc 100644 Binary files a/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Vertical_Fill.expected.png and b/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Vertical_Fill.expected.png differ diff --git a/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue.expected.png b/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue.expected.png new file mode 100644 index 0000000000..fce116d98d Binary files /dev/null and b/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png new file mode 100644 index 0000000000..3725f2acbc Binary files /dev/null and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png differ diff --git a/tests/TestFiles/Skia/Shapes/Path/Path_With_PenLineCap.expected.png b/tests/TestFiles/Skia/Shapes/Path/Path_With_PenLineCap.expected.png new file mode 100644 index 0000000000..d33068d62c Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/Path_With_PenLineCap.expected.png differ