diff --git a/.gitignore b/.gitignore index c36f64e5de..640725fa26 100644 --- a/.gitignore +++ b/.gitignore @@ -162,7 +162,8 @@ $RECYCLE.BIN/ ################# ## Cake ################# -tools/ +tools/* +!tools/packages.config .nuget artifacts/ nuget diff --git a/Avalonia.sln b/Avalonia.sln index f12af02236..afaee6a907 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -191,6 +191,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.LinuxFramebuffer", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Direct3DInteropSample", "samples\interop\Direct3DInteropSample\Direct3DInteropSample.csproj", "{638580B0-7910-40EF-B674-DCB34DA308CD}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Win32.Interop", "src\Windows\Avalonia.Win32.Interop\Avalonia.Win32.Interop.csproj", "{CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{2f59f3d0-748d-4652-b01e-e0d954756308}*SharedItemsImports = 13 @@ -2589,6 +2591,46 @@ Global {638580B0-7910-40EF-B674-DCB34DA308CD}.Release|Mono.Build.0 = Release|Any CPU {638580B0-7910-40EF-B674-DCB34DA308CD}.Release|x86.ActiveCfg = Release|Any CPU {638580B0-7910-40EF-B674-DCB34DA308CD}.Release|x86.Build.0 = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Ad-Hoc|Mono.ActiveCfg = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Ad-Hoc|Mono.Build.0 = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Ad-Hoc|x86.Build.0 = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.AppStore|Any CPU.ActiveCfg = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.AppStore|Any CPU.Build.0 = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.AppStore|iPhone.ActiveCfg = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.AppStore|iPhone.Build.0 = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.AppStore|Mono.ActiveCfg = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.AppStore|Mono.Build.0 = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.AppStore|x86.ActiveCfg = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.AppStore|x86.Build.0 = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Debug|iPhone.Build.0 = Debug|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Debug|Mono.ActiveCfg = Debug|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Debug|Mono.Build.0 = Debug|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Debug|x86.ActiveCfg = Debug|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Debug|x86.Build.0 = Debug|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Release|Any CPU.Build.0 = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Release|iPhone.ActiveCfg = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Release|iPhone.Build.0 = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Release|Mono.ActiveCfg = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Release|Mono.Build.0 = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Release|x86.ActiveCfg = Release|Any CPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2649,5 +2691,6 @@ Global {4D6FAF79-58B4-482F-9122-0668C346364C} = {74487168-7D91-487E-BF93-055F2251461E} {854568D5-13D1-4B4F-B50D-534DC7EFD3C9} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {638580B0-7910-40EF-B674-DCB34DA308CD} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E} = {B39A8919-9F95-48FE-AD7B-76E08B509888} EndGlobalSection EndGlobal diff --git a/appveyor.yml b/appveyor.yml index a52b2dfa70..8409cac57c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -13,6 +13,8 @@ environment: MYGET_API_KEY: secure: OtVfyN3ErqQrDTnWH2HDfJDlCiu/i4/X4wFmK3ZXXP7HmCiXYPSbTjMPwwdOxRaK MYGET_API_URL: https://www.myget.org/F/avalonia-ci/api/v2/package +init: +- ps: if (Test-Path env:nuget_address) {[System.IO.File]::AppendAllText("C:\Windows\System32\drivers\etc\hosts", "`n$($env:nuget_address)`tapi.nuget.org")} install: - if not exist gtk-sharp-2.12.26.msi appveyor DownloadFile http://download.xamarin.com/GTKforWindows/Windows/gtk-sharp-2.12.26.msi - if not exist dotnet-1.0.1.exe appveyor DownloadFile https://go.microsoft.com/fwlink/?linkid=843448 -FileName "dotnet-1.0.1.exe" diff --git a/build.cake b/build.cake index 021e1e0cc3..801acac324 100644 --- a/build.cake +++ b/build.cake @@ -11,7 +11,7 @@ // TOOLS /////////////////////////////////////////////////////////////////////////////// -#tool "nuget:?package=xunit.runner.console&version=2.1.0" +#tool "nuget:?package=xunit.runner.console&version=2.2.0" #tool "nuget:?package=OpenCover" /////////////////////////////////////////////////////////////////////////////// @@ -98,7 +98,6 @@ Task("Clean") CleanDirectory(parameters.TestsRoot); }); - Task("Restore-NuGet-Packages") .IsDependentOn("Clean") .WithCriteria(parameters.IsRunningOnWindows) @@ -170,23 +169,25 @@ void RunCoreTest(string dir, Parameters parameters, bool net461Only) continue; Information("Running for " + fw); DotNetCoreTest(System.IO.Path.Combine(dir, System.IO.Path.GetFileName(dir)+".csproj"), - new DotNetCoreTestSettings{Framework = fw}); + new DotNetCoreTestSettings { + Configuration = parameters.Configuration, + Framework = fw + }); } } - Task("Run-Net-Core-Unit-Tests") .IsDependentOn("Clean") .Does(() => { RunCoreTest("./tests/Avalonia.Base.UnitTests", parameters, false); - RunCoreTest("./tests/Avalonia.Controls.UnitTests", parameters, true); - RunCoreTest("./tests/Avalonia.Input.UnitTests", parameters, true); - RunCoreTest("./tests/Avalonia.Interactivity.UnitTests", parameters, true); - RunCoreTest("./tests/Avalonia.Layout.UnitTests", parameters, true); - //RunCoreTest("./tests/Avalonia.Markup.UnitTests", parameters, true); - //RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", parameters, true); - RunCoreTest("./tests/Avalonia.Styling.UnitTests", parameters, true); - RunCoreTest("./tests/Avalonia.Visuals.UnitTests", parameters, true); + RunCoreTest("./tests/Avalonia.Controls.UnitTests", parameters, false); + RunCoreTest("./tests/Avalonia.Input.UnitTests", parameters, false); + RunCoreTest("./tests/Avalonia.Interactivity.UnitTests", parameters, false); + RunCoreTest("./tests/Avalonia.Layout.UnitTests", parameters, false); + RunCoreTest("./tests/Avalonia.Markup.UnitTests", parameters, false); + RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", parameters, false); + RunCoreTest("./tests/Avalonia.Styling.UnitTests", parameters, false); + RunCoreTest("./tests/Avalonia.Visuals.UnitTests", parameters, false); }); Task("Run-Unit-Tests") diff --git a/build/Base.props b/build/Base.props new file mode 100644 index 0000000000..6689465338 --- /dev/null +++ b/build/Base.props @@ -0,0 +1,5 @@ + + + + + diff --git a/build/Moq.props b/build/Moq.props index c8544b8309..55242d922e 100644 --- a/build/Moq.props +++ b/build/Moq.props @@ -1,5 +1,5 @@  - + diff --git a/build/SharpDX.props b/build/SharpDX.props index e381bc03e6..0eb910e71e 100644 --- a/build/SharpDX.props +++ b/build/SharpDX.props @@ -3,6 +3,7 @@ + diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index 77407f9996..04e8a3ad4f 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - + diff --git a/build/XUnit.props b/build/XUnit.props index 58df7e8d3c..27e0afc987 100644 --- a/build/XUnit.props +++ b/build/XUnit.props @@ -7,7 +7,9 @@ - - + + + + diff --git a/docs/tutorial/from-wpf.md b/docs/tutorial/from-wpf.md index aa7a9bc13a..2db40cfd86 100644 --- a/docs/tutorial/from-wpf.md +++ b/docs/tutorial/from-wpf.md @@ -33,7 +33,7 @@ placed in a `DataTemplates` collection on each control (and on `Application`): - + diff --git a/packages.cake b/packages.cake index 1e8e356694..07c77f64fd 100644 --- a/packages.cake +++ b/packages.cake @@ -75,22 +75,26 @@ public class Packages var SplatVersion = packageVersions["Splat"].FirstOrDefault().Item1; var SpracheVersion = packageVersions["Sprache"].FirstOrDefault().Item1; var SystemReactiveVersion = packageVersions["System.Reactive"].FirstOrDefault().Item1; + var SystemValueTupleVersion = packageVersions["System.ValueTuple"].FirstOrDefault().Item1; SkiaSharpVersion = packageVersions["SkiaSharp"].FirstOrDefault().Item1; SkiaSharpLinuxVersion = packageVersions["Avalonia.Skia.Linux.Natives"].FirstOrDefault().Item1; var SharpDXVersion = packageVersions["SharpDX"].FirstOrDefault().Item1; var SharpDXDirect2D1Version = packageVersions["SharpDX.Direct2D1"].FirstOrDefault().Item1; var SharpDXDirect3D11Version = packageVersions["SharpDX.Direct3D11"].FirstOrDefault().Item1; + var SharpDXDirect3D9Version = packageVersions["SharpDX.Direct3D9"].FirstOrDefault().Item1; var SharpDXDXGIVersion = packageVersions["SharpDX.DXGI"].FirstOrDefault().Item1; context.Information("Package: Serilog, version: {0}", SerilogVersion); context.Information("Package: Splat, version: {0}", SplatVersion); context.Information("Package: Sprache, version: {0}", SpracheVersion); context.Information("Package: System.Reactive, version: {0}", SystemReactiveVersion); + context.Information("Package: System.ValueTuple, version: {0}", SystemValueTupleVersion); context.Information("Package: SkiaSharp, version: {0}", SkiaSharpVersion); context.Information("Package: Avalonia.Skia.Linux.Natives, version: {0}", SkiaSharpLinuxVersion); context.Information("Package: SharpDX, version: {0}", SharpDXVersion); context.Information("Package: SharpDX.Direct2D1, version: {0}", SharpDXDirect2D1Version); context.Information("Package: SharpDX.Direct3D11, version: {0}", SharpDXDirect3D11Version); + context.Information("Package: SharpDX.Direct3D9, version: {0}", SharpDXDirect3D9Version); context.Information("Package: SharpDX.DXGI, version: {0}", SharpDXDXGIVersion); var nugetPackagesDir = System.Environment.GetEnvironmentVariable("NUGET_HOME") @@ -197,6 +201,7 @@ public class Packages new NuSpecDependency() { Id = "Splat", Version = SplatVersion }, new NuSpecDependency() { Id = "Sprache", Version = SpracheVersion }, new NuSpecDependency() { Id = "System.Reactive", Version = SystemReactiveVersion }, + new NuSpecDependency() { Id = "System.ValueTuple", Version = SystemValueTupleVersion }, //.NET Core new NuSpecDependency() { Id = "System.Threading.ThreadPool", TargetFramework = "netcoreapp1.0", Version = "4.3.0" }, new NuSpecDependency() { Id = "Microsoft.Extensions.DependencyModel", TargetFramework = "netcoreapp1.0", Version = "1.1.0" }, @@ -204,7 +209,8 @@ public class Packages new NuSpecDependency() { Id = "Splat", TargetFramework = "netcoreapp1.0", Version = SplatVersion }, new NuSpecDependency() { Id = "Serilog", TargetFramework = "netcoreapp1.0", Version = SerilogVersion }, new NuSpecDependency() { Id = "Sprache", TargetFramework = "netcoreapp1.0", Version = SpracheVersion }, - new NuSpecDependency() { Id = "System.Reactive", TargetFramework = "netcoreapp1.0", Version = SystemReactiveVersion } + new NuSpecDependency() { Id = "System.Reactive", TargetFramework = "netcoreapp1.0", Version = SystemReactiveVersion }, + new NuSpecDependency() { Id = "System.ValueTuple", TargetFramework = "netcoreapp1.0", Version = SystemValueTupleVersion } }, Files = coreLibrariesNuSpecContent .Concat(win32CoreLibrariesNuSpecContent).Concat(net45RuntimePlatform) @@ -465,6 +471,22 @@ 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 /////////////////////////////////////////////////////////////////////////////// diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml b/samples/ControlCatalog/Pages/MenuPage.xaml index 98171f29d6..9c5591c849 100644 --- a/samples/ControlCatalog/Pages/MenuPage.xaml +++ b/samples/ControlCatalog/Pages/MenuPage.xaml @@ -31,5 +31,28 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/interop/WindowsInteropTest/EmbedToWpfDemo.xaml b/samples/interop/WindowsInteropTest/EmbedToWpfDemo.xaml index 1115cf5768..1d8dc32a69 100644 --- a/samples/interop/WindowsInteropTest/EmbedToWpfDemo.xaml +++ b/samples/interop/WindowsInteropTest/EmbedToWpfDemo.xaml @@ -1,10 +1,12 @@  @@ -14,8 +16,18 @@ + + + + + + + + + + - + diff --git a/samples/interop/WindowsInteropTest/EmbedToWpfDemo.xaml.cs b/samples/interop/WindowsInteropTest/EmbedToWpfDemo.xaml.cs index e60c9ced0a..c7a23c22fc 100644 --- a/samples/interop/WindowsInteropTest/EmbedToWpfDemo.xaml.cs +++ b/samples/interop/WindowsInteropTest/EmbedToWpfDemo.xaml.cs @@ -11,7 +11,9 @@ using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; +using Avalonia; using Avalonia.Controls; +using Avalonia.VisualTree; using ControlCatalog; using Window = System.Windows.Window; @@ -25,7 +27,18 @@ namespace WindowsInteropTest public EmbedToWpfDemo() { InitializeComponent(); - Host.Content = new MainView(); + var view = new MainView(); + view.AttachedToVisualTree += delegate + { + ((TopLevel) view.GetVisualRoot()).AttachDevTools(); + }; + Host.Content = view; + var btn = (Avalonia.Controls.Button) RightBtn.Content; + btn.Click += delegate + { + btn.Content += "!"; + }; + } } } diff --git a/samples/interop/WindowsInteropTest/Program.cs b/samples/interop/WindowsInteropTest/Program.cs index 4770688ecf..fac06d74b0 100644 --- a/samples/interop/WindowsInteropTest/Program.cs +++ b/samples/interop/WindowsInteropTest/Program.cs @@ -15,7 +15,7 @@ namespace WindowsInteropTest { System.Windows.Forms.Application.EnableVisualStyles(); System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false); - AppBuilder.Configure().UseWin32().UseSkia().SetupWithoutStarting(); + AppBuilder.Configure().UseWin32().UseDirect2D1().SetupWithoutStarting(); System.Windows.Forms.Application.Run(new SelectorForm()); } } diff --git a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj index 3891c2bc5c..84bd709f8c 100644 --- a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj +++ b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj @@ -14,7 +14,7 @@ true - AnyCPU + x86 true full false @@ -164,6 +164,10 @@ {3e908f67-5543-4879-a1dc-08eace79b3cd} Avalonia.Direct2D1 + + {cbc4ff2f-92d4-420b-be21-9fe0b930b04e} + Avalonia.Win32.Interop + {811a76cf-1cf6-440f-963b-bbe31bd72a82} Avalonia.Win32 @@ -181,4 +185,5 @@ + \ No newline at end of file diff --git a/scripts/ReplaceNugetCache.ps1 b/scripts/ReplaceNugetCache.ps1 new file mode 100644 index 0000000000..854442eb09 --- /dev/null +++ b/scripts/ReplaceNugetCache.ps1 @@ -0,0 +1,5 @@ +copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp1.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp1.0\ +copy ..\samples\ControlCatalog.NetCore.\bin\Debug\netcoreapp1.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard1.1\ +copy ..\samples\ControlCatalog.NetCore.\bin\Debug\netcoreapp1.1\Avalonia**.dll ~\.nuget\packages\avalonia.gtk3\$args\lib\netstandard1.1\ +copy ..\samples\ControlCatalog.NetCore.\bin\Debug\netcoreapp1.1\Avalonia**.dll ~\.nuget\packages\avalonia.skia.desktop\$args\lib\netstandard1.3\ +copy ..\samples\ControlCatalog.NetCore.\bin\Debug\netcoreapp1.1\Avalonia**.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard1.1\ diff --git a/scripts/ReplaceNugetCache.sh b/scripts/ReplaceNugetCache.sh new file mode 100755 index 0000000000..2ce3e7648d --- /dev/null +++ b/scripts/ReplaceNugetCache.sh @@ -0,0 +1,7 @@ + #!/usr/bin/env bash + + cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp1.1/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netcoreapp1.0/ + cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp1.1/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netstandard1.1/ + cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp1.1/Avalonia**.dll ~/.nuget/packages/avalonia.gtk3/$1/lib/netstandard1.1/ + cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp1.1/Avalonia**.dll ~/.nuget/packages/avalonia.skia.desktop/$1/lib/netstandard1.3/ + diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index c779031e6c..e9b4ad0a6d 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -51,7 +51,6 @@ namespace Avalonia.Android .Bind().ToTransient() .Bind().ToTransient() .Bind().ToSingleton() - .Bind().ToSingleton() .Bind().ToConstant(Instance) .Bind().ToConstant(ImmediateRenderer.Factory) .Bind().ToConstant(new AndroidThreadingInterface()) diff --git a/src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs b/src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs index 9ae79efb48..d52eeb15e4 100644 --- a/src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs +++ b/src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs @@ -4,6 +4,8 @@ namespace Avalonia.Android.Platform.Input { public class AndroidMouseDevice : MouseDevice { + public static AndroidMouseDevice Instance { get; } = new AndroidMouseDevice(); + public AndroidMouseDevice() { diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs index 64dbeb89cc..051a058363 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs @@ -44,7 +44,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public int Width { get; } public int Height { get; } public int RowBytes { get; } - public Size Dpi { get; } = new Size(96, 96); + public Vector Dpi { get; } = new Vector(96, 96); public PixelFormat Format { get; } [DllImport("android")] diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 75772be171..0c62eb9060 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -10,6 +10,7 @@ using Avalonia.Platform; using System; using System.Collections.Generic; using System.Reactive.Disposables; +using Avalonia.Android.Platform.Input; using Avalonia.Controls; using Avalonia.Controls.Platform.Surfaces; @@ -65,6 +66,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform } } + public IMouseDevice MouseDevice => AndroidMouseDevice.Instance; + public Action Closed { get; set; } public Action Input { get; set; } diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs index 702829b91c..0f90472bd0 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs @@ -71,7 +71,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers if (x <= _point.X && r >= _point.X && y <= _point.Y && b >= _point.Y) { var inputRoot = _getInputRoot(); - var mouseDevice = MouseDevice.Instance; + var mouseDevice = Avalonia.Android.Platform.Input.AndroidMouseDevice.Instance; //in order the controls to work in a predictable way //we need to generate mouse move before first mouse down event diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 95be67c98c..cc458545e2 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -30,6 +30,7 @@ Properties\SharedAssemblyInfo.cs + \ No newline at end of file diff --git a/src/Avalonia.Base/Data/BindingNotification.cs b/src/Avalonia.Base/Data/BindingNotification.cs index ecaf59e174..125c29b21b 100644 --- a/src/Avalonia.Base/Data/BindingNotification.cs +++ b/src/Avalonia.Base/Data/BindingNotification.cs @@ -44,11 +44,7 @@ namespace Avalonia.Data public static readonly BindingNotification UnsetValue = new BindingNotification(AvaloniaProperty.UnsetValue); - // Null cannot be held in WeakReference as it's indistinguishable from an expired value so - // use this value in its place. - private static readonly object NullValue = new object(); - - private WeakReference _value; + private object _value; /// /// Initializes a new instance of the class. @@ -56,7 +52,7 @@ namespace Avalonia.Data /// The binding value. public BindingNotification(object value) { - _value = new WeakReference(value ?? NullValue); + _value = value; } /// @@ -73,6 +69,7 @@ namespace Avalonia.Data Error = error; ErrorType = errorType; + _value = AvaloniaProperty.UnsetValue; } /// @@ -84,7 +81,7 @@ namespace Avalonia.Data public BindingNotification(Exception error, BindingErrorType errorType, object fallbackValue) : this(error, errorType) { - _value = new WeakReference(fallbackValue ?? NullValue); + _value = fallbackValue; } /// @@ -95,31 +92,12 @@ namespace Avalonia.Data /// If this property is read when is false then it will return /// . /// - public object Value - { - get - { - if (_value != null) - { - object result; - - if (_value.TryGetTarget(out result)) - { - return result == NullValue ? null : result; - } - } - - // There's the possibility of a race condition in that HasValue can return true, - // and then the value is GC'd before Value is read. We should be ok though as - // we return UnsetValue which should be a safe alternative. - return AvaloniaProperty.UnsetValue; - } - } + public object Value => _value; /// /// Gets a value indicating whether should be pushed to the target. /// - public bool HasValue => _value != null; + public bool HasValue => _value != AvaloniaProperty.UnsetValue; /// /// Gets the error that occurred on the source, if any. @@ -248,7 +226,7 @@ namespace Avalonia.Data /// public void ClearValue() { - _value = null; + _value = AvaloniaProperty.UnsetValue; } /// @@ -256,7 +234,7 @@ namespace Avalonia.Data /// public void SetValue(object value) { - _value = new WeakReference(value ?? NullValue); + _value = value; } /// diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 7ed1c7fd8c..78bbd836be 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -207,7 +207,11 @@ namespace Avalonia.Controls /// The event args. protected virtual void OnClick(RoutedEventArgs e) { - Command?.Execute(CommandParameter); + if (Command != null) + { + Command.Execute(CommandParameter); + e.Handled = true; + } } /// @@ -215,13 +219,16 @@ namespace Avalonia.Controls { base.OnPointerPressed(e); - PseudoClasses.Add(":pressed"); - e.Device.Capture(this); - e.Handled = true; - - if (ClickMode == ClickMode.Press) + if (e.MouseButton == MouseButton.Left) { - RaiseClickEvent(); + PseudoClasses.Add(":pressed"); + e.Device.Capture(this); + e.Handled = true; + + if (ClickMode == ClickMode.Press) + { + RaiseClickEvent(); + } } } @@ -230,13 +237,16 @@ namespace Avalonia.Controls { base.OnPointerReleased(e); - e.Device.Capture(null); - PseudoClasses.Remove(":pressed"); - e.Handled = true; - - if (ClickMode == ClickMode.Release && Classes.Contains(":pointerover")) + if (e.MouseButton == MouseButton.Left) { - RaiseClickEvent(); + e.Device.Capture(null); + PseudoClasses.Remove(":pressed"); + e.Handled = true; + + if (ClickMode == ClickMode.Release && new Rect(Bounds.Size).Contains(e.GetPosition(this))) + { + RaiseClickEvent(); + } } } diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 0a69a5277f..fdb04f4ade 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -19,7 +19,7 @@ namespace Avalonia.Controls { ContextMenuProperty.Changed.Subscribe(ContextMenuChanged); - MenuItem.ClickEvent.AddClassHandler(x => x.OnContextMenuClick); + MenuItem.ClickEvent.AddClassHandler(x => x.OnContextMenuClick, handledEventsToo: true); } /// diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 758f7bbf55..83a76cb1a7 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -118,6 +118,7 @@ namespace Avalonia.Controls public Control() { _nameScope = this as INameScope; + _isAttachedToLogicalTree = this is IStyleRoot; } /// @@ -369,6 +370,12 @@ namespace Avalonia.Controls } } + /// + void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + this.OnAttachedToLogicalTreeCore(e); + } + /// void ILogical.NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { @@ -418,7 +425,7 @@ namespace Avalonia.Controls if (_isAttachedToLogicalTree) { - var oldRoot = FindStyleRoot(old); + var oldRoot = FindStyleRoot(old) ?? this as IStyleRoot; if (oldRoot == null) { @@ -436,7 +443,7 @@ namespace Avalonia.Controls _parent = (IControl)parent; - if (_parent is IStyleRoot || _parent?.IsAttachedToLogicalTree == true) + if (_parent is IStyleRoot || _parent?.IsAttachedToLogicalTree == true || this is IStyleRoot) { var newRoot = FindStyleRoot(this); diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs index b8d54fa67b..179dccaf76 100644 --- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs +++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs @@ -22,6 +22,8 @@ namespace Avalonia.Controls.Embedding [CanBeNull] public new IEmbeddableWindowImpl PlatformImpl => (IEmbeddableWindowImpl) base.PlatformImpl; + protected bool EnforceClientSize { get; set; } = true; + public void Prepare() { EnsureInitialized(); @@ -38,12 +40,12 @@ namespace Avalonia.Controls.Embedding init.EndInit(); } } - + protected override Size MeasureOverride(Size availableSize) { - var cs = PlatformImpl?.ClientSize ?? default(Size); - base.MeasureOverride(cs); - return cs; + if (EnforceClientSize) + availableSize = PlatformImpl?.ClientSize ?? default(Size); + return base.MeasureOverride(availableSize); } private readonly NameScope _nameScope = new NameScope(); diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index e919275d4f..994af9dab8 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -47,7 +47,7 @@ namespace Avalonia.Controls static Menu() { ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel); - MenuItem.ClickEvent.AddClassHandler(x => x.OnMenuClick); + MenuItem.ClickEvent.AddClassHandler(x => x.OnMenuClick, handledEventsToo: true); MenuItem.SubmenuOpenedEvent.AddClassHandler(x => x.OnSubmenuOpened); } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 3d15ed99e7..3d66fbc51b 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -102,6 +102,11 @@ namespace Avalonia.Controls AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler(x => x.AccessKeyPressed); } + public MenuItem() + { + + } + /// /// Occurs when a without a submenu is clicked. /// @@ -192,7 +197,11 @@ namespace Avalonia.Controls /// The click event args. protected virtual void OnClick(RoutedEventArgs e) { - Command?.Execute(CommandParameter); + if (Command != null) + { + Command.Execute(CommandParameter); + e.Handled = true; + } } /// diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 16f436fd45..ba3ecb99c5 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using Avalonia.Input; using Avalonia.Input.Raw; +using JetBrains.Annotations; namespace Avalonia.Platform { @@ -93,5 +94,11 @@ namespace Avalonia.Platform /// Gets or sets a method called when the underlying implementation is destroyed. /// Action Closed { get; set; } + + /// + /// Gets a mouse device associated with toplevel + /// + [CanBeNull] + IMouseDevice MouseDevice { get; } } } diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 40fc2f302c..c1adff402a 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Templates; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Media; +using Avalonia.VisualTree; namespace Avalonia.Controls.Presenters { @@ -88,6 +89,7 @@ namespace Avalonia.Controls.Presenters static ContentPresenter() { ContentProperty.Changed.AddClassHandler(x => x.ContentChanged); + ContentTemplateProperty.Changed.AddClassHandler(x => x.ContentChanged); TemplatedParentProperty.Changed.AddClassHandler(x => x.TemplatedParentChanged); } @@ -313,27 +315,22 @@ namespace Avalonia.Controls.Presenters if (content != null && newChild == null) { - // We have content and it isn't a control, so first try to recycle the existing - // child control to display the new data by querying if the template that created - // the child can recycle items and that it also matches the new data. - if (oldChild != null && - _dataTemplate != null && - _dataTemplate.SupportsRecycling && - _dataTemplate.Match(content)) + var dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default; + + // We have content and it isn't a control, so if the new data template is the same + // as the old data template, try to recycle the existing child control to display + // the new data. + if (dataTemplate == _dataTemplate && dataTemplate.SupportsRecycling) { newChild = oldChild; } else { - // We couldn't recycle an existing control so find a data template for the data - // and use it to create a control. - _dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default; + _dataTemplate = dataTemplate; newChild = _dataTemplate.Build(content); - // Try to give the new control its own name scope. - var controlResult = newChild as Control; - - if (controlResult != null) + // Give the new control its own name scope. + if (newChild is Control controlResult) { NameScope.SetNameScope(controlResult, new NameScope()); } @@ -424,6 +421,19 @@ namespace Avalonia.Controls.Presenters private void ContentChanged(AvaloniaPropertyChangedEventArgs e) { _createdChild = false; + + if (((ILogical)this).IsAttachedToLogicalTree) + { + UpdateChild(); + } + else if (Child != null) + { + VisualChildren.Remove(Child); + LogicalChildren.Remove(Child); + Child = null; + _dataTemplate = null; + } + InvalidateMeasure(); } diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index bb2a61c024..daea187a69 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -340,11 +340,11 @@ namespace Avalonia.Controls.Primitives switch (mode) { case PlacementMode.Pointer: - if (MouseDevice.Instance != null) + if(PopupRoot != null) { // Scales the Horizontal and Vertical offset to screen co-ordinates. var screenOffset = new Point(HorizontalOffset * (PopupRoot as ILayoutRoot).LayoutScaling, VerticalOffset * (PopupRoot as ILayoutRoot).LayoutScaling); - return MouseDevice.Instance.Position + screenOffset; + return (((IInputRoot)PopupRoot)?.MouseDevice?.Position ?? default(Point)) + screenOffset; } return default(Point); diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 7a42c48053..1ddfb97c14 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -285,6 +285,17 @@ namespace Avalonia.Controls.Primitives return this; } + /// + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + if (VisualChildren.Count > 0) + { + ((ILogical)VisualChildren[0]).NotifyAttachedToLogicalTree(e); + } + + base.OnAttachedToLogicalTree(e); + } + /// protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index d2e8085d8c..92ab12f82e 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -236,6 +236,11 @@ namespace Avalonia.Controls { _presenter = e.NameScope.Get("PART_TextPresenter"); _presenter.Cursor = new Cursor(StandardCursorType.Ibeam); + + if(IsFocused) + { + _presenter.ShowCaret(); + } } protected override void OnGotFocus(GotFocusEventArgs e) @@ -254,7 +259,7 @@ namespace Avalonia.Controls } else { - _presenter.ShowCaret(); + _presenter?.ShowCaret(); } } @@ -263,7 +268,7 @@ namespace Avalonia.Controls base.OnLostFocus(e); SelectionStart = 0; SelectionEnd = 0; - _presenter.HideCaret(); + _presenter?.HideCaret(); } protected override void OnTextInput(TextInputEventArgs e) diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index cff3dab150..e1b69637af 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -105,21 +105,29 @@ namespace Avalonia.Controls { if (control != null && control.IsVisible && control.GetVisualRoot() != null) { - if (s_popup != null) - { - throw new AvaloniaInternalException("Previous ToolTip not disposed."); - } + var cp = (control.GetVisualRoot() as IInputRoot)?.MouseDevice?.GetPosition(control); - var cp = MouseDevice.Instance?.GetPosition(control); - var position = control.PointToScreen(cp ?? new Point(0, 0)) + new Vector(0, 22); + if (cp.HasValue && control.IsVisible && new Rect(control.Bounds.Size).Contains(cp.Value)) + { + var position = control.PointToScreen(cp.Value) + new Vector(0, 22); + + if (s_popup == null) + { + s_popup = new PopupRoot(); + s_popup.Content = new ToolTip(); + } + else + { + ((ISetLogicalParent)s_popup).SetParent(null); + } - s_popup = new PopupRoot(); ((ISetLogicalParent)s_popup).SetParent(control); - s_popup.Content = new ToolTip { Content = GetTip(control) }; - s_popup.Position = position; - s_popup.Show(); + ((ToolTip)s_popup.Content).Content = GetTip(control); + s_popup.Position = position; + s_popup.Show(); - s_current = control; + s_current = control; + } } } @@ -147,16 +155,23 @@ namespace Avalonia.Controls { if (s_popup != null) { - // Clear the ToolTip's Content in case it has control content: this will - // reset its visual parent allowing it to be used again. - ((ToolTip)s_popup.Content).Content = null; - - // Dispose of the popup. - s_popup.Dispose(); - s_popup = null; + DisposeTooltip(); + s_show.OnNext(null); } + } + } + + private static void DisposeTooltip() + { + if (s_popup != null) + { + // Clear the ToolTip's Content in case it has control content: this will + // reset its visual parent allowing it to be used again. + ((ToolTip)s_popup.Content).Content = null; - s_show.OnNext(null); + // Dispose of the popup. + s_popup.Dispose(); + s_popup = null; } } } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 1f1a29afa3..a0a8f6b27e 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -163,6 +163,9 @@ namespace Avalonia.Controls set { SetValue(PointerOverElementProperty, value); } } + /// + IMouseDevice IInputRoot.MouseDevice => PlatformImpl?.MouseDevice; + /// /// Gets or sets a value indicating whether access keys are shown in the window. /// diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index b966d09b1f..5d1b9a1462 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -16,7 +16,7 @@ namespace Avalonia.Controls /// /// Displays a hierachical tree of data. /// - public class TreeView : ItemsControl + public class TreeView : ItemsControl, ICustomKeyboardNavigation { /// /// Defines the property. @@ -90,6 +90,26 @@ namespace Avalonia.Controls } } + (bool handled, IInputElement next) ICustomKeyboardNavigation.GetNext(IInputElement element, NavigationDirection direction) + { + if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous) + { + if (!this.IsVisualAncestorOf(element)) + { + IControl result = _selectedItem != null ? + ItemContainerGenerator.Index.ContainerFromItem(_selectedItem) : + ItemContainerGenerator.ContainerFromIndex(0); + return (true, result); + } + else + { + return (true, null); + } + } + + return (false, null); + } + /// protected override IItemContainerGenerator CreateItemContainerGenerator() { diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 1f484fd6cb..fbdf64b14a 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -29,6 +29,7 @@ namespace Avalonia.Controls public static readonly DirectProperty IsActiveProperty = AvaloniaProperty.RegisterDirect(nameof(IsActive), o => o.IsActive); + private bool _hasExecutedInitialLayoutPass; private bool _isActive; private bool _ignoreVisibilityChange; @@ -136,7 +137,13 @@ namespace Avalonia.Controls { EnsureInitialized(); IsVisible = true; - LayoutManager.Instance.ExecuteInitialLayoutPass(this); + + if (!_hasExecutedInitialLayoutPass) + { + LayoutManager.Instance.ExecuteInitialLayoutPass(this); + _hasExecutedInitialLayoutPass = true; + } + PlatformImpl?.Show(); } finally diff --git a/src/Avalonia.Diagnostics/DevTools.xaml.cs b/src/Avalonia.Diagnostics/DevTools.xaml.cs index 10337d7386..6593a8cd42 100644 --- a/src/Avalonia.Diagnostics/DevTools.xaml.cs +++ b/src/Avalonia.Diagnostics/DevTools.xaml.cs @@ -14,11 +14,11 @@ using Avalonia.VisualTree; namespace Avalonia { - public static class WindowExtensions + public static class DevToolsExtensions { - public static void AttachDevTools(this Window window) + public static void AttachDevTools(this TopLevel control) { - Avalonia.Diagnostics.DevTools.Attach(window); + Avalonia.Diagnostics.DevTools.Attach(control); } } } @@ -27,7 +27,7 @@ namespace Avalonia.Diagnostics { public class DevTools : UserControl { - private static Dictionary s_open = new Dictionary(); + private static Dictionary s_open = new Dictionary(); private IDisposable _keySubscription; public DevTools(IControl root) @@ -43,9 +43,9 @@ namespace Avalonia.Diagnostics public IControl Root { get; } - public static IDisposable Attach(Window window) + public static IDisposable Attach(TopLevel control) { - return window.AddHandler( + return control.AddHandler( KeyDownEvent, WindowPreviewKeyDown, RoutingStrategies.Tunnel); @@ -55,16 +55,16 @@ namespace Avalonia.Diagnostics { if (e.Key == Key.F12) { - var window = (Window)sender; + var control = (TopLevel)sender; var devToolsWindow = default(Window); - if (s_open.TryGetValue(window, out devToolsWindow)) + if (s_open.TryGetValue(control, out devToolsWindow)) { devToolsWindow.Activate(); } else { - var devTools = new DevTools(window); + var devTools = new DevTools(control); devToolsWindow = new Window { @@ -78,7 +78,7 @@ namespace Avalonia.Diagnostics }; devToolsWindow.Closed += devTools.DevToolsClosed; - s_open.Add((Window)sender, devToolsWindow); + s_open.Add(control, devToolsWindow); devToolsWindow.Show(); } } @@ -88,9 +88,7 @@ namespace Avalonia.Diagnostics { var devToolsWindow = (Window)sender; var devTools = (DevTools)devToolsWindow.Content; - var window = (Window)devTools.Root; - - s_open.Remove(window); + s_open.Remove((TopLevel)devTools.Root); _keySubscription.Dispose(); devToolsWindow.Closed -= DevToolsClosed; } @@ -106,7 +104,8 @@ namespace Avalonia.Diagnostics if ((e.Modifiers) == modifiers) { - var point = MouseDevice.Instance.GetPosition(Root); + + var point = (Root.VisualRoot as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default(Point); var control = Root.GetVisualsAt(point, x => (!(x is AdornerLayer) && x.IsVisible)) .FirstOrDefault(); diff --git a/src/Avalonia.HtmlRenderer/Adapters/ControlAdapter.cs b/src/Avalonia.HtmlRenderer/Adapters/ControlAdapter.cs index 523da3508e..9a55768f6a 100644 --- a/src/Avalonia.HtmlRenderer/Adapters/ControlAdapter.cs +++ b/src/Avalonia.HtmlRenderer/Adapters/ControlAdapter.cs @@ -10,9 +10,11 @@ // - Sun Tsu, // "The Art of War" +using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Html; using Avalonia.Input; +using Avalonia.VisualTree; using TheArtOfDev.HtmlRenderer.Adapters; using TheArtOfDev.HtmlRenderer.Adapters.Entities; using TheArtOfDev.HtmlRenderer.Core.Utils; @@ -54,7 +56,8 @@ namespace TheArtOfDev.HtmlRenderer.Avalonia.Adapters { get { - return Util.Convert(MouseDevice.Instance.GetPosition(_control)); + var pos = (_control.GetVisualRoot() as IInputRoot)?.MouseDevice?.Position ?? default(Point); + return Util.Convert(pos); } } diff --git a/src/Avalonia.HtmlRenderer/HtmlControl.cs b/src/Avalonia.HtmlRenderer/HtmlControl.cs index 88a6e5fda4..0051f6427b 100644 --- a/src/Avalonia.HtmlRenderer/HtmlControl.cs +++ b/src/Avalonia.HtmlRenderer/HtmlControl.cs @@ -17,6 +17,7 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Threading; +using Avalonia.VisualTree; using TheArtOfDev.HtmlRenderer.Core; using TheArtOfDev.HtmlRenderer.Core.Entities; using TheArtOfDev.HtmlRenderer.Avalonia; @@ -512,7 +513,7 @@ namespace Avalonia.Controls.Html protected virtual void InvokeMouseMove() { - _htmlContainer.HandleMouseMove(this, MouseDevice.Instance?.GetPosition(this) ?? default(Point)); + _htmlContainer.HandleMouseMove(this, (this.GetVisualRoot() as IInputRoot)?.MouseDevice?.GetPosition(this) ?? default(Point)); } /// diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs index e5cc5a8557..102da6efc4 100644 --- a/src/Avalonia.Input/FocusManager.cs +++ b/src/Avalonia.Input/FocusManager.cs @@ -176,9 +176,10 @@ namespace Avalonia.Input /// The event args. private void OnPreviewPointerPressed(object sender, RoutedEventArgs e) { - if (sender == e.Source) + var ev = (PointerPressedEventArgs)e; + + if (sender == e.Source && ev.MouseButton == MouseButton.Left) { - var ev = (PointerPressedEventArgs)e; var element = (ev.Device?.Captured as IInputElement) ?? (e.Source as IInputElement); if (element == null || !CanFocus(element)) diff --git a/src/Avalonia.Input/ICustomKeyboardNavigation.cs b/src/Avalonia.Input/ICustomKeyboardNavigation.cs new file mode 100644 index 0000000000..de5f98e04b --- /dev/null +++ b/src/Avalonia.Input/ICustomKeyboardNavigation.cs @@ -0,0 +1,15 @@ +// 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; + +namespace Avalonia.Input +{ + /// + /// Designates a control as handling its own keyboard navigation. + /// + public interface ICustomKeyboardNavigation + { + (bool handled, IInputElement next) GetNext(IInputElement element, NavigationDirection direction); + } +} diff --git a/src/Avalonia.Input/IInputDevice.cs b/src/Avalonia.Input/IInputDevice.cs index 916f29376a..72fa2ab9bf 100644 --- a/src/Avalonia.Input/IInputDevice.cs +++ b/src/Avalonia.Input/IInputDevice.cs @@ -1,9 +1,16 @@ // 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.Input.Raw; + namespace Avalonia.Input { public interface IInputDevice { + /// + /// Processes raw event. Is called after preprocessing by InputManager + /// + /// + void ProcessRawEvent(RawInputEventArgs ev); } } diff --git a/src/Avalonia.Input/IInputRoot.cs b/src/Avalonia.Input/IInputRoot.cs index 6c01bb9c0e..6b3e1e6bc5 100644 --- a/src/Avalonia.Input/IInputRoot.cs +++ b/src/Avalonia.Input/IInputRoot.cs @@ -1,6 +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 JetBrains.Annotations; + namespace Avalonia.Input { /// @@ -27,5 +29,11 @@ namespace Avalonia.Input /// Gets or sets a value indicating whether access keys are shown in the window. /// bool ShowAccessKeys { get; set; } + + /// + /// Gets associated mouse device + /// + [CanBeNull] + IMouseDevice MouseDevice { get; } } } diff --git a/src/Avalonia.Input/InputManager.cs b/src/Avalonia.Input/InputManager.cs index 7aa609e65c..9e2d5ffea3 100644 --- a/src/Avalonia.Input/InputManager.cs +++ b/src/Avalonia.Input/InputManager.cs @@ -35,6 +35,7 @@ namespace Avalonia.Input public void ProcessInput(RawInputEventArgs e) { _preProcess.OnNext(e); + e.Device?.ProcessRawEvent(e); _process.OnNext(e); _postProcess.OnNext(e); } diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index 51af01f69b..d815f8082b 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -16,14 +16,6 @@ namespace Avalonia.Input { private IInputElement _focusedElement; - public KeyboardDevice() - { - InputManager.Process - .OfType() - .Where(e => e.Device == this && !e.Handled) - .Subscribe(ProcessRawEvent); - } - public event PropertyChangedEventHandler PropertyChanged; public static IKeyboardDevice Instance => AvaloniaLocator.Current.GetService(); @@ -77,8 +69,10 @@ namespace Avalonia.Input PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } - private void ProcessRawEvent(RawInputEventArgs e) + public void ProcessRawEvent(RawInputEventArgs e) { + if(e.Handled) + return; IInputElement element = FocusedElement; if (element != null) diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs index 57da49fa03..bf2b61d08b 100644 --- a/src/Avalonia.Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs @@ -2,7 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Linq; using Avalonia.Input.Navigation; +using Avalonia.VisualTree; namespace Avalonia.Input { @@ -52,6 +54,31 @@ namespace Avalonia.Input { Contract.Requires(element != null); + var customHandler = element.GetSelfAndVisualAncestors() + .OfType() + .FirstOrDefault(); + + if (customHandler != null) + { + var (handled, next) = customHandler.GetNext(element, direction); + + if (handled) + { + if (next != null) + { + return next; + } + else if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous) + { + return TabNavigation.GetNextInTabOrder((IInputElement)customHandler, direction, true); + } + else + { + return null; + } + } + } + if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous) { return TabNavigation.GetNextInTabOrder(element, direction); diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index cbcb4382ec..875a5ebaee 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -20,23 +20,7 @@ namespace Avalonia.Input private int _clickCount; private Rect _lastClickRect; private uint _lastClickTime; - - /// - /// Intializes a new instance of . - /// - public MouseDevice() - { - InputManager.Process - .OfType() - .Where(e => e.Device == this && !e.Handled) - .Subscribe(ProcessRawEvent); - } - - /// - /// Gets the current mouse device instance. - /// - public static IMouseDevice Instance => AvaloniaLocator.Current.GetService(); - + /// /// Gets the control that is currently capturing by the mouse, if any. /// @@ -50,12 +34,7 @@ namespace Avalonia.Input get; protected set; } - - /// - /// Gets the application's input manager. - /// - public IInputManager InputManager => AvaloniaLocator.Current.GetService(); - + /// /// Gets the mouse position, in screen coordinates. /// @@ -102,6 +81,12 @@ namespace Avalonia.Input return root.PointToClient(Position) - p; } + public void ProcessRawEvent(RawInputEventArgs e) + { + if (!e.Handled && e is RawMouseEventArgs margs) + ProcessRawEvent(margs); + } + private void ProcessRawEvent(RawMouseEventArgs e) { Contract.Requires(e != null); diff --git a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs b/src/Avalonia.Input/Navigation/DirectionalNavigation.cs index a88ed1e8aa..75cb3a39e8 100644 --- a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs +++ b/src/Avalonia.Input/Navigation/DirectionalNavigation.cs @@ -41,7 +41,7 @@ namespace Avalonia.Input.Navigation { case KeyboardNavigationMode.Continue: return GetNextInContainer(element, container, direction) ?? - GetFirstInNextContainer(element, direction); + GetFirstInNextContainer(element, element, direction); case KeyboardNavigationMode.Cycle: return GetNextInContainer(element, container, direction) ?? GetFocusableDescendant(container, direction); @@ -173,10 +173,12 @@ namespace Avalonia.Input.Navigation /// /// Gets the first item that should be focused in the next container. /// + /// The element being navigated away from. /// The container. /// The direction of the search. /// The first element, or null if there are no more elements. private static IInputElement GetFirstInNextContainer( + IInputElement element, IInputElement container, NavigationDirection direction) { @@ -200,6 +202,16 @@ namespace Avalonia.Input.Navigation if (sibling != null) { + if (sibling is ICustomKeyboardNavigation custom) + { + var (handled, customNext) = custom.GetNext(element, direction); + + if (handled) + { + return customNext; + } + } + if (sibling.CanFocus()) { next = sibling; @@ -214,7 +226,7 @@ namespace Avalonia.Input.Navigation if (next == null) { - next = GetFirstInNextContainer(parent, direction); + next = GetFirstInNextContainer(element, parent, direction); } } else diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index 6ba7ab1a0c..6e077e887f 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -18,13 +18,17 @@ namespace Avalonia.Input.Navigation /// /// The element. /// The tab direction. Must be Next or Previous. + /// + /// If true will not descend into to find next control. + /// /// /// The next element in the specified direction, or null if /// was the last in the requested direction. /// public static IInputElement GetNextInTabOrder( IInputElement element, - NavigationDirection direction) + NavigationDirection direction, + bool outsideElement = false) { Contract.Requires(element != null); Contract.Requires( @@ -40,20 +44,20 @@ namespace Avalonia.Input.Navigation switch (mode) { case KeyboardNavigationMode.Continue: - return GetNextInContainer(element, container, direction) ?? - GetFirstInNextContainer(element, direction); + return GetNextInContainer(element, container, direction, outsideElement) ?? + GetFirstInNextContainer(element, element, direction); case KeyboardNavigationMode.Cycle: - return GetNextInContainer(element, container, direction) ?? + return GetNextInContainer(element, container, direction, outsideElement) ?? GetFocusableDescendant(container, direction); case KeyboardNavigationMode.Contained: - return GetNextInContainer(element, container, direction); + return GetNextInContainer(element, container, direction, outsideElement); default: - return GetFirstInNextContainer(container, direction); + return GetFirstInNextContainer(element, container, direction); } } else { - return GetFocusableDescendants(element).FirstOrDefault(); + return GetFocusableDescendants(element, direction).FirstOrDefault(); } } @@ -66,16 +70,17 @@ namespace Avalonia.Input.Navigation private static IInputElement GetFocusableDescendant(IInputElement container, NavigationDirection direction) { return direction == NavigationDirection.Next ? - GetFocusableDescendants(container).FirstOrDefault() : - GetFocusableDescendants(container).LastOrDefault(); + GetFocusableDescendants(container, direction).FirstOrDefault() : + GetFocusableDescendants(container, direction).LastOrDefault(); } /// /// Gets the focusable descendants of the specified element. /// /// The element. + /// The tab direction. Must be Next or Previous. /// The element's focusable descendants. - private static IEnumerable GetFocusableDescendants(IInputElement element) + private static IEnumerable GetFocusableDescendants(IInputElement element, NavigationDirection direction) { var mode = KeyboardNavigation.GetTabNavigation((InputElement)element); @@ -103,16 +108,25 @@ namespace Avalonia.Input.Navigation foreach (var child in children) { - if (child.CanFocus()) + var customNext = GetCustomNext(child, direction); + + if (customNext.handled) { - yield return child; + yield return customNext.next; } - - if (child.CanFocusDescendants()) + else { - foreach (var descendant in GetFocusableDescendants(child)) + if (child.CanFocus()) { - yield return descendant; + yield return child; + } + + if (child.CanFocusDescendants()) + { + foreach (var descendant in GetFocusableDescendants(child, direction)) + { + yield return descendant; + } } } } @@ -124,15 +138,19 @@ namespace Avalonia.Input.Navigation /// The starting element/ /// The container. /// The direction. + /// + /// If true will not descend into to find next control. + /// /// The next element, or null if the element is the last. private static IInputElement GetNextInContainer( IInputElement element, IInputElement container, - NavigationDirection direction) + NavigationDirection direction, + bool outsideElement) { - if (direction == NavigationDirection.Next) + if (direction == NavigationDirection.Next && !outsideElement) { - var descendant = GetFocusableDescendants(element).FirstOrDefault(); + var descendant = GetFocusableDescendants(element, direction).FirstOrDefault(); if (descendant != null) { @@ -167,7 +185,7 @@ namespace Avalonia.Input.Navigation if (element != null && direction == NavigationDirection.Previous) { - var descendant = GetFocusableDescendants(element).LastOrDefault(); + var descendant = GetFocusableDescendants(element, direction).LastOrDefault(); if (descendant != null) { @@ -184,10 +202,12 @@ namespace Avalonia.Input.Navigation /// /// Gets the first item that should be focused in the next container. /// + /// The element being navigated away from. /// The container. /// The direction of the search. /// The first element, or null if there are no more elements. private static IInputElement GetFirstInNextContainer( + IInputElement element, IInputElement container, NavigationDirection direction) { @@ -210,6 +230,13 @@ namespace Avalonia.Input.Navigation if (sibling != null) { + var customNext = GetCustomNext(sibling, direction); + + if (customNext.handled) + { + return customNext.next; + } + if (sibling.CanFocus()) { next = sibling; @@ -217,24 +244,34 @@ namespace Avalonia.Input.Navigation else { next = direction == NavigationDirection.Next ? - GetFocusableDescendants(sibling).FirstOrDefault() : - GetFocusableDescendants(sibling).LastOrDefault(); + GetFocusableDescendants(sibling, direction).FirstOrDefault() : + GetFocusableDescendants(sibling, direction).LastOrDefault(); } } if (next == null) { - next = GetFirstInNextContainer(parent, direction); + next = GetFirstInNextContainer(element, parent, direction); } } else { next = direction == NavigationDirection.Next ? - GetFocusableDescendants(container).FirstOrDefault() : - GetFocusableDescendants(container).LastOrDefault(); + GetFocusableDescendants(container, direction).FirstOrDefault() : + GetFocusableDescendants(container, direction).LastOrDefault(); } return next; } + + private static (bool handled, IInputElement next) GetCustomNext(IInputElement element, NavigationDirection direction) + { + if (element is ICustomKeyboardNavigation custom) + { + return custom.GetNext(element, direction); + } + + return (false, null); + } } } diff --git a/src/Avalonia.Layout/IEmbeddedLayoutRoot.cs b/src/Avalonia.Layout/IEmbeddedLayoutRoot.cs new file mode 100644 index 0000000000..24f0ccd82e --- /dev/null +++ b/src/Avalonia.Layout/IEmbeddedLayoutRoot.cs @@ -0,0 +1,10 @@ +namespace Avalonia.Layout +{ + /// + /// A special layout root with enforced size for Arrange pass + /// + public interface IEmbeddedLayoutRoot : ILayoutRoot + { + Size AllocatedSize { get; } + } +} \ No newline at end of file diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index b7b83bf852..3244f5e7dc 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -14,8 +14,8 @@ namespace Avalonia.Layout /// public class LayoutManager : ILayoutManager { - private readonly HashSet _toMeasure = new HashSet(); - private readonly HashSet _toArrange = new HashSet(); + private readonly Queue _toMeasure = new Queue(); + private readonly Queue _toArrange = new Queue(); private bool _queued; private bool _running; @@ -30,8 +30,18 @@ namespace Avalonia.Layout Contract.Requires(control != null); Dispatcher.UIThread.VerifyAccess(); - _toMeasure.Add(control); - _toArrange.Add(control); + if (!control.IsAttachedToVisualTree) + { +#if DEBUG + throw new AvaloniaInternalException( + "LayoutManager.InvalidateMeasure called on a control that is detached from the visual tree."); +#else + return; +#endif + } + + _toMeasure.Enqueue(control); + _toArrange.Enqueue(control); QueueLayoutPass(); } @@ -41,7 +51,17 @@ namespace Avalonia.Layout Contract.Requires(control != null); Dispatcher.UIThread.VerifyAccess(); - _toArrange.Add(control); + if (!control.IsAttachedToVisualTree) + { +#if DEBUG + throw new AvaloniaInternalException( + "LayoutManager.InvalidateArrange called on a control that is detached from the visual tree."); +#else + return; +#endif + } + + _toArrange.Enqueue(control); QueueLayoutPass(); } @@ -108,8 +128,12 @@ namespace Avalonia.Layout { while (_toMeasure.Count > 0) { - var next = _toMeasure.First(); - Measure(next); + var control = _toMeasure.Dequeue(); + + if (!control.IsMeasureValid && control.IsAttachedToVisualTree) + { + Measure(control); + } } } @@ -117,53 +141,62 @@ namespace Avalonia.Layout { while (_toArrange.Count > 0 && _toMeasure.Count == 0) { - var next = _toArrange.First(); - Arrange(next); + var control = _toArrange.Dequeue(); + + if (!control.IsArrangeValid && control.IsAttachedToVisualTree) + { + Arrange(control); + } } } private void Measure(ILayoutable control) { - var root = control as ILayoutRoot; - var parent = control.VisualParent as ILayoutable; - - if (root != null) - { - root.Measure(root.MaxClientSize); - } - else if (parent != null) + // Controls closest to the visual root need to be arranged first. We don't try to store + // ordered invalidation lists, instead we traverse the tree upwards, measuring the + // controls closest to the root first. This has been shown by benchmarks to be the + // fastest and most memory-efficent algorithm. + if (control.VisualParent is ILayoutable parent) { Measure(parent); } - if (!control.IsMeasureValid) + // If the control being measured has IsMeasureValid == true here then its measure was + // handed by an ancestor and can be ignored. The measure may have also caused the + // control to be removed. + if (!control.IsMeasureValid && control.IsAttachedToVisualTree) { - control.Measure(control.PreviousMeasure.Value); + if (control is ILayoutRoot root) + { + root.Measure(Size.Infinity); + } + else + { + control.Measure(control.PreviousMeasure.Value); + } } - - _toMeasure.Remove(control); } private void Arrange(ILayoutable control) { - var root = control as ILayoutRoot; - var parent = control.VisualParent as ILayoutable; - - if (root != null) - { - root.Arrange(new Rect(root.DesiredSize)); - } - else if (parent != null) + if (control.VisualParent is ILayoutable parent) { Arrange(parent); } - if (control.PreviousArrange.HasValue) + if (!control.IsArrangeValid && control.IsAttachedToVisualTree) { - control.Arrange(control.PreviousArrange.Value); + if (control is IEmbeddedLayoutRoot embeddedRoot) + control.Arrange(new Rect(embeddedRoot.AllocatedSize)); + else if (control is ILayoutRoot root) + control.Arrange(new Rect(root.DesiredSize)); + else if (control.PreviousArrange != null) + { + // Has been observed that PreviousArrange sometimes is null, probably a bug somewhere else. + // Condition observed: control.VisualParent is Scrollbar, control is Border. + control.Arrange(control.PreviousArrange.Value); + } } - - _toArrange.Remove(control); } private void QueueLayoutPass() diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index 20050058bf..523c720e2f 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -367,6 +367,14 @@ namespace Avalonia.Layout } } + + /// + /// Called by InvalidateMeasure + /// + protected virtual void OnMeasureInvalidated() + { + } + /// /// Invalidates the measurement of the control and queues a new layout pass. /// @@ -378,8 +386,13 @@ namespace Avalonia.Layout IsMeasureValid = false; IsArrangeValid = false; - LayoutManager.Instance?.InvalidateMeasure(this); - InvalidateVisual(); + + if (((ILayoutable)this).IsAttachedToVisualTree) + { + LayoutManager.Instance?.InvalidateMeasure(this); + InvalidateVisual(); + } + OnMeasureInvalidated(); } } @@ -393,8 +406,12 @@ namespace Avalonia.Layout Logger.Verbose(LogArea.Layout, this, "Invalidated arrange"); IsArrangeValid = false; - LayoutManager.Instance?.InvalidateArrange(this); - InvalidateVisual(); + + if (((ILayoutable)this).IsAttachedToVisualTree) + { + LayoutManager.Instance?.InvalidateArrange(this); + InvalidateVisual(); + } } } diff --git a/src/Avalonia.Styling/LogicalTree/ILogical.cs b/src/Avalonia.Styling/LogicalTree/ILogical.cs index f2291b42e9..006a9f5cc1 100644 --- a/src/Avalonia.Styling/LogicalTree/ILogical.cs +++ b/src/Avalonia.Styling/LogicalTree/ILogical.cs @@ -36,6 +36,16 @@ namespace Avalonia.LogicalTree /// IAvaloniaReadOnlyList LogicalChildren { get; } + /// + /// Notifies the control that it is being attached to a rooted logical tree. + /// + /// The event args. + /// + /// This method will be called automatically by the framework, you should not need to call + /// this method yourself. + /// + void NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e); + /// /// Notifies the control that it is being detached from a rooted logical tree. /// diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index be4282cdc0..c050ff0e75 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -61,12 +61,12 @@ namespace Avalonia.Styling } /// - /// Gets or sets style's selector. + /// Gets or sets the style's selector. /// public Selector Selector { get; set; } /// - /// Gets or sets style's setters. + /// Gets or sets the style's setters. /// [Content] public IEnumerable Setters { get; set; } = new List(); diff --git a/src/Avalonia.Visuals/Platform/ILockedFramebuffer.cs b/src/Avalonia.Visuals/Platform/ILockedFramebuffer.cs index 92ec2877ab..45ca1a5a99 100644 --- a/src/Avalonia.Visuals/Platform/ILockedFramebuffer.cs +++ b/src/Avalonia.Visuals/Platform/ILockedFramebuffer.cs @@ -27,7 +27,7 @@ namespace Avalonia.Platform /// /// DPI of underling screen /// - Size Dpi { get; } + Vector Dpi { get; } /// /// Pixel format diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index cc1c700690..69cbfd9592 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -52,8 +52,6 @@ namespace Avalonia return new Point(a._x, a._y); } - - /// /// Calculates the dot product of two vectors /// @@ -65,6 +63,17 @@ namespace Avalonia return a.X*b.X + a.Y*b.Y; } + /// + /// Scales a vector. + /// + /// The vector + /// The scaling factor. + /// The scaled vector. + public static Vector operator *(Vector vector, double scale) + { + return new Vector(vector._x * scale, vector._y * scale); + } + /// /// Length of the vector /// diff --git a/src/Gtk/Avalonia.Gtk/GtkPlatform.cs b/src/Gtk/Avalonia.Gtk/GtkPlatform.cs index d387ed0320..ef7e4d3362 100644 --- a/src/Gtk/Avalonia.Gtk/GtkPlatform.cs +++ b/src/Gtk/Avalonia.Gtk/GtkPlatform.cs @@ -51,7 +51,6 @@ namespace Avalonia.Gtk .Bind().ToSingleton() .Bind().ToConstant(CursorFactory.Instance) .Bind().ToConstant(GtkKeyboardDevice.Instance) - .Bind().ToConstant(GtkMouseDevice.Instance) .Bind().ToConstant(s_instance) .Bind().ToConstant(s_instance) .Bind().ToConstant(s_instance) diff --git a/src/Gtk/Avalonia.Gtk/SurfaceFramebuffer.cs b/src/Gtk/Avalonia.Gtk/SurfaceFramebuffer.cs index 7e6da0e76a..29f4ce1d15 100644 --- a/src/Gtk/Avalonia.Gtk/SurfaceFramebuffer.cs +++ b/src/Gtk/Avalonia.Gtk/SurfaceFramebuffer.cs @@ -48,7 +48,7 @@ namespace Avalonia.Gtk public int Height => _surface.Height; public int RowBytes => _surface.Stride; //TODO: Proper DPI detect - public Size Dpi => new Size(96, 96); + public Vector Dpi => new Vector(96, 96); public PixelFormat Format => PixelFormat.Bgra8888; } } diff --git a/src/Gtk/Avalonia.Gtk/TopLevelImpl.cs b/src/Gtk/Avalonia.Gtk/TopLevelImpl.cs index a1bb3f847e..ce1f50ac0f 100644 --- a/src/Gtk/Avalonia.Gtk/TopLevelImpl.cs +++ b/src/Gtk/Avalonia.Gtk/TopLevelImpl.cs @@ -75,6 +75,8 @@ namespace Avalonia.Gtk } } + public IMouseDevice MouseDevice => GtkMouseDevice.Instance; + public Avalonia.Controls.WindowState WindowState { get @@ -114,6 +116,7 @@ namespace Avalonia.Gtk public Action Closed { get; set; } + public Action Deactivated { get; set; } public Action Input { get; set; } diff --git a/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs b/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs index a2913b4066..a3db0def74 100644 --- a/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs +++ b/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs @@ -34,7 +34,6 @@ namespace Avalonia.Gtk3 .Bind().ToSingleton() .Bind().ToConstant(new CursorFactory()) .Bind().ToConstant(Keyboard) - .Bind().ToConstant(Mouse) .Bind().ToConstant(Instance) .Bind().ToConstant(Instance) .Bind().ToSingleton() diff --git a/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs b/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs index 3263018a17..61b1e69aa2 100644 --- a/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs +++ b/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs @@ -52,12 +52,11 @@ namespace Avalonia.Gtk3 public int RowBytes { get; } - public Size Dpi + public Vector Dpi { get { - - return new Size(96, 96) * _factor; + return new Vector(96, 96) * _factor; } } diff --git a/src/Gtk/Avalonia.Gtk3/Interop/GObject.cs b/src/Gtk/Avalonia.Gtk3/Interop/GObject.cs index 9ead1d2cb3..9766e8ca36 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/GObject.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/GObject.cs @@ -41,7 +41,7 @@ namespace Avalonia.Gtk3.Interop class GtkWindow : GtkWidget { - + public static GtkWindow Null { get; } = new GtkWindow(); } class GtkImContext : GObject diff --git a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs index 9f38861b07..fb1a9955e3 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs @@ -500,6 +500,24 @@ namespace Avalonia.Gtk3.Interop public gdouble delta_y; } + [StructLayout(LayoutKind.Sequential)] + unsafe struct GdkEventCrossing + { + public GdkEventType type; + public IntPtr window; + public gint8 send_event; + public IntPtr subwindow; + public guint32 time; + public gdouble x; + public gdouble y; + public gdouble x_root; + public gdouble y_root; + public int mode; + public int detail; + public bool focus; + public GdkModifierType state; + }; + [StructLayout(LayoutKind.Sequential)] unsafe struct GdkEventWindowState { diff --git a/src/Gtk/Avalonia.Gtk3/Interop/Utf8Buffer.cs b/src/Gtk/Avalonia.Gtk3/Interop/Utf8Buffer.cs index f108c291b8..fc76fefd1a 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/Utf8Buffer.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/Utf8Buffer.cs @@ -11,6 +11,8 @@ namespace Avalonia.Gtk3.Interop public Utf8Buffer(string s) : base(IntPtr.Zero, true) { + if (s == null) + return; _data = Encoding.UTF8.GetBytes(s); _gchandle = GCHandle.Alloc(_data, GCHandleType.Pinned); handle = _gchandle.AddrOfPinnedObject(); diff --git a/src/Gtk/Avalonia.Gtk3/SystemDialogs.cs b/src/Gtk/Avalonia.Gtk3/SystemDialogs.cs index f6232ac68e..fb8af02d5d 100644 --- a/src/Gtk/Avalonia.Gtk3/SystemDialogs.cs +++ b/src/Gtk/Avalonia.Gtk3/SystemDialogs.cs @@ -18,7 +18,8 @@ namespace Avalonia.Gtk3 bool multiselect, string initialFileName) { GtkFileChooser dlg; - using (var name = title != null ? new Utf8Buffer(title) : null) + parent = parent ?? GtkWindow.Null; + using (var name = new Utf8Buffer(title)) dlg = Native.GtkFileChooserDialogNew(name, parent, action, IntPtr.Zero); if (multiselect) Native.GtkFileChooserSetSelectMultiple(dlg, true); diff --git a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs index 478580e65e..39304940d2 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs @@ -45,6 +45,7 @@ namespace Avalonia.Gtk3 ConnectEvent("window-state-event", OnStateChanged); ConnectEvent("key-press-event", OnKeyEvent); ConnectEvent("key-release-event", OnKeyEvent); + ConnectEvent("leave-notify-event", OnLeaveNotifyEvent); Connect("destroy", OnDestroy); Native.GtkWidgetRealize(gtkWidget); _lastSize = ClientSize; @@ -194,6 +195,18 @@ namespace Avalonia.Gtk3 return true; } + private unsafe bool OnLeaveNotifyEvent(IntPtr w, IntPtr pev, IntPtr userData) + { + var evnt = (GdkEventCrossing*) pev; + var position = new Point(evnt->x, evnt->y); + Input(new RawMouseEventArgs(Gtk3Platform.Mouse, + evnt->time, + _inputRoot, + RawMouseEventType.Move, + position, GetModifierKeys(evnt->state))); + return true; + } + private unsafe bool OnCommit(IntPtr gtkwidget, IntPtr utf8string, IntPtr userdata) { Input(new RawTextInputEventArgs(Gtk3Platform.Keyboard, _lastKbdEvent, Utf8Buffer.StringFromPtr(utf8string))); @@ -233,6 +246,7 @@ namespace Avalonia.Gtk3 } } + public IMouseDevice MouseDevice => Gtk3Platform.Mouse; public double Scaling => (double) 1 / (Native.GtkWidgetGetScaleFactor?.Invoke(GtkWidget) ?? 1); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 193d2c1d05..0854f9acf4 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -56,6 +56,7 @@ namespace Avalonia.LinuxFramebuffer } public Size ClientSize => _fb.PixelSize; + public IMouseDevice MouseDevice => LinuxFramebufferPlatform.MouseDevice; public double Scaling => 1; public IEnumerable Surfaces => new object[] {_fb}; public Action Input { get; set; } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs index 5aec5408a4..8d04360edf 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs @@ -13,16 +13,16 @@ namespace Avalonia.LinuxFramebuffer { public sealed unsafe class LinuxFramebuffer : IFramebufferPlatformSurface, IDisposable { - private readonly Size _dpi; + private readonly Vector _dpi; private int _fd; private fb_fix_screeninfo _fixedInfo; private fb_var_screeninfo _varInfo; private IntPtr _mappedLength; private IntPtr _mappedAddress; - public LinuxFramebuffer(string fileName = null, Size? dpi = null) + public LinuxFramebuffer(string fileName = null, Vector? dpi = null) { - _dpi = dpi ?? new Size(96, 96); + _dpi = dpi ?? new Vector(96, 96); fileName = fileName ?? Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0"; _fd = NativeUnsafeMethods.open(fileName, 2, 0); if (_fd <= 0) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index a65a91d6b0..5f1c55fd5d 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -33,7 +33,6 @@ namespace Avalonia.LinuxFramebuffer AvaloniaLocator.CurrentMutable .Bind().ToTransient() .Bind().ToConstant(KeyboardDevice) - .Bind().ToConstant(MouseDevice) .Bind().ToSingleton() .Bind().ToConstant(ImmediateRenderer.Factory) .Bind().ToConstant(PlatformThreadingInterface.Instance) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs b/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs index d8330fcb70..795d9648ea 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs @@ -11,7 +11,7 @@ namespace Avalonia.LinuxFramebuffer private fb_var_screeninfo _varInfo; private readonly IntPtr _address; - public LockedFramebuffer(int fb, fb_fix_screeninfo fixedInfo, fb_var_screeninfo varInfo, IntPtr address, Size dpi) + public LockedFramebuffer(int fb, fb_fix_screeninfo fixedInfo, fb_var_screeninfo varInfo, IntPtr address, Vector dpi) { _fb = fb; _fixedInfo = fixedInfo; @@ -41,7 +41,7 @@ namespace Avalonia.LinuxFramebuffer public int Width => (int)_varInfo.xres; public int Height => (int) _varInfo.yres; public int RowBytes => (int) _fixedInfo.line_length; - public Size Dpi { get; } + public Vector Dpi { get; } public PixelFormat Format => _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 : PixelFormat.Bgra8888; } } \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs index 69190be220..621e06efba 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs @@ -102,7 +102,7 @@ namespace Avalonia.Markup.Xaml.Data private object ConvertValue(IList values, Type targetType) { - var converted = Converter.Convert(values, targetType, null, CultureInfo.CurrentUICulture); + var converted = Converter.Convert(values, targetType, null, CultureInfo.CurrentCulture); if (converted == AvaloniaProperty.UnsetValue && FallbackValue != null) { diff --git a/src/Markup/Avalonia.Markup/Data/BindingExpression.cs b/src/Markup/Avalonia.Markup/Data/BindingExpression.cs index 0f4c091bff..5b9959e42e 100644 --- a/src/Markup/Avalonia.Markup/Data/BindingExpression.cs +++ b/src/Markup/Avalonia.Markup/Data/BindingExpression.cs @@ -122,7 +122,7 @@ namespace Avalonia.Markup.Data value, type, ConverterParameter, - CultureInfo.CurrentUICulture); + CultureInfo.CurrentCulture); if (converted == AvaloniaProperty.UnsetValue) { @@ -186,7 +186,7 @@ namespace Avalonia.Markup.Data value, _targetType, ConverterParameter, - CultureInfo.CurrentUICulture); + CultureInfo.CurrentCulture); notification = converted as BindingNotification; diff --git a/src/Skia/Avalonia.Skia.Desktop.NetStandard/Avalonia.Skia.Desktop.NetStandard.csproj b/src/Skia/Avalonia.Skia.Desktop.NetStandard/Avalonia.Skia.Desktop.NetStandard.csproj index ca9f6c76e1..311abce88f 100644 --- a/src/Skia/Avalonia.Skia.Desktop.NetStandard/Avalonia.Skia.Desktop.NetStandard.csproj +++ b/src/Skia/Avalonia.Skia.Desktop.NetStandard/Avalonia.Skia.Desktop.NetStandard.csproj @@ -5,6 +5,7 @@ false Avalonia.Skia.Desktop Avalonia.Skia.Desktop + true true @@ -42,4 +43,4 @@ - \ No newline at end of file + diff --git a/src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.csproj b/src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.csproj index e2d4522d7e..f0964680a1 100644 --- a/src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.csproj +++ b/src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.csproj @@ -54,6 +54,9 @@ prompt MinimumRecommendedRules.ruleset + + true + @@ -106,4 +109,4 @@ - \ No newline at end of file + diff --git a/src/Skia/Avalonia.Skia/BitmapImpl.cs b/src/Skia/Avalonia.Skia/BitmapImpl.cs index e9c241b848..e5e8faec5f 100644 --- a/src/Skia/Avalonia.Skia/BitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/BitmapImpl.cs @@ -131,7 +131,7 @@ namespace Avalonia.Skia public int Width => _bmp.Width; public int Height => _bmp.Height; public int RowBytes => _bmp.RowBytes; - public Size Dpi { get; } = new Size(96, 96); + public Vector Dpi { get; } = new Vector(96, 96); public PixelFormat Format => _bmp.ColorType.ToPixelFormat(); } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 4a9f2c6572..3ed0509c0a 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -141,23 +141,17 @@ namespace Avalonia.Skia var rv = new PaintWrapper(paint); paint.IsStroke = false; - // TODO: SkiaSharp does not contain alpha yet! + double opacity = brush.Opacity * _currentOpacity; - //paint.SetAlpha(paint.GetAlpha() * opacity); paint.IsAntialias = true; - SKColor color = new SKColor(255, 255, 255, 255); - var solid = brush as ISolidColorBrush; - if (solid != null) - color = solid.Color.ToSKColor(); - - paint.Color = (new SKColor(color.Red, color.Green, color.Blue, (byte)(color.Alpha * opacity))); - if (solid != null) { + paint.Color = new SKColor(solid.Color.R, solid.Color.G, solid.Color.B, (byte) (solid.Color.A * opacity)); return rv; } + paint.Color = (new SKColor(255, 255, 255, (byte)(255 * opacity))); var gradient = brush as IGradientBrush; if (gradient != null) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 8568c80c04..1d224f97d7 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -42,7 +42,6 @@ namespace Avalonia.Skia _paint.Typeface = skiaTypeface; _paint.TextSize = (float)(typeface?.FontSize ?? 12); _paint.TextAlign = textAlignment.ToSKTextAlign(); - _paint.BlendMode = SKBlendMode.Src; _wrapping = wrapping; _constraint = constraint; @@ -200,66 +199,65 @@ namespace Avalonia.Skia } ctx->Canvas->restore(); */ - SKPaint paint = _paint; - IDisposable currd = null; - var currentWrapper = foreground; - - try + using (var paint = _paint.Clone()) { - SKPaint currFGPaint = ApplyWrapperTo(ref foreground, ref currd, paint); - bool hasCusomFGBrushes = _foregroundBrushes.Any(); - - for (int c = 0; c < _skiaLines.Count; c++) + IDisposable currd = null; + var currentWrapper = foreground; + SKPaint currentPaint = null; + try { - AvaloniaFormattedTextLine line = _skiaLines[c]; - - float x = TransformX(origin.X, 0, paint.TextAlign); + ApplyWrapperTo(ref currentPaint, foreground, ref currd, paint); + bool hasCusomFGBrushes = _foregroundBrushes.Any(); - if (!hasCusomFGBrushes) - { - var subString = Text.Substring(line.Start, line.Length); - canvas.DrawText(subString, x, origin.Y + line.Top + _lineOffset, paint); - } - else + for (int c = 0; c < _skiaLines.Count; c++) { - float currX = x; - string subStr; - int len; + AvaloniaFormattedTextLine line = _skiaLines[c]; - for (int i = line.Start; i < line.Start + line.Length;) - { - var fb = GetNextForegroundBrush(ref line, i, out len); - - if (fb != null) - { - //TODO: figure out how to get the brush size - currentWrapper = context.CreatePaint(fb, new Size()); - } - else - { - if (!currentWrapper.Equals(foreground)) currentWrapper.Dispose(); - currentWrapper = foreground; - } + float x = TransformX(origin.X, 0, paint.TextAlign); - subStr = Text.Substring(i, len); + if (!hasCusomFGBrushes) + { + var subString = Text.Substring(line.Start, line.Length); + canvas.DrawText(subString, x, origin.Y + line.Top + _lineOffset, paint); + } + else + { + float currX = x; + string subStr; + int len; - if (currFGPaint != currentWrapper.Paint) + for (int i = line.Start; i < line.Start + line.Length;) { - currFGPaint = ApplyWrapperTo(ref currentWrapper, ref currd, paint); + var fb = GetNextForegroundBrush(ref line, i, out len); + + if (fb != null) + { + //TODO: figure out how to get the brush size + currentWrapper = context.CreatePaint(fb, new Size()); + } + else + { + if (!currentWrapper.Equals(foreground)) currentWrapper.Dispose(); + currentWrapper = foreground; + } + + subStr = Text.Substring(i, len); + + ApplyWrapperTo(ref currentPaint, currentWrapper, ref currd, paint); + + canvas.DrawText(subStr, currX, origin.Y + line.Top + _lineOffset, paint); + + i += len; + currX += paint.MeasureText(subStr); } - - canvas.DrawText(subStr, currX, origin.Y + line.Top + _lineOffset, paint); - - i += len; - currX += paint.MeasureText(subStr); } } } - } - finally - { - if (!currentWrapper.Equals(foreground)) currentWrapper.Dispose(); - currd?.Dispose(); + finally + { + if (!currentWrapper.Equals(foreground)) currentWrapper.Dispose(); + currd?.Dispose(); + } } } @@ -278,12 +276,13 @@ namespace Avalonia.Skia private Size _size; private List _skiaLines; - private static SKPaint ApplyWrapperTo(ref DrawingContextImpl.PaintWrapper wrapper, + private static void ApplyWrapperTo(ref SKPaint current, DrawingContextImpl.PaintWrapper wrapper, ref IDisposable curr, SKPaint paint) { + if (current == wrapper.Paint) + return; curr?.Dispose(); curr = wrapper.ApplyTo(paint); - return wrapper.Paint; } private static bool IsBreakChar(char c) diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index 0eacdf41ac..ae8e653e55 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -76,7 +76,7 @@ namespace Avalonia.Skia canvas.RestoreToCount(0); canvas.Save(); canvas.ResetMatrix(); - var scale = Matrix.CreateScale(fb.Dpi.Width / 96, fb.Dpi.Height / 96); + var scale = Matrix.CreateScale(fb.Dpi.X / 96, fb.Dpi.Y / 96); return new DrawingContextImpl(canvas, visualBrushRenderer, scale, canvas, surface, shim, fb); } } diff --git a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj index bd2e77d022..38fd7254dc 100644 --- a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj +++ b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj @@ -50,7 +50,9 @@ + + diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 469c29d626..3176dee7a4 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -135,12 +135,17 @@ namespace Avalonia.Direct2D1 public IRenderTarget CreateRenderTarget(IEnumerable surfaces) { - var nativeWindow = surfaces?.OfType().FirstOrDefault(); - if (nativeWindow != null) + foreach (var s in surfaces) { - if(nativeWindow.HandleDescriptor != "HWND") - throw new NotSupportedException("Don't know how to create a Direct2D1 renderer from " + nativeWindow.HandleDescriptor); - return new HwndRenderTarget(nativeWindow); + if (s is IPlatformHandle nativeWindow) + { + if (nativeWindow.HandleDescriptor != "HWND") + throw new NotSupportedException("Don't know how to create a Direct2D1 renderer from " + + nativeWindow.HandleDescriptor); + return new HwndRenderTarget(nativeWindow); + } + if (s is IExternalDirect2DRenderTargetSurface external) + return new ExternalRenderTarget(external, s_dwfactory); } throw new NotSupportedException("Don't know how to create a Direct2D1 renderer from any of provided surfaces"); } diff --git a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs new file mode 100644 index 0000000000..307048f7b4 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Direct2D1.Media; +using Avalonia.Platform; +using Avalonia.Rendering; +using SharpDX; +using DirectWriteFactory = SharpDX.DirectWrite.Factory; + +namespace Avalonia.Direct2D1 +{ + class ExternalRenderTarget : IRenderTarget + { + private readonly IExternalDirect2DRenderTargetSurface _externalRenderTargetProvider; + private readonly DirectWriteFactory _dwFactory; + public ExternalRenderTarget(IExternalDirect2DRenderTargetSurface externalRenderTargetProvider, + DirectWriteFactory dwFactory) + { + _externalRenderTargetProvider = externalRenderTargetProvider; + _dwFactory = dwFactory; + } + + public void Dispose() + { + _externalRenderTargetProvider.DestroyRenderTarget(); + } + + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + { + var target = _externalRenderTargetProvider.GetOrCreateRenderTarget(); + _externalRenderTargetProvider.BeforeDrawing(); + return new DrawingContextImpl(visualBrushRenderer, target, _dwFactory, null, () => + { + try + { + _externalRenderTargetProvider.AfterDrawing(); + } + catch (SharpDXException ex) when ((uint) ex.HResult == 0x8899000C) // D2DERR_RECREATE_TARGET + { + _externalRenderTargetProvider.DestroyRenderTarget(); + } + }); + } + } +} diff --git a/src/Windows/Avalonia.Direct2D1/IExternalDirect2DRenderTargetSurface.cs b/src/Windows/Avalonia.Direct2D1/IExternalDirect2DRenderTargetSurface.cs new file mode 100644 index 0000000000..aad51f46d5 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/IExternalDirect2DRenderTargetSurface.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Direct2D1 +{ + public interface IExternalDirect2DRenderTargetSurface + { + SharpDX.Direct2D1.RenderTarget GetOrCreateRenderTarget(); + void DestroyRenderTarget(); + void BeforeDrawing(); + void AfterDrawing(); + } +} diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 535ca900c2..0b46ba1c47 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -23,6 +23,7 @@ namespace Avalonia.Direct2D1.Media private readonly IVisualBrushRenderer _visualBrushRenderer; private readonly SharpDX.Direct2D1.RenderTarget _renderTarget; private readonly SharpDX.DXGI.SwapChain1 _swapChain; + private readonly Action _finishedCallback; private SharpDX.DirectWrite.Factory _directWriteFactory; /// @@ -32,15 +33,18 @@ namespace Avalonia.Direct2D1.Media /// The render target to draw to. /// The DirectWrite factory. /// An optional swap chain associated with this drawing context. + /// An optional delegate to be called when context is disposed. public DrawingContextImpl( IVisualBrushRenderer visualBrushRenderer, SharpDX.Direct2D1.RenderTarget renderTarget, SharpDX.DirectWrite.Factory directWriteFactory, - SharpDX.DXGI.SwapChain1 swapChain = null) + SharpDX.DXGI.SwapChain1 swapChain = null, + Action finishedCallback = null) { _visualBrushRenderer = visualBrushRenderer; _renderTarget = renderTarget; _swapChain = swapChain; + _finishedCallback = finishedCallback; _directWriteFactory = directWriteFactory; _swapChain = swapChain; _renderTarget.BeginDraw(); @@ -73,6 +77,7 @@ namespace Avalonia.Direct2D1.Media _renderTarget.EndDraw(); _swapChain?.Present(1, SharpDX.DXGI.PresentFlags.None); + _finishedCallback?.Invoke(); } catch (SharpDXException ex) when ((uint)ex.HResult == 0x8899000C) // D2DERR_RECREATE_TARGET { diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WritableWicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WritableWicBitmapImpl.cs index 06eb26b407..5dc07e06c4 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WritableWicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WritableWicBitmapImpl.cs @@ -37,7 +37,7 @@ namespace Avalonia.Direct2D1.Media.Imaging public int Width => _lock.Size.Width; public int Height => _lock.Size.Height; public int RowBytes => _lock.Stride; - public Size Dpi { get; } = new Size(96, 96); + public Vector Dpi { get; } = new Vector(96, 96); public PixelFormat Format => _format; } diff --git a/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj b/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj new file mode 100644 index 0000000000..5f1a065028 --- /dev/null +++ b/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj @@ -0,0 +1,123 @@ + + + + + Debug + AnyCPU + {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E} + Library + Properties + Avalonia.Win32.Interop + Avalonia.Win32.Interop + v4.5.2 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + UnmanagedMethods.cs + + + + + + + + + + + + + + {d211e587-d8bc-45b9-95a4-f297c8fa5200} + Avalonia.Animation + + + {b09b78d8-9b26-48b0-9149-d64a2f120f3f} + Avalonia.Base + + + {d2221c82-4a25-4583-9b43-d791e3f6820c} + Avalonia.Controls + + + {4a1abb09-9047-4bd5-a4ad-a055e52c5ee0} + Avalonia.DotNetFrameworkRuntime + + + {62024b2d-53eb-4638-b26b-85eeaa54866e} + Avalonia.Input + + + {6b0ed19d-a08b-461c-a9d9-a9ee40b0c06b} + Avalonia.Interactivity + + + {42472427-4774-4c81-8aff-9f27b8e31721} + Avalonia.Layout + + + {f1baa01a-f176-4c6a-b39d-5b40bb1b148f} + Avalonia.Styling + + + {eb582467-6abb-43a1-b052-e981ba910e3a} + Avalonia.Visuals + + + {fb05ac90-89ba-4f2f-a924-f37875fb547c} + Avalonia.Cairo + + + {3e53a01a-b331-47f3-b828-4a5717e77a24} + Avalonia.Markup.Xaml + + + {6417e941-21bc-467b-a771-0de389353ce6} + Avalonia.Markup + + + {3e908f67-5543-4879-a1dc-08eace79b3cd} + Avalonia.Direct2D1 + + + {811a76cf-1cf6-440f-963b-bbe31bd72a82} + Avalonia.Win32 + + + + true + + + + \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32.Interop/Properties/AssemblyInfo.cs b/src/Windows/Avalonia.Win32.Interop/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..7c0d638381 --- /dev/null +++ b/src/Windows/Avalonia.Win32.Interop/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Avalonia.Win32.Interop")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Avalonia.Win32.Interop")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("cbc4ff2f-92d4-420b-be21-9fe0b930b04e")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/CursorShim.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/CursorShim.cs new file mode 100644 index 0000000000..6ae898ae3d --- /dev/null +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/CursorShim.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace Avalonia.Win32.Interop.Wpf +{ + static class CursorShim + { + public static Cursor FromHCursor(IntPtr hcursor) + { + var field = typeof(Cursor).GetFields(BindingFlags.NonPublic | BindingFlags.Instance) + .FirstOrDefault(f => f.FieldType == typeof(SafeHandle)); + if (field == null) + return null; + var rv = (Cursor) FormatterServices.GetUninitializedObject(typeof(Cursor)); + field.SetValue(rv, new SafeHandleShim(hcursor)); + return rv; + } + + class SafeHandleShim : SafeHandle + { + public SafeHandleShim(IntPtr hcursor) : base(new IntPtr(-1), false) + { + this.handle = hcursor; + } + + protected override bool ReleaseHandle() => true; + + public override bool IsInvalid => false; + } + } +} diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/Direct2DImageSurface.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/Direct2DImageSurface.cs new file mode 100644 index 0000000000..8fe7275a0f --- /dev/null +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/Direct2DImageSurface.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Interop; +using Avalonia.Direct2D1; +using SharpDX; +using SharpDX.Direct2D1; +using SharpDX.Direct3D11; +using SharpDX.Direct3D9; +using SharpDX.DXGI; +using AlphaMode = SharpDX.Direct2D1.AlphaMode; +using Device = SharpDX.Direct3D11.Device; +using Format = SharpDX.DXGI.Format; +using MapFlags = SharpDX.Direct3D11.MapFlags; +using PresentParameters = SharpDX.DXGI.PresentParameters; +using Query = SharpDX.Direct3D11.Query; +using QueryType = SharpDX.Direct3D11.QueryType; +using RenderTarget = SharpDX.Direct2D1.RenderTarget; +using Surface = SharpDX.DXGI.Surface; +using SwapEffect = SharpDX.DXGI.SwapEffect; +using Usage = SharpDX.Direct3D9.Usage; + +namespace Avalonia.Win32.Interop.Wpf +{ + class Direct2DImageSurface : IExternalDirect2DRenderTargetSurface, IDisposable + { + class SwapBuffer: IDisposable + { + private readonly Query _event; + private readonly SharpDX.Direct3D11.Resource _resource; + private readonly SharpDX.Direct3D11.Resource _sharedResource; + public SharpDX.Direct3D9.Surface Texture { get; } + public RenderTarget Target { get;} + public IntSize Size { get; } + + public SwapBuffer(IntSize size, Vector dpi) + { + int width = (int) size.Width; + int height = (int) size.Height; + _event = new Query(s_dxDevice, new QueryDescription {Type = QueryType.Event}); + using (var texture = new Texture2D(s_dxDevice, new Texture2DDescription + { + Width = width, + Height = height, + ArraySize = 1, + MipLevels = 1, + Format = Format.B8G8R8A8_UNorm, + Usage = ResourceUsage.Default, + SampleDescription = new SampleDescription(2, 0), + BindFlags = BindFlags.RenderTarget, + })) + using (var surface = texture.QueryInterface()) + + { + _resource = texture.QueryInterface(); + + Target = new RenderTarget(AvaloniaLocator.Current.GetService(), surface, + new RenderTargetProperties + { + DpiX = (float) dpi.X, + DpiY = (float) dpi.Y, + MinLevel = FeatureLevel.Level_10, + PixelFormat = new PixelFormat(Format.B8G8R8A8_UNorm, AlphaMode.Premultiplied), + + }); + } + using (var texture = new Texture2D(s_dxDevice, new Texture2DDescription + { + Width = width, + Height = height, + ArraySize = 1, + MipLevels = 1, + Format = Format.B8G8R8A8_UNorm, + Usage = ResourceUsage.Default, + SampleDescription = new SampleDescription(1, 0), + BindFlags = BindFlags.RenderTarget|BindFlags.ShaderResource, + OptionFlags = ResourceOptionFlags.Shared, + })) + using (var resource = texture.QueryInterface()) + { + _sharedResource = texture.QueryInterface(); + var handle = resource.SharedHandle; + using (var texture9 = new Texture(s_d3DDevice, texture.Description.Width, + texture.Description.Height, 1, + Usage.RenderTarget, SharpDX.Direct3D9.Format.A8R8G8B8, Pool.Default, ref handle)) + Texture = texture9.GetSurfaceLevel(0); + } + Size = size; + } + + public void Dispose() + { + Texture?.Dispose(); + Target?.Dispose(); + _resource?.Dispose(); + _sharedResource?.Dispose(); + _event?.Dispose(); + } + + public void Flush() + { + s_dxDevice.ImmediateContext.ResolveSubresource(_resource, 0, _sharedResource, 0, Format.B8G8R8A8_UNorm); + s_dxDevice.ImmediateContext.Flush(); + s_dxDevice.ImmediateContext.End(_event); + s_dxDevice.ImmediateContext.GetData(_event).Dispose(); + } + } + + private D3DImage _image; + private SwapBuffer _backBuffer; + private readonly WpfTopLevelImpl _impl; + private static Device s_dxDevice; + private static Direct3DEx s_d3DContext; + private static DeviceEx s_d3DDevice; + private Vector _oldDpi; + + + [DllImport("user32.dll", SetLastError = false)] + private static extern IntPtr GetDesktopWindow(); + void EnsureDirectX() + { + if(s_d3DDevice != null) + return; + s_d3DContext = new Direct3DEx(); + + SharpDX.Direct3D9.PresentParameters presentparams = new SharpDX.Direct3D9.PresentParameters + { + Windowed = true, + SwapEffect = SharpDX.Direct3D9.SwapEffect.Discard, + DeviceWindowHandle = GetDesktopWindow(), + PresentationInterval = PresentInterval.Default + }; + s_dxDevice = s_dxDevice ?? AvaloniaLocator.Current.GetService() + .QueryInterface(); + s_d3DDevice = new DeviceEx(s_d3DContext, 0, DeviceType.Hardware, IntPtr.Zero, CreateFlags.HardwareVertexProcessing | CreateFlags.Multithreaded | CreateFlags.FpuPreserve, presentparams); + + } + + public Direct2DImageSurface(WpfTopLevelImpl impl) + { + _impl = impl; + } + + public RenderTarget GetOrCreateRenderTarget() + { + EnsureDirectX(); + var scale = _impl.GetScaling(); + var size = new IntSize(_impl.ActualWidth * scale.X, _impl.ActualHeight * scale.Y); + var dpi = scale * 96; + + if (_backBuffer!=null && _backBuffer.Size == size) + return _backBuffer.Target; + + if (_image == null || _oldDpi.X != dpi.X || _oldDpi.Y != dpi.Y) + { + _image = new D3DImage(dpi.X, dpi.Y); + } + _impl.ImageSource = _image; + + RemoveAndDispose(ref _backBuffer); + if (size == default(IntSize)) + { + _image.Lock(); + _image.SetBackBuffer(D3DResourceType.IDirect3DSurface9, IntPtr.Zero); + _image.Unlock(); + return null; + } + _backBuffer = new SwapBuffer(size, dpi); + + return _backBuffer.Target; + } + + void RemoveAndDispose(ref T d) where T : IDisposable + { + d?.Dispose(); + d = default(T); + } + + void Swap() + { + _backBuffer.Flush(); + _image.Lock(); + _image.SetBackBuffer(D3DResourceType.IDirect3DSurface9, _backBuffer?.Texture?.NativePointer ?? IntPtr.Zero, true); + _image.AddDirtyRect(new Int32Rect(0, 0, _image.PixelWidth, _image.PixelHeight)); + _image.Unlock(); + } + + public void DestroyRenderTarget() + { + RemoveAndDispose(ref _backBuffer); + } + + public void BeforeDrawing() + { + + } + + public void AfterDrawing() => Swap(); + public void Dispose() + { + RemoveAndDispose(ref _backBuffer); + } + } +} diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/IntSize.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/IntSize.cs new file mode 100644 index 0000000000..3fdbdedfd9 --- /dev/null +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/IntSize.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Win32.Interop.Wpf +{ + struct IntSize : IEquatable + { + public bool Equals(IntSize other) + { + return Width == other.Width && Height == other.Height; + } + + public IntSize(int width, int height) + { + Width = width; + Height = height; + } + + public IntSize(double width, double height) : this((int) width, (int) height) + { + + } + + public static implicit operator IntSize(System.Windows.Size size) + { + return new IntSize {Width = (int) size.Width, Height = (int) size.Height}; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + return obj is IntSize && Equals((IntSize) obj); + } + + public override int GetHashCode() + { + unchecked + { + return (Width * 397) ^ Height; + } + } + + public static bool operator ==(IntSize left, IntSize right) + { + return left.Equals(right); + } + + public static bool operator !=(IntSize left, IntSize right) + { + return !left.Equals(right); + } + + public int Width { get; set; } + public int Height { get; set; } + } +} diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfAvaloniaHost.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfAvaloniaHost.cs new file mode 100644 index 0000000000..6dc9ba9e09 --- /dev/null +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfAvaloniaHost.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Markup; +using System.Windows.Media; +using Avalonia.Markup.Xaml.Styling; +using Avalonia.Platform; +using Avalonia.Styling; + +namespace Avalonia.Win32.Interop.Wpf +{ + [ContentProperty("Content")] + public class WpfAvaloniaHost : FrameworkElement, IDisposable, IAddChild + { + private WpfTopLevelImpl _impl; + private readonly SynchronizationContext _sync; + private bool _hasChildren; + public WpfAvaloniaHost() + { + _sync = SynchronizationContext.Current; + _impl = new WpfTopLevelImpl(); + _impl.ControlRoot.Prepare(); + _impl.Visibility = Visibility.Visible; + SnapsToDevicePixels = true; + UseLayoutRounding = true; + PresentationSource.AddSourceChangedHandler(this, OnSourceChanged); + } + + private void OnSourceChanged(object sender, SourceChangedEventArgs e) + { + if (e.NewSource != null && !_hasChildren) + { + AddLogicalChild(_impl); + AddVisualChild(_impl); + _hasChildren = true; + } + else + { + RemoveVisualChild(_impl); + RemoveLogicalChild(_impl); + _hasChildren = false; + } + } + + public object Content + { + get => _impl.ControlRoot.Content; + set => _impl.ControlRoot.Content = value; + } + + //Separate class is needed to prevent accidential resurrection + class Disposer + { + private readonly WpfTopLevelImpl _impl; + + public Disposer(WpfTopLevelImpl impl) + { + _impl = impl; + } + + public void Callback(object state) + { + _impl.Dispose(); + } + } + + protected override System.Windows.Size MeasureOverride(System.Windows.Size constraint) + { + _impl.InvalidateMeasure(); + _impl.Measure(constraint); + return _impl.DesiredSize; + } + + protected override System.Windows.Size ArrangeOverride(System.Windows.Size arrangeSize) + { + _impl.Arrange(new System.Windows.Rect(arrangeSize)); + return arrangeSize; + } + + protected override int VisualChildrenCount => 1; + protected override System.Windows.Media.Visual GetVisualChild(int index) => _impl; + + ~WpfAvaloniaHost() + { + if (_impl != null) + _sync.Post(new Disposer(_impl).Callback, null); + } + + public void Dispose() + { + if (_impl != null) + { + RemoveVisualChild(_impl); + RemoveLogicalChild(_impl); + _impl.Dispose(); + _impl = null; + GC.SuppressFinalize(this); + } + } + + void IAddChild.AddChild(object value) + { + if (Content == null) + Content = value; + else + throw new InvalidOperationException(); + } + + void IAddChild.AddText(string text) + { + // + } + } +} diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfInteropExtensions.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfInteropExtensions.cs new file mode 100644 index 0000000000..6433ff05e0 --- /dev/null +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfInteropExtensions.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Win32.Interop.Wpf +{ + static class WpfInteropExtensions + { + public static System.Windows.Point ToWpfPoint(this Point pt) => new System.Windows.Point(pt.X, pt.Y); + public static Point ToAvaloniaPoint(this System.Windows.Point pt) => new Point(pt.X, pt.Y); + public static System.Windows.Size ToWpfSize(this Size pt) => new System.Windows.Size(pt.Width, pt.Height); + public static Size ToAvaloniaSize(this System.Windows.Size pt) => new Size(pt.Width, pt.Height); + } +} diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfMouseDevice.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfMouseDevice.cs new file mode 100644 index 0000000000..4aad80f8a5 --- /dev/null +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfMouseDevice.cs @@ -0,0 +1,30 @@ +using System; +using Avalonia.Controls.Embedding; +using Avalonia.Input; +using Avalonia.VisualTree; + +namespace Avalonia.Win32.Interop.Wpf +{ + class WpfMouseDevice : MouseDevice + { + private readonly WpfTopLevelImpl _impl; + + public WpfMouseDevice(WpfTopLevelImpl impl) + { + _impl = impl; + } + + public override void Capture(IInputElement control) + { + if (control == null) + { + System.Windows.Input.Mouse.Capture(null); + } + else if ((control.GetVisualRoot() as EmbeddableControlRoot)?.PlatformImpl != _impl) + throw new ArgumentException("Visual belongs to unknown toplevel"); + else + System.Windows.Input.Mouse.Capture(_impl); + base.Capture(control); + } + } +} \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs new file mode 100644 index 0000000000..fbed2f621c --- /dev/null +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Interop; +using System.Windows.Media; +using Avalonia.Controls.Embedding; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Layout; +using Avalonia.Platform; +using Key = Avalonia.Input.Key; +using KeyEventArgs = System.Windows.Input.KeyEventArgs; +using MouseButton = System.Windows.Input.MouseButton; + +namespace Avalonia.Win32.Interop.Wpf +{ + class WpfTopLevelImpl : FrameworkElement, IEmbeddableWindowImpl + { + private HwndSource _currentHwndSource; + private readonly HwndSourceHook _hook; + private readonly IEmbeddableWindowImpl _ttl; + private IInputRoot _inputRoot; + private readonly IEnumerable _surfaces; + private readonly IMouseDevice _mouse; + private readonly IKeyboardDevice _keyboard; + private Size _finalSize; + + public EmbeddableControlRoot ControlRoot { get; } + internal ImageSource ImageSource { get; set; } + + public class CustomControlRoot : EmbeddableControlRoot, IEmbeddedLayoutRoot + { + public CustomControlRoot(WpfTopLevelImpl impl) : base(impl) + { + EnforceClientSize = false; + } + + protected override void OnMeasureInvalidated() + { + ((FrameworkElement)PlatformImpl)?.InvalidateMeasure(); + } + + protected override void HandleResized(Size clientSize) + { + ClientSize = clientSize; + LayoutManager.Instance.ExecuteLayoutPass(); + Renderer?.Resized(clientSize); + } + + public Size AllocatedSize => ClientSize; + } + + public WpfTopLevelImpl() + { + PresentationSource.AddSourceChangedHandler(this, OnSourceChanged); + _hook = WndProc; + _ttl = this; + _surfaces = new object[] {new WritableBitmapSurface(this), new Direct2DImageSurface(this)}; + _mouse = new WpfMouseDevice(this); + _keyboard = AvaloniaLocator.Current.GetService(); + + ControlRoot = new CustomControlRoot(this); + SnapsToDevicePixels = true; + Focusable = true; + DataContextChanged += delegate + { + ControlRoot.DataContext = DataContext; + }; + } + + private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam, ref bool handled) + { + if (msg == (int)UnmanagedMethods.WindowsMessage.WM_DPICHANGED) + _ttl.ScalingChanged?.Invoke(_ttl.Scaling); + return IntPtr.Zero; + } + + private void OnSourceChanged(object sender, SourceChangedEventArgs e) + { + _currentHwndSource?.RemoveHook(_hook); + _currentHwndSource = e.NewSource as HwndSource; + _currentHwndSource?.AddHook(_hook); + _ttl.ScalingChanged?.Invoke(_ttl.Scaling); + } + + public void Dispose() + { + _ttl.Closed?.Invoke(); + foreach(var d in _surfaces.OfType()) + d.Dispose(); + } + + Size ITopLevelImpl.ClientSize => _finalSize; + IMouseDevice ITopLevelImpl.MouseDevice => _mouse; + + double ITopLevelImpl.Scaling => PresentationSource.FromVisual(this)?.CompositionTarget?.TransformToDevice.M11 ?? 1; + + IEnumerable ITopLevelImpl.Surfaces => _surfaces; + + private Size _previousSize; + protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize) + { + _finalSize = finalSize.ToAvaloniaSize(); + if (_finalSize == _previousSize) + return finalSize; + _previousSize = _finalSize; + _ttl.Resized?.Invoke(finalSize.ToAvaloniaSize()); + return base.ArrangeOverride(finalSize); + } + + protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize) + { + ControlRoot.Measure(availableSize.ToAvaloniaSize()); + return ControlRoot.DesiredSize.ToWpfSize(); + } + + protected override void OnRender(DrawingContext drawingContext) + { + if(ActualHeight == 0 || ActualWidth == 0) + return; + _ttl.Paint?.Invoke(new Rect(0, 0, ActualWidth, ActualHeight)); + if (ImageSource != null) + drawingContext.DrawImage(ImageSource, new System.Windows.Rect(0, 0, ActualWidth, ActualHeight)); + } + + void ITopLevelImpl.Invalidate(Rect rect) => InvalidateVisual(); + + void ITopLevelImpl.SetInputRoot(IInputRoot inputRoot) => _inputRoot = inputRoot; + + Point ITopLevelImpl.PointToClient(Point point) => PointFromScreen(point.ToWpfPoint()).ToAvaloniaPoint(); + + Point ITopLevelImpl.PointToScreen(Point point) => PointToScreen(point.ToWpfPoint()).ToAvaloniaPoint(); + + protected override void OnLostFocus(RoutedEventArgs e) => LostFocus?.Invoke(); + + + InputModifiers GetModifiers() + { + var state = Keyboard.Modifiers; + var rv = default(InputModifiers); + if (state.HasFlag(ModifierKeys.Windows)) + rv |= InputModifiers.Windows; + if (state.HasFlag(ModifierKeys.Alt)) + rv |= InputModifiers.Alt; + if (state.HasFlag(ModifierKeys.Control)) + rv |= InputModifiers.Control; + if (state.HasFlag(ModifierKeys.Shift)) + rv |= InputModifiers.Shift; + //TODO: mouse modifiers + + + return rv; + } + + void MouseEvent(RawMouseEventType type, MouseEventArgs e) + => _ttl.Input?.Invoke(new RawMouseEventArgs(_mouse, (uint)e.Timestamp, _inputRoot, type, + e.GetPosition(this).ToAvaloniaPoint(), GetModifiers())); + + protected override void OnMouseDown(MouseButtonEventArgs e) + { + RawMouseEventType type; + if(e.ChangedButton == MouseButton.Left) + type = RawMouseEventType.LeftButtonDown; + else if (e.ChangedButton == MouseButton.Middle) + type = RawMouseEventType.MiddleButtonDown; + else if (e.ChangedButton == MouseButton.Right) + type = RawMouseEventType.RightButtonDown; + else + return; + MouseEvent(type, e); + Focus(); + } + + protected override void OnMouseUp(MouseButtonEventArgs e) + { + RawMouseEventType type; + if (e.ChangedButton == MouseButton.Left) + type = RawMouseEventType.LeftButtonUp; + else if (e.ChangedButton == MouseButton.Middle) + type = RawMouseEventType.MiddleButtonUp; + else if (e.ChangedButton == MouseButton.Right) + type = RawMouseEventType.RightButtonUp; + else + return; + MouseEvent(type, e); + Focus(); + } + + protected override void OnMouseMove(MouseEventArgs e) + { + MouseEvent(RawMouseEventType.Move, e); + } + + protected override void OnMouseWheel(MouseWheelEventArgs e) => + _ttl.Input?.Invoke(new RawMouseWheelEventArgs(_mouse, (uint) e.Timestamp, _inputRoot, + e.GetPosition(this).ToAvaloniaPoint(), new Vector(0, e.Delta), GetModifiers())); + + protected override void OnMouseLeave(MouseEventArgs e) => MouseEvent(RawMouseEventType.LeaveWindow, e); + + protected override void OnKeyDown(KeyEventArgs e) + => _ttl.Input?.Invoke(new RawKeyEventArgs(_keyboard, (uint) e.Timestamp, RawKeyEventType.KeyDown, + (Key) e.Key, + GetModifiers())); + + protected override void OnKeyUp(KeyEventArgs e) + => _ttl.Input?.Invoke(new RawKeyEventArgs(_keyboard, (uint)e.Timestamp, RawKeyEventType.KeyUp, + (Key)e.Key, + GetModifiers())); + + protected override void OnTextInput(TextCompositionEventArgs e) + => _ttl.Input?.Invoke(new RawTextInputEventArgs(_keyboard, (uint) e.Timestamp, e.Text)); + + void ITopLevelImpl.SetCursor(IPlatformHandle cursor) + { + if (cursor == null) + Cursor = Cursors.Arrow; + else if (cursor.HandleDescriptor == "HCURSOR") + Cursor = CursorShim.FromHCursor(cursor.Handle); + } + + Action ITopLevelImpl.Input { get; set; } //TODO + Action ITopLevelImpl.Paint { get; set; } + Action ITopLevelImpl.Resized { get; set; } + Action ITopLevelImpl.ScalingChanged { get; set; } + Action ITopLevelImpl.Closed { get; set; } + public new event Action LostFocus; + + internal Vector GetScaling() + { + var src = PresentationSource.FromVisual(this)?.CompositionTarget; + if (src == null) + return new Vector(1, 1); + return new Vector(src.TransformToDevice.M11, src.TransformToDevice.M22); + } + } +} diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WritableBitmapSurface.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WritableBitmapSurface.cs new file mode 100644 index 0000000000..0f8752fb8d --- /dev/null +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WritableBitmapSurface.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Platform; +using PixelFormat = Avalonia.Platform.PixelFormat; + +namespace Avalonia.Win32.Interop.Wpf +{ + class WritableBitmapSurface : IFramebufferPlatformSurface + { + private readonly WpfTopLevelImpl _impl; + private WriteableBitmap _bitmap; + public WritableBitmapSurface(WpfTopLevelImpl impl) + { + _impl = impl; + } + + public ILockedFramebuffer Lock() + { + var scale = _impl.GetScaling(); + var size = new Size(_impl.ActualWidth * scale.X, _impl.ActualHeight * scale.Y); + var dpi = scale * 96; + if (_bitmap == null || _bitmap.PixelWidth != (int) size.Width || _bitmap.PixelHeight != (int) size.Height) + { + _bitmap = new WriteableBitmap((int) size.Width, (int) size.Height, dpi.X, dpi.Y, + PixelFormats.Bgra32, null); + } + return new LockedFramebuffer(_impl, _bitmap, dpi); + } + + internal class LockedFramebuffer : ILockedFramebuffer + { + private readonly WpfTopLevelImpl _impl; + private readonly WriteableBitmap _bitmap; + + public LockedFramebuffer(WpfTopLevelImpl impl, WriteableBitmap bitmap, Vector dpi) + { + _impl = impl; + _bitmap = bitmap; + Dpi = dpi; + _bitmap.Lock(); + } + + public void Dispose() + { + _bitmap.AddDirtyRect(new Int32Rect(0, 0, _bitmap.PixelWidth, _bitmap.PixelHeight)); + _bitmap.Unlock(); + /* + using (var fileStream = new FileStream("c:\\tools\\wat.png", FileMode.Create)) + { + BitmapEncoder encoder = new PngBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create(_bitmap)); + encoder.Save(fileStream); + }*/ + _impl.ImageSource = _bitmap; + } + + public IntPtr Address => _bitmap.BackBuffer; + public int Width => _bitmap.PixelWidth; + public int Height => _bitmap.PixelHeight; + public int RowBytes => _bitmap.BackBufferStride; + public Vector Dpi { get; } + public PixelFormat Format => PixelFormat.Bgra8888; + } + } +} diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index 41728ee486..ea742f8911 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -57,7 +57,6 @@ Component - diff --git a/src/Windows/Avalonia.Win32/Embedding/WpfAvaloniaControlHost.cs b/src/Windows/Avalonia.Win32/Embedding/WpfAvaloniaControlHost.cs deleted file mode 100644 index 663f6906ed..0000000000 --- a/src/Windows/Avalonia.Win32/Embedding/WpfAvaloniaControlHost.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Forms.Integration; -using System.Windows.Interop; -using Avalonia.Controls; -using Avalonia.Win32.Interop; - -namespace Avalonia.Win32.Embedding -{ - public class WpfAvaloniaControlHost : HwndHost - { - private WinFormsAvaloniaControlHost _host; - private Avalonia.Controls.Control _content; - - public Avalonia.Controls.Control Content - { - get { return _content; } - set - { - if (_host != null) - _host.Content = value; - _content = value; - - } - } - - void DestroyHost() - { - _host?.Dispose(); - _host = null; - } - - protected override HandleRef BuildWindowCore(HandleRef hwndParent) - { - DestroyHost(); - _host = new WinFormsAvaloniaControlHost {Content = _content}; - UnmanagedMethods.SetParent(_host.Handle, hwndParent.Handle); - return new HandleRef(this, _host.Handle); - } - - protected override void DestroyWindowCore(HandleRef hwnd) - { - DestroyHost(); - } - } -} diff --git a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs index a55c808415..3d8d04d6cc 100644 --- a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs +++ b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs @@ -10,7 +10,7 @@ namespace Avalonia.Win32.Input { class WindowsMouseDevice : MouseDevice { - public new static WindowsMouseDevice Instance { get; } = new WindowsMouseDevice(); + public static WindowsMouseDevice Instance { get; } = new WindowsMouseDevice(); public WindowImpl CurrentWindow { diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index b6cfb03221..e02c67c0e6 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -66,7 +66,6 @@ namespace Avalonia.Win32 .Bind().ToSingleton() .Bind().ToConstant(CursorFactory.Instance) .Bind().ToConstant(WindowsKeyboardDevice.Instance) - .Bind().ToConstant(WindowsMouseDevice.Instance) .Bind().ToConstant(s_instance) .Bind().ToConstant(s_instance) .Bind().ToConstant(new RenderLoop(60)) diff --git a/src/Windows/Avalonia.Win32/WindowFramebuffer.cs b/src/Windows/Avalonia.Win32/WindowFramebuffer.cs index fe4fe5c668..df238c919e 100644 --- a/src/Windows/Avalonia.Win32/WindowFramebuffer.cs +++ b/src/Windows/Avalonia.Win32/WindowFramebuffer.cs @@ -39,7 +39,7 @@ namespace Avalonia.Win32 public int RowBytes => Width * 4; public PixelFormat Format => PixelFormat.Bgra8888; - public Size Dpi + public Vector Dpi { get { @@ -56,10 +56,10 @@ namespace Avalonia.Win32 out dpix, out dpiy) == 0) { - return new Size(dpix, dpiy); + return new Vector(dpix, dpiy); } } - return new Size(96, 96); + return new Vector(96, 96); } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index f2d7e0e043..f1537e53f5 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -133,6 +133,8 @@ namespace Avalonia.Win32 } } + public IMouseDevice MouseDevice => WindowsMouseDevice.Instance; + public WindowState WindowState { get diff --git a/src/iOS/Avalonia.iOS/EmulatedFramebuffer.cs b/src/iOS/Avalonia.iOS/EmulatedFramebuffer.cs index f3fc90a2ab..58cf6edd78 100644 --- a/src/iOS/Avalonia.iOS/EmulatedFramebuffer.cs +++ b/src/iOS/Avalonia.iOS/EmulatedFramebuffer.cs @@ -24,7 +24,7 @@ namespace Avalonia.iOS Width = (int) frame.Width * factor; Height = (int) frame.Height * factor; RowBytes = Width * 4; - Dpi = new Size(96, 96) * factor; + Dpi = new Vector(96, 96) * factor; Format = PixelFormat.Rgba8888; Address = Marshal.AllocHGlobal(Height * RowBytes); } @@ -53,7 +53,7 @@ namespace Avalonia.iOS public int Width { get; } public int Height { get; } public int RowBytes { get; } - public Size Dpi { get; } + public Vector Dpi { get; } public PixelFormat Format { get; } } } diff --git a/src/iOS/Avalonia.iOS/TopLevelImpl.cs b/src/iOS/Avalonia.iOS/TopLevelImpl.cs index 7949e331fe..cf4801dbb7 100644 --- a/src/iOS/Avalonia.iOS/TopLevelImpl.cs +++ b/src/iOS/Avalonia.iOS/TopLevelImpl.cs @@ -61,6 +61,8 @@ namespace Avalonia.iOS public Size ClientSize => Bounds.Size.ToAvalonia(); + public IMouseDevice MouseDevice => iOSPlatform.MouseDevice; + public override void Draw(CGRect rect) { Paint?.Invoke(new Rect(rect.X, rect.Y, rect.Width, rect.Height)); diff --git a/src/iOS/Avalonia.iOS/iOSPlatform.cs b/src/iOS/Avalonia.iOS/iOSPlatform.cs index 6d6e1fab03..4c4e497cd7 100644 --- a/src/iOS/Avalonia.iOS/iOSPlatform.cs +++ b/src/iOS/Avalonia.iOS/iOSPlatform.cs @@ -57,7 +57,6 @@ namespace Avalonia.iOS //.Bind().ToTransient() .Bind().ToTransient() .Bind().ToConstant(KeyboardDevice) - .Bind().ToConstant(MouseDevice) .Bind().ToConstant(ImmediateRenderer.Factory) .Bind().ToSingleton() .Bind().ToConstant(PlatformThreadingInterface.Instance) diff --git a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj index d55dc8d544..c656801d90 100644 --- a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj +++ b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj @@ -1,6 +1,7 @@  net461;netcoreapp1.1 + Library diff --git a/tests/Avalonia.Base.UnitTests/Properties/AssemblyInfo.cs b/tests/Avalonia.Base.UnitTests/Properties/AssemblyInfo.cs index 8e5e3a305b..4b93ea8400 100644 --- a/tests/Avalonia.Base.UnitTests/Properties/AssemblyInfo.cs +++ b/tests/Avalonia.Base.UnitTests/Properties/AssemblyInfo.cs @@ -7,4 +7,4 @@ using Xunit; [assembly: AssemblyTitle("Avalonia.UnitTests")] // Don't run tests in parallel. -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index be6480618b..1d987e2238 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -49,6 +49,7 @@ + @@ -100,6 +101,6 @@ - + \ No newline at end of file diff --git a/tests/Avalonia.Benchmarks/Layout/Measure.cs b/tests/Avalonia.Benchmarks/Layout/Measure.cs new file mode 100644 index 0000000000..d1fdae9971 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Layout/Measure.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.UnitTests; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Layout +{ + [MemoryDiagnoser] + public class Measure : IDisposable + { + private IDisposable _app; + private TestRoot root; + private List controls = new List(); + + public Measure() + { + _app = UnitTestApplication.Start(TestServices.RealLayoutManager); + + var panel = new StackPanel(); + root = new TestRoot { Child = panel }; + controls.Add(panel); + CreateChildren(panel, 3, 5); + LayoutManager.Instance.ExecuteInitialLayoutPass(root); + } + + public void Dispose() + { + _app.Dispose(); + } + + [Benchmark] + public void Remeasure_Half() + { + var random = new Random(1); + + foreach (var control in controls) + { + if (random.Next(2) == 0) + { + control.InvalidateMeasure(); + } + } + + LayoutManager.Instance.ExecuteLayoutPass(); + } + + private void CreateChildren(IPanel parent, int childCount, int iterations) + { + for (var i = 0; i < childCount; ++i) + { + var control = new StackPanel(); + parent.Children.Add(control); + + if (iterations > 0) + { + CreateChildren(control, childCount, iterations - 1); + } + + controls.Add(control); + } + } + } +} diff --git a/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs b/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs index 0af451efd2..33af55fdf9 100644 --- a/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs +++ b/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs @@ -11,6 +11,7 @@ using Avalonia.VisualTree; namespace Avalonia.Benchmarks.Styling { + [MemoryDiagnoser] public class ApplyStyling : IDisposable { private IDisposable _app; diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj index f8235f7d68..957cdd7036 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -1,12 +1,16 @@  net461;netcoreapp1.1 + Library + + + diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs new file mode 100644 index 0000000000..e32c703409 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs @@ -0,0 +1,291 @@ +// 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.Linq; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.LogicalTree; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Moq; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Presenters +{ + /// + /// Tests for ContentControls that are hosted in a control template. + /// + public class ContentPresenterTests_InTemplate + { + [Fact] + public void Should_Register_With_Host_When_TemplatedParent_Set() + { + var host = new Mock(); + var target = new ContentPresenter(); + + target.SetValue(Control.TemplatedParentProperty, host.Object); + + host.Verify(x => x.RegisterContentPresenter(target)); + } + + [Fact] + public void Setting_Content_To_Control_Should_Set_Child() + { + var (target, _) = CreateTarget(); + var child = new Border(); + + target.Content = child; + + Assert.Equal(child, target.Child); + } + + [Fact] + public void Setting_Content_To_Control_Should_Update_Logical_Tree() + { + var (target, parent) = CreateTarget(); + var child = new Border(); + + target.Content = child; + + Assert.Equal(parent, child.GetLogicalParent()); + Assert.Equal(new[] { child }, parent.GetLogicalChildren()); + } + + [Fact] + public void Setting_Content_To_Control_Should_Update_Visual_Tree() + { + var (target, _) = CreateTarget(); + var child = new Border(); + + target.Content = child; + + Assert.Equal(target, child.GetVisualParent()); + Assert.Equal(new[] { child }, target.GetVisualChildren()); + } + + [Fact] + public void Setting_Content_To_String_Should_Create_TextBlock() + { + var (target, _) = CreateTarget(); + + target.Content = "Foo"; + + Assert.IsType(target.Child); + Assert.Equal("Foo", ((TextBlock)target.Child).Text); + } + + [Fact] + public void Setting_Content_To_String_Should_Update_Logical_Tree() + { + var (target, parent) = CreateTarget(); + + target.Content = "Foo"; + + var child = target.Child; + Assert.Equal(parent, child.GetLogicalParent()); + Assert.Equal(new[] { child }, parent.GetLogicalChildren()); + } + + [Fact] + public void Setting_Content_To_String_Should_Update_Visual_Tree() + { + var (target, _) = CreateTarget(); + + target.Content = "Foo"; + + var child = target.Child; + Assert.Equal(target, child.GetVisualParent()); + Assert.Equal(new[] { child }, target.GetVisualChildren()); + } + + [Fact] + public void Clearing_Control_Content_Should_Update_Logical_Tree() + { + var (target, _) = CreateTarget(); + var child = new Border(); + + target.Content = child; + target.Content = null; + + Assert.Equal(null, child.GetLogicalParent()); + Assert.Empty(target.GetLogicalChildren()); + } + + [Fact] + public void Clearing_Control_Content_Should_Update_Visual_Tree() + { + var (target, _) = CreateTarget(); + var child = new Border(); + + target.Content = child; + target.Content = null; + + Assert.Equal(null, child.GetVisualParent()); + Assert.Empty(target.GetVisualChildren()); + } + + [Fact] + public void Control_Content_Should_Not_Be_NameScope() + { + var (target, _) = CreateTarget(); + + target.Content = new TextBlock(); + + Assert.IsType(target.Child); + Assert.Null(NameScope.GetNameScope((Control)target.Child)); + } + + [Fact] + public void DataTemplate_Created_Control_Should_Be_NameScope() + { + var (target, _) = CreateTarget(); + + target.Content = "Foo"; + + Assert.IsType(target.Child); + Assert.NotNull(NameScope.GetNameScope((Control)target.Child)); + } + + [Fact] + public void Assigning_Control_To_Content_Should_Not_Set_DataContext() + { + var (target, _) = CreateTarget(); + target.Content = new Border(); + + Assert.False(target.IsSet(Control.DataContextProperty)); + } + + [Fact] + public void Assigning_NonControl_To_Content_Should_Set_DataContext_On_UpdateChild() + { + var (target, _) = CreateTarget(); + target.Content = "foo"; + + Assert.Equal("foo", target.DataContext); + } + + [Fact] + public void Should_Use_ContentTemplate_If_Specified() + { + var (target, _) = CreateTarget(); + + target.ContentTemplate = new FuncDataTemplate(_ => new Canvas()); + target.Content = "Foo"; + + Assert.IsType(target.Child); + } + + [Fact] + public void Should_Update_If_ContentTemplate_Changed() + { + var (target, _) = CreateTarget(); + + target.Content = "Foo"; + Assert.IsType(target.Child); + + target.ContentTemplate = new FuncDataTemplate(_ => new Canvas()); + Assert.IsType(target.Child); + + target.ContentTemplate = null; + Assert.IsType(target.Child); + } + + [Fact] + public void Assigning_Control_To_Content_After_NonControl_Should_Clear_DataContext() + { + var (target, _) = CreateTarget(); + + target.Content = "foo"; + + Assert.True(target.IsSet(Control.DataContextProperty)); + + target.Content = new Border(); + + Assert.False(target.IsSet(Control.DataContextProperty)); + } + + [Fact] + public void Recycles_DataTemplate() + { + var (target, _) = CreateTarget(); + target.DataTemplates.Add(new FuncDataTemplate(_ => new Border(), true)); + + target.Content = "foo"; + + var control = target.Child; + Assert.IsType(control); + + target.Content = "bar"; + Assert.Same(control, target.Child); + } + + [Fact] + public void Detects_DataTemplate_Doesnt_Match_And_Doesnt_Recycle() + { + var (target, _) = CreateTarget(); + target.DataTemplates.Add(new FuncDataTemplate(x => x == "foo", _ => new Border(), true)); + + target.Content = "foo"; + + var control = target.Child; + Assert.IsType(control); + + target.Content = "bar"; + Assert.IsType(target.Child); + } + + [Fact] + public void Detects_DataTemplate_Doesnt_Support_Recycling() + { + var (target, _) = CreateTarget(); + target.DataTemplates.Add(new FuncDataTemplate(_ => new Border(), false)); + + target.Content = "foo"; + + var control = target.Child; + Assert.IsType(control); + + target.Content = "bar"; + Assert.NotSame(control, target.Child); + } + + [Fact] + public void Reevaluates_DataTemplates_When_Recycling() + { + var (target, _) = CreateTarget(); + + target.DataTemplates.Add(new FuncDataTemplate(x => x == "bar", _ => new Canvas(), true)); + target.DataTemplates.Add(new FuncDataTemplate(_ => new Border(), true)); + + target.Content = "foo"; + + var control = target.Child; + Assert.IsType(control); + + target.Content = "bar"; + Assert.IsType(target.Child); + } + + (ContentPresenter presenter, ContentControl templatedParent) CreateTarget() + { + var templatedParent = new ContentControl + { + Template = new FuncControlTemplate(x => + new ContentPresenter + { + Name = "PART_ContentPresenter", + }), + }; + var root = new TestRoot { Child = templatedParent }; + + templatedParent.ApplyTemplate(); + + return ((ContentPresenter)templatedParent.Presenter, templatedParent); + } + + private class TestContentControl : ContentControl + { + public IControl Child { get; set; } + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs similarity index 52% rename from tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs rename to tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs index 88d26334ed..589b1d67d2 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs @@ -15,91 +15,13 @@ using Xunit; namespace Avalonia.Controls.UnitTests.Presenters { - public class ContentPresenterTests + /// + /// Tests for ContentControls that aren't hosted in a control template. + /// + public class ContentPresenterTests_Standalone { [Fact] - public void Should_Register_With_Host_When_TemplatedParent_Set() - { - var host = new Mock(); - var target = new ContentPresenter(); - - target.SetValue(Control.TemplatedParentProperty, host.Object); - - host.Verify(x => x.RegisterContentPresenter(target)); - } - - [Fact] - public void Setting_Content_To_Control_Should_Set_Child() - { - var target = new ContentPresenter(); - var child = new Border(); - - target.Content = child; - - Assert.Null(target.Child); - target.UpdateChild(); - Assert.Equal(child, target.Child); - } - - [Fact] - public void Setting_Content_To_String_Should_Create_TextBlock() - { - var target = new ContentPresenter(); - - target.Content = "Foo"; - - Assert.Null(target.Child); - target.UpdateChild(); - Assert.IsType(target.Child); - Assert.Equal("Foo", ((TextBlock)target.Child).Text); - } - - [Fact] - public void Control_Content_Should_Not_Be_NameScope() - { - var target = new ContentPresenter(); - - target.Content = new TextBlock(); - - Assert.Null(target.Child); - target.UpdateChild(); - Assert.IsType(target.Child); - Assert.Null(NameScope.GetNameScope((Control)target.Child)); - } - - [Fact] - public void DataTemplate_Created_Control_Should_Be_NameScope() - { - var target = new ContentPresenter(); - - target.Content = "Foo"; - - Assert.Null(target.Child); - target.UpdateChild(); - Assert.IsType(target.Child); - Assert.NotNull(NameScope.GetNameScope((Control)target.Child)); - } - - [Fact] - public void Should_Set_Childs_Parent_To_TemplatedParent() - { - var content = new Border(); - var target = new TestContentControl - { - Template = new FuncControlTemplate(parent => - new ContentPresenter { Content = parent.Child }), - Child = content, - }; - - target.ApplyTemplate(); - var presenter = ((ContentPresenter)target.GetVisualChildren().Single()); - presenter.UpdateChild(); - - Assert.Same(target, content.Parent); - } - - [Fact] - public void Should_Set_Childs_Parent_To_Itself_Outside_Template() + public void Should_Set_Childs_Parent_To_Itself_Standalone() { var content = new Border(); var target = new ContentPresenter { Content = content }; @@ -110,7 +32,7 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void Should_Add_Child_To_Own_LogicalChildren_Outside_Template() + public void Should_Add_Child_To_Own_LogicalChildren_Standalone() { var content = new Border(); var target = new ContentPresenter { Content = content }; @@ -124,94 +46,7 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void Adding_To_Logical_Tree_Should_Reevaluate_DataTemplates() - { - var target = new ContentPresenter - { - Content = "Foo", - }; - - target.UpdateChild(); - Assert.IsType(target.Child); - - var root = new TestRoot - { - DataTemplates = new DataTemplates - { - new FuncDataTemplate(x => new Decorator()), - }, - }; - - root.Child = target; - target.ApplyTemplate(); - Assert.IsType(target.Child); - } - - [Fact] - public void Assigning_Control_To_Content_Should_Not_Set_DataContext() - { - var target = new ContentPresenter - { - Content = new Border(), - }; - - Assert.False(target.IsSet(Control.DataContextProperty)); - } - - [Fact] - public void Assigning_NonControl_To_Content_Should_Set_DataContext_On_UpdateChild() - { - var target = new ContentPresenter - { - Content = "foo", - }; - - target.UpdateChild(); - - Assert.Equal("foo", target.DataContext); - } - - [Fact] - public void Assigning_Control_To_Content_After_NonControl_Should_Clear_DataContext() - { - var target = new ContentPresenter(); - - target.Content = "foo"; - target.UpdateChild(); - - Assert.True(target.IsSet(Control.DataContextProperty)); - - target.Content = new Border(); - target.UpdateChild(); - - Assert.False(target.IsSet(Control.DataContextProperty)); - } - - [Fact] - public void Tries_To_Recycle_DataTemplate() - { - var target = new ContentPresenter - { - DataTemplates = new DataTemplates - { - new FuncDataTemplate(_ => new Border(), true), - }, - Content = "foo", - }; - - target.UpdateChild(); - var control = target.Child; - - Assert.IsType(control); - - target.Content = "bar"; - target.UpdateChild(); - - Assert.Same(control, target.Child); - } - - [Fact] - public void Should_Raise_DetachedFromLogicalTree_On_Content_Changed_OutsideTemplate() + public void Should_Raise_DetachedFromLogicalTree_On_Content_Changed_Standalone() { var target = new ContentPresenter { @@ -250,7 +85,7 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void Should_Raise_DetachedFromLogicalTree_In_ContentControl_On_Content_Changed_OutsideTemplate() + public void Should_Raise_DetachedFromLogicalTree_In_ContentControl_On_Content_Changed_Standalone() { var contentControl = new ContentControl { @@ -292,13 +127,14 @@ namespace Avalonia.Controls.UnitTests.Presenters var tbbar = target.Child as ContentControl; Assert.NotNull(tbbar); + Assert.True(tbbar != tbfoo); Assert.False((tbfoo as IControl).IsAttachedToLogicalTree); Assert.True(foodetached); } [Fact] - public void Should_Raise_DetachedFromLogicalTree_On_Detached_OutsideTemplate() + public void Should_Raise_DetachedFromLogicalTree_On_Detached_Standalone() { var target = new ContentPresenter { @@ -332,7 +168,7 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void Should_Remove_Old_Child_From_LogicalChildren_On_ContentChanged_OutsideTemplate() + public void Should_Remove_Old_Child_From_LogicalChildren_On_ContentChanged_Standalone() { var target = new ContentPresenter { @@ -363,9 +199,5 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.NotEqual(foo, logicalChildren.First()); } - private class TestContentControl : ContentControl - { - public IControl Child { get; set; } - } } } \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs new file mode 100644 index 0000000000..3585109dee --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs @@ -0,0 +1,102 @@ +// 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.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Presenters +{ + /// + /// Tests for ContentControls that are not attached to a logical tree. + /// + public class ContentPresenterTests_Unrooted + { + [Fact] + public void Setting_Content_To_Control_Should_Not_Set_Child_Unless_UpdateChild_Called() + { + var target = new ContentPresenter(); + var child = new Border(); + + target.Content = child; + Assert.Null(target.Child); + + target.ApplyTemplate(); + Assert.Null(target.Child); + + target.UpdateChild(); + Assert.Equal(child, target.Child); + } + + [Fact] + public void Setting_Content_To_String_Should_Not_Create_TextBlock_Unless_UpdateChild_Called() + { + var target = new ContentPresenter(); + + target.Content = "Foo"; + Assert.Null(target.Child); + + target.ApplyTemplate(); + Assert.Null(target.Child); + + target.UpdateChild(); + Assert.IsType(target.Child); + Assert.Equal("Foo", ((TextBlock)target.Child).Text); + } + + [Fact] + public void Clearing_Control_Content_Should_Remove_Child_Immediately() + { + var target = new ContentPresenter(); + var child = new Border(); + + target.Content = child; + target.UpdateChild(); + Assert.Equal(child, target.Child); + + target.Content = null; + Assert.Null(target.Child); + } + + [Fact] + public void Clearing_String_Content_Should_Remove_Child_Immediately() + { + var target = new ContentPresenter(); + + target.Content = "Foo"; + target.UpdateChild(); + Assert.IsType(target.Child); + + target.Content = null; + Assert.Null(target.Child); + } + + [Fact] + public void Adding_To_Logical_Tree_Should_Reevaluate_DataTemplates() + { + var root = new TestRoot(); + var target = new ContentPresenter(); + + target.Content = "Foo"; + Assert.Null(target.Child); + + root.Child = target; + target.ApplyTemplate(); + Assert.IsType(target.Child); + + root.Child = null; + root = new TestRoot + { + DataTemplates = new DataTemplates + { + new FuncDataTemplate(x => new Decorator()), + }, + }; + + root.Child = target; + target.ApplyTemplate(); + Assert.IsType(target.Child); + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs new file mode 100644 index 0000000000..64344a1584 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -0,0 +1,109 @@ +// 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.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.LogicalTree; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Primitives +{ + public class PopupRootTests + { + [Fact] + public void PopupRoot_IsAttachedToLogicalTree_Is_True() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = CreateTarget(); + + Assert.True(((ILogical)target).IsAttachedToLogicalTree); + } + } + + [Fact] + public void Templated_Child_IsAttachedToLogicalTree_Is_True() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = CreateTarget(); + + Assert.True(target.Presenter.IsAttachedToLogicalTree); + } + } + + [Fact] + public void Attaching_PopupRoot_To_Parent_Logical_Tree_Raises_DetachedFromLogicalTree_And_AttachedToLogicalTree() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var child = new Decorator(); + var target = CreateTarget(); + var window = new Window(); + var detachedCount = 0; + var attachedCount = 0; + + target.Content = child; + + target.DetachedFromLogicalTree += (s, e) => ++detachedCount; + child.DetachedFromLogicalTree += (s, e) => ++detachedCount; + target.AttachedToLogicalTree += (s, e) => ++attachedCount; + child.AttachedToLogicalTree += (s, e) => ++attachedCount; + + ((ISetLogicalParent)target).SetParent(window); + + Assert.Equal(2, detachedCount); + Assert.Equal(2, attachedCount); + } + } + + [Fact] + public void Detaching_PopupRoot_From_Parent_Logical_Tree_Raises_DetachedFromLogicalTree_And_AttachedToLogicalTree() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var child = new Decorator(); + var target = CreateTarget(); + var window = new Window(); + var detachedCount = 0; + var attachedCount = 0; + + target.Content = child; + ((ISetLogicalParent)target).SetParent(window); + + target.DetachedFromLogicalTree += (s, e) => ++detachedCount; + child.DetachedFromLogicalTree += (s, e) => ++detachedCount; + target.AttachedToLogicalTree += (s, e) => ++attachedCount; + child.AttachedToLogicalTree += (s, e) => ++attachedCount; + + ((ISetLogicalParent)target).SetParent(null); + + // Despite being detached from the parent logical tree, we're still attached to a + // logical tree as PopupRoot itself is a logical tree root. + Assert.True(((ILogical)target).IsAttachedToLogicalTree); + Assert.True(((ILogical)child).IsAttachedToLogicalTree); + Assert.Equal(2, detachedCount); + Assert.Equal(2, attachedCount); + } + } + + private PopupRoot CreateTarget() + { + var result = new PopupRoot + { + Template = new FuncControlTemplate(_ => + new ContentPresenter + { + Name = "PART_ContentPresenter", + }), + }; + + result.ApplyTemplate(); + + return result; + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs index 636492ed1c..72c8073f21 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs @@ -527,6 +527,34 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void Moving_To_New_LogicalTree_Should_Detach_Attach_Template_Child() + { + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + TestTemplatedControl target; + var root = new TestRoot + { + Child = target = new TestTemplatedControl + { + Template = new FuncControlTemplate(_ => new Decorator()), + } + }; + + Assert.NotNull(target.Template); + target.ApplyTemplate(); + + var templateChild = (ILogical)target.GetVisualChildren().Single(); + Assert.True(templateChild.IsAttachedToLogicalTree); + + root.Child = null; + Assert.False(templateChild.IsAttachedToLogicalTree); + + var newRoot = new TestRoot { Child = target }; + Assert.True(templateChild.IsAttachedToLogicalTree); + } + } + private static IControl ScrollingContentControlTemplate(ContentControl control) { return new Border diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 5cd3c57e2e..da30336be6 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -2,24 +2,33 @@ // 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.Subjects; -using Moq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Layout; +using Avalonia.LogicalTree; using Avalonia.Platform; -using Avalonia.Rendering; -using Avalonia.Styling; using Avalonia.UnitTests; +using Moq; using Xunit; namespace Avalonia.Controls.UnitTests { public class TopLevelTests { + [Fact] + public void IsAttachedToLogicalTree_Is_True() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var impl = new Mock(); + var target = new TestTopLevel(impl.Object); + + Assert.True(((ILogical)target).IsAttachedToLogicalTree); + } + } + [Fact] public void ClientSize_Should_Be_Set_On_Construction() { diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 52d36a33fa..44ef7192ff 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -315,6 +315,50 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new[] { "NewChild1" }, ExtractItemHeader(target, 1)); } + [Fact] + public void Keyboard_Navigation_Should_Move_To_Last_Selected_Node() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var focus = FocusManager.Instance; + var navigation = AvaloniaLocator.Current.GetService(); + var data = CreateTestTreeData(); + + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = data, + DataTemplates = CreateNodeDataTemplate(), + }; + + var button = new Button(); + + var root = new TestRoot + { + Child = new StackPanel + { + Children = { target, button }, + } + }; + + ApplyTemplates(target); + + var item = data[0].Children[0]; + var node = target.ItemContainerGenerator.Index.ContainerFromItem(item); + Assert.NotNull(node); + + target.SelectedItem = item; + node.Focus(); + Assert.Same(node, focus.Current); + + navigation.Move(focus.Current, NavigationDirection.Next); + Assert.Same(button, focus.Current); + + navigation.Move(focus.Current, NavigationDirection.Next); + Assert.Same(node, focus.Current); + } + } + private void ApplyTemplates(TreeView tree) { tree.ApplyTemplate(); diff --git a/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj b/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj index d35542b51f..8dd8faf9db 100644 --- a/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj +++ b/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj @@ -1,6 +1,7 @@  net461;netcoreapp1.1 + Library diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs new file mode 100644 index 0000000000..a090dcd18d --- /dev/null +++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs @@ -0,0 +1,214 @@ +// 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 Xunit; + +namespace Avalonia.Input.UnitTests +{ + public class KeyboardNavigationTests_Custom + { + [Fact] + public void Tab_Should_Custom_Navigate_Within_Children() + { + Button current; + Button next; + var target = new CustomNavigatingStackPanel + { + Children = + { + (current = new Button { Content = "Button 1" }), + new Button { Content = "Button 2" }, + (next = new Button { Content = "Button 3" }), + }, + NextControl = next, + }; + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); + + Assert.Same(next, result); + } + + [Fact] + public void Right_Should_Custom_Navigate_Within_Children() + { + Button current; + Button next; + var target = new CustomNavigatingStackPanel + { + Children = + { + (current = new Button { Content = "Button 1" }), + new Button { Content = "Button 2" }, + (next = new Button { Content = "Button 3" }), + }, + NextControl = next, + }; + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Right); + + Assert.Same(next, result); + } + + [Fact] + public void Tab_Should_Custom_Navigate_From_Outside() + { + Button current; + Button next; + var target = new CustomNavigatingStackPanel + { + Children = + { + new Button { Content = "Button 1" }, + new Button { Content = "Button 2" }, + (next = new Button { Content = "Button 3" }), + }, + NextControl = next, + }; + + var root = new StackPanel + { + Children = + { + (current = new Button { Content = "Outside" }), + target, + } + }; + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); + + Assert.Same(next, result); + } + + [Fact] + public void Tab_Should_Custom_Navigate_From_Outside_When_Wrapping() + { + Button current; + Button next; + var target = new CustomNavigatingStackPanel + { + Children = + { + new Button { Content = "Button 1" }, + new Button { Content = "Button 2" }, + (next = new Button { Content = "Button 3" }), + }, + NextControl = next, + }; + + var root = new StackPanel + { + Children = + { + target, + (current = new Button { Content = "Outside" }), + } + }; + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); + + Assert.Same(next, result); + } + + [Fact] + public void ShiftTab_Should_Custom_Navigate_From_Outside() + { + Button current; + Button next; + var target = new CustomNavigatingStackPanel + { + Children = + { + new Button { Content = "Button 1" }, + new Button { Content = "Button 2" }, + (next = new Button { Content = "Button 3" }), + }, + NextControl = next, + }; + + var root = new StackPanel + { + Children = + { + (current = new Button { Content = "Outside" }), + target, + } + }; + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); + + Assert.Same(next, result); + } + + [Fact] + public void Right_Should_Custom_Navigate_From_Outside() + { + Button current; + Button next; + var target = new CustomNavigatingStackPanel + { + Children = + { + new Button { Content = "Button 1" }, + new Button { Content = "Button 2" }, + (next = new Button { Content = "Button 3" }), + }, + NextControl = next, + }; + + var root = new StackPanel + { + Children = + { + (current = new Button { Content = "Outside" }), + target, + }, + [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, + }; + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Right); + + Assert.Same(next, result); + } + + [Fact] + public void Tab_Should_Navigate_Outside_When_Null_Returned_As_Next() + { + Button current; + Button next; + var target = new CustomNavigatingStackPanel + { + Children = + { + new Button { Content = "Button 1" }, + (current = new Button { Content = "Button 2" }), + new Button { Content = "Button 3" }, + }, + }; + + var root = new StackPanel + { + Children = + { + target, + (next = new Button { Content = "Outside" }), + } + }; + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); + + Assert.Same(next, result); + } + + private class CustomNavigatingStackPanel : StackPanel, ICustomKeyboardNavigation + { + public bool CustomNavigates { get; set; } = true; + public IInputElement NextControl { get; set; } + + public (bool handled, IInputElement next) GetNext(IInputElement element, NavigationDirection direction) + { + return (CustomNavigates, NextControl); + } + } + } +} diff --git a/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj b/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj index 8f9607fe67..86c9cf0617 100644 --- a/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj +++ b/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj @@ -1,6 +1,7 @@  net461;netcoreapp1.1 + Library diff --git a/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj b/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj index af33c80352..0950856dca 100644 --- a/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj +++ b/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj @@ -1,6 +1,7 @@  net461;netcoreapp1.1 + Library diff --git a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index f67c5a353f..361e7678be 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -2,25 +2,276 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using Avalonia.Controls; +using Avalonia.UnitTests; +using System; using Xunit; +using System.Collections.Generic; namespace Avalonia.Layout.UnitTests { public class LayoutManagerTests { [Fact] - public void Invalidating_Child_Should_Remeasure_Parent() + public void Measures_And_Arranges_InvalidateMeasured_Control() { - var layoutManager = new LayoutManager(); + var target = new LayoutManager(); - using (AvaloniaLocator.EnterScope()) + using (Start(target)) { - AvaloniaLocator.CurrentMutable.Bind().ToConstant(layoutManager); + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + + target.ExecuteInitialLayoutPass(root); + control.Measured = control.Arranged = false; + + control.InvalidateMeasure(); + target.ExecuteLayoutPass(); + + Assert.True(control.Measured); + Assert.True(control.Arranged); + } + } + + [Fact] + public void Arranges_InvalidateArranged_Control() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + + target.ExecuteInitialLayoutPass(root); + control.Measured = control.Arranged = false; + + control.InvalidateArrange(); + target.ExecuteLayoutPass(); + + Assert.False(control.Measured); + Assert.True(control.Arranged); + } + } + + [Fact] + public void Measures_Parent_Of_Newly_Added_Control() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot(); + + target.ExecuteInitialLayoutPass(root); + root.Child = control; + root.Measured = root.Arranged = false; + + target.ExecuteLayoutPass(); + + Assert.True(root.Measured); + Assert.True(root.Arranged); + Assert.True(control.Measured); + Assert.True(control.Arranged); + } + } + + [Fact] + public void Measures_In_Correct_Order() + { + var target = new LayoutManager(); + + using (Start(target)) + { + LayoutTestControl control1; + LayoutTestControl control2; + var root = new LayoutTestRoot + { + Child = control1 = new LayoutTestControl + { + Child = control2 = new LayoutTestControl(), + } + }; + + + var order = new List(); + Size MeasureOverride(ILayoutable control, Size size) + { + order.Add(control); + return new Size(10, 10); + } + + root.DoMeasureOverride = MeasureOverride; + control1.DoMeasureOverride = MeasureOverride; + control2.DoMeasureOverride = MeasureOverride; + target.ExecuteInitialLayoutPass(root); + + control2.InvalidateMeasure(); + control1.InvalidateMeasure(); + root.InvalidateMeasure(); + + order.Clear(); + target.ExecuteLayoutPass(); + + Assert.Equal(new ILayoutable[] { root, control1, control2 }, order); + } + } + + [Fact] + public void Measures_Root_And_Grandparent_In_Correct_Order() + { + var target = new LayoutManager(); + + using (Start(target)) + { + LayoutTestControl control1; + LayoutTestControl control2; + var root = new LayoutTestRoot + { + Child = control1 = new LayoutTestControl + { + Child = control2 = new LayoutTestControl(), + } + }; + + + var order = new List(); + Size MeasureOverride(ILayoutable control, Size size) + { + order.Add(control); + return new Size(10, 10); + } + + root.DoMeasureOverride = MeasureOverride; + control1.DoMeasureOverride = MeasureOverride; + control2.DoMeasureOverride = MeasureOverride; + target.ExecuteInitialLayoutPass(root); + + control2.InvalidateMeasure(); + root.InvalidateMeasure(); + + order.Clear(); + target.ExecuteLayoutPass(); + + Assert.Equal(new ILayoutable[] { root, control2 }, order); + } + } + + [Fact] + public void Doesnt_Measure_Non_Invalidated_Root() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + + target.ExecuteInitialLayoutPass(root); + root.Measured = root.Arranged = false; + control.Measured = control.Arranged = false; + + control.InvalidateMeasure(); + target.ExecuteLayoutPass(); + + Assert.False(root.Measured); + Assert.False(root.Arranged); + Assert.True(control.Measured); + Assert.True(control.Arranged); + } + } + + [Fact] + public void Doesnt_Measure_Removed_Control() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + + target.ExecuteInitialLayoutPass(root); + control.Measured = control.Arranged = false; + + control.InvalidateMeasure(); + root.Child = null; + target.ExecuteLayoutPass(); + + Assert.False(control.Measured); + Assert.False(control.Arranged); + } + } + + [Fact] + public void Measures_Root_With_Infinity() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var root = new LayoutTestRoot(); + var availableSize = default(Size); + + // Should not measure with this size. + root.MaxClientSize = new Size(123, 456); + + root.DoMeasureOverride = (_, s) => + { + availableSize = s; + return new Size(100, 100); + }; + + target.ExecuteInitialLayoutPass(root); + + Assert.Equal(Size.Infinity, availableSize); + } + } + + [Fact] + public void Arranges_Root_With_DesiredSize() + { + var target = new LayoutManager(); + + using (Start(target)) + { + var root = new LayoutTestRoot + { + Width = 100, + Height = 100, + }; + + var arrangeSize = default(Size); + + root.DoArrangeOverride = (_, s) => + { + arrangeSize = s; + return s; + }; + + target.ExecuteInitialLayoutPass(root); + Assert.Equal(new Size(100, 100), arrangeSize); + + root.Width = 120; + + target.ExecuteLayoutPass(); + Assert.Equal(new Size(120, 100), arrangeSize); + } + } + + [Fact] + public void Invalidating_Child_Remeasures_Parent() + { + var target = new LayoutManager(); + + using (Start(target)) + { + AvaloniaLocator.CurrentMutable.Bind().ToConstant(target); Border border; StackPanel panel; - var root = new TestLayoutRoot + var root = new LayoutTestRoot { Child = panel = new StackPanel { @@ -31,15 +282,22 @@ namespace Avalonia.Layout.UnitTests } }; - layoutManager.ExecuteInitialLayoutPass(root); + target.ExecuteInitialLayoutPass(root); Assert.Equal(new Size(0, 0), root.DesiredSize); border.Width = 100; border.Height = 100; - layoutManager.ExecuteLayoutPass(); + target.ExecuteLayoutPass(); Assert.Equal(new Size(100, 100), panel.DesiredSize); } } + + private IDisposable Start(LayoutManager layoutManager) + { + var result = AvaloniaLocator.EnterScope(); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(layoutManager); + return result; + } } } diff --git a/tests/Avalonia.Layout.UnitTests/LayoutTestControl.cs b/tests/Avalonia.Layout.UnitTests/LayoutTestControl.cs new file mode 100644 index 0000000000..f7f072eb1e --- /dev/null +++ b/tests/Avalonia.Layout.UnitTests/LayoutTestControl.cs @@ -0,0 +1,29 @@ +using System; +using Avalonia.Controls; + +namespace Avalonia.Layout.UnitTests +{ + internal class LayoutTestControl : Decorator + { + public bool Measured { get; set; } + public bool Arranged { get; set; } + public Func DoMeasureOverride { get; set; } + public Func DoArrangeOverride { get; set; } + + protected override Size MeasureOverride(Size availableSize) + { + Measured = true; + return DoMeasureOverride != null ? + DoMeasureOverride(this, availableSize) : + base.MeasureOverride(availableSize); + } + + protected override Size ArrangeOverride(Size finalSize) + { + Arranged = true; + return DoArrangeOverride != null ? + DoArrangeOverride(this, finalSize) : + base.ArrangeOverride(finalSize); + } + } +} diff --git a/tests/Avalonia.Layout.UnitTests/LayoutTestRoot.cs b/tests/Avalonia.Layout.UnitTests/LayoutTestRoot.cs new file mode 100644 index 0000000000..07476844e0 --- /dev/null +++ b/tests/Avalonia.Layout.UnitTests/LayoutTestRoot.cs @@ -0,0 +1,43 @@ +// 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.UnitTests; + +namespace Avalonia.Layout.UnitTests +{ + internal class LayoutTestRoot : TestRoot, ILayoutable + { + public bool Measured { get; set; } + public bool Arranged { get; set; } + public Func DoMeasureOverride { get; set; } + public Func DoArrangeOverride { get; set; } + + void ILayoutable.Measure(Size availableSize) + { + Measured = true; + Measure(availableSize); + } + + void ILayoutable.Arrange(Rect rect) + { + Arranged = true; + Arrange(rect); + } + + protected override Size MeasureOverride(Size availableSize) + { + return DoMeasureOverride != null ? + DoMeasureOverride(this, availableSize) : + base.MeasureOverride(availableSize); + } + + protected override Size ArrangeOverride(Size finalSize) + { + Arranged = true; + return DoArrangeOverride != null ? + DoArrangeOverride(this, finalSize) : + base.ArrangeOverride(finalSize); + } + } +} diff --git a/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs new file mode 100644 index 0000000000..dcc65edc74 --- /dev/null +++ b/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs @@ -0,0 +1,90 @@ +using System; +using Avalonia.Controls; +using Moq; +using Xunit; + +namespace Avalonia.Layout.UnitTests +{ + public class LayoutableTests + { + [Fact] + public void Only_Calls_LayoutManager_InvalidateMeasure_Once() + { + var target = new Mock(); + + using (Start(target.Object)) + { + var control = new Decorator(); + var root = new LayoutTestRoot { Child = control }; + + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + target.ResetCalls(); + + control.InvalidateMeasure(); + control.InvalidateMeasure(); + + target.Verify(x => x.InvalidateMeasure(control), Times.Once()); + } + } + + [Fact] + public void Only_Calls_LayoutManager_InvalidateArrange_Once() + { + var target = new Mock(); + + using (Start(target.Object)) + { + var control = new Decorator(); + var root = new LayoutTestRoot { Child = control }; + + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + target.ResetCalls(); + + control.InvalidateArrange(); + control.InvalidateArrange(); + + target.Verify(x => x.InvalidateArrange(control), Times.Once()); + } + } + + [Fact] + public void Attaching_Control_To_Tree_Invalidates_Parent_Measure() + { + var target = new Mock(); + + using (Start(target.Object)) + { + var control = new Decorator(); + var root = new LayoutTestRoot { Child = control }; + + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + Assert.True(control.IsMeasureValid); + + root.Child = null; + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + + Assert.False(control.IsMeasureValid); + Assert.True(root.IsMeasureValid); + + target.ResetCalls(); + + root.Child = control; + + Assert.False(root.IsMeasureValid); + Assert.False(control.IsMeasureValid); + target.Verify(x => x.InvalidateMeasure(root), Times.Once()); + } + } + + private IDisposable Start(ILayoutManager layoutManager) + { + var result = AvaloniaLocator.EnterScope(); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(layoutManager); + return result; + } + } +} diff --git a/tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs b/tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs deleted file mode 100644 index fab1647c5d..0000000000 --- a/tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs +++ /dev/null @@ -1,24 +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 Avalonia.Controls; - -namespace Avalonia.Layout.UnitTests -{ - internal class TestLayoutRoot : Decorator, ILayoutRoot - { - public TestLayoutRoot() - { - ClientSize = new Size(500, 500); - } - - public Size ClientSize - { - get; - set; - } - - public Size MaxClientSize => Size.Infinity; - public double LayoutScaling => 1; - } -} diff --git a/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj b/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj index b7c4811495..3ccd3da044 100644 --- a/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj +++ b/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj @@ -1,6 +1,7 @@  net461;netcoreapp1.1 + Library diff --git a/tests/Avalonia.Markup.UnitTests/ControlLocatorTests.cs b/tests/Avalonia.Markup.UnitTests/ControlLocatorTests.cs index b9e1cb353a..a5414f1e8c 100644 --- a/tests/Avalonia.Markup.UnitTests/ControlLocatorTests.cs +++ b/tests/Avalonia.Markup.UnitTests/ControlLocatorTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Reactive.Linq; +using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.UnitTests; using Xunit; @@ -13,7 +14,7 @@ namespace Avalonia.Markup.UnitTests public class ControlLocatorTests { [Fact] - public async void Track_By_Name_Should_Find_Control_Added_Earlier() + public async Task Track_By_Name_Should_Find_Control_Added_Earlier() { TextBlock target; TextBlock relativeTo; diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs index 062402d465..f5c0c6ec15 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.Reactive.Linq; using System.Threading; +using System.Threading.Tasks; using Avalonia.Data; using Avalonia.Markup.Data; using Avalonia.UnitTests; @@ -17,13 +18,15 @@ namespace Avalonia.Markup.UnitTests.Data public class BindingExpressionTests : IClassFixture { [Fact] - public async void Should_Get_Simple_Property_Value() + public async Task Should_Get_Simple_Property_Value() { var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string)); var result = await target.Take(1); Assert.Equal("foo", result); + + GC.KeepAlive(data); } [Fact] @@ -35,6 +38,8 @@ namespace Avalonia.Markup.UnitTests.Data target.OnNext("bar"); Assert.Equal("bar", data.StringValue); + + GC.KeepAlive(data); } [Fact] @@ -46,102 +51,87 @@ namespace Avalonia.Markup.UnitTests.Data target.OnNext("bar"); Assert.Equal("bar", data.Foo[0]); + + GC.KeepAlive(data); } [Fact] - public async void Should_Convert_Get_String_To_Double() + public async Task Should_Convert_Get_String_To_Double() { -#if NET461 - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; -#else - CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; -#endif var data = new Class1 { StringValue = "5.6" }; var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double)); var result = await target.Take(1); Assert.Equal(5.6, result); + + GC.KeepAlive(data); } [Fact] - public async void Getting_Invalid_Double_String_Should_Return_BindingError() + public async Task Getting_Invalid_Double_String_Should_Return_BindingError() { var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double)); var result = await target.Take(1); Assert.IsType(result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Coerce_Get_Null_Double_String_To_UnsetValue() + public async Task Should_Coerce_Get_Null_Double_String_To_UnsetValue() { var data = new Class1 { StringValue = null }; var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double)); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); } [Fact] public void Should_Convert_Set_String_To_Double() { -#if NET461 - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; -#else - CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; -#endif - var data = new Class1 { StringValue = (5.6).ToString() }; var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double)); target.OnNext(6.7); Assert.Equal((6.7).ToString(), data.StringValue); + + GC.KeepAlive(data); } [Fact] - public async void Should_Convert_Get_Double_To_String() + public async Task Should_Convert_Get_Double_To_String() { -#if NET461 - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; -#else - CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; -#endif - var data = new Class1 { DoubleValue = 5.6 }; var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); var result = await target.Take(1); Assert.Equal((5.6).ToString(), result); + + GC.KeepAlive(data); } [Fact] public void Should_Convert_Set_Double_To_String() { -#if NET461 - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; -#else - CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; -#endif - var data = new Class1 { DoubleValue = 5.6 }; var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); target.OnNext("6.7"); Assert.Equal(6.7, data.DoubleValue); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_BindingNotification_With_FallbackValue_For_NonConvertibe_Target_Value() + public async Task Should_Return_BindingNotification_With_FallbackValue_For_NonConvertibe_Target_Value() { -#if NET461 - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; -#else - CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; -#endif - var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( new ExpressionObserver(data, "StringValue"), @@ -156,17 +146,13 @@ namespace Avalonia.Markup.UnitTests.Data BindingErrorType.Error, 42), result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_BindingNotification_With_FallbackValue_For_NonConvertibe_Target_Value_With_Data_Validation() + public async Task Should_Return_BindingNotification_With_FallbackValue_For_NonConvertibe_Target_Value_With_Data_Validation() { -#if NET461 - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; -#else - CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; -#endif - var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( new ExpressionObserver(data, "StringValue", true), @@ -181,17 +167,13 @@ namespace Avalonia.Markup.UnitTests.Data BindingErrorType.Error, 42), result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_BindingNotification_For_Invalid_FallbackValue() + public async Task Should_Return_BindingNotification_For_Invalid_FallbackValue() { -#if NET461 - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; -#else - CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; -#endif - var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( new ExpressionObserver(data, "StringValue"), @@ -203,21 +185,17 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal( new BindingNotification( new AggregateException( - new InvalidCastException("Could not convert 'foo' to 'System.Int32'"), + new InvalidCastException("'foo' is not a valid number."), new InvalidCastException("Could not convert FallbackValue 'bar' to 'System.Int32'")), BindingErrorType.Error), result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_BindingNotification_For_Invalid_FallbackValue_With_Data_Validation() + public async Task Should_Return_BindingNotification_For_Invalid_FallbackValue_With_Data_Validation() { -#if NET461 - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; -#else - CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; -#endif - var data = new Class1 { StringValue = "foo" }; var target = new BindingExpression( new ExpressionObserver(data, "StringValue", true), @@ -229,10 +207,12 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal( new BindingNotification( new AggregateException( - new InvalidCastException("Could not convert 'foo' to 'System.Int32'"), + new InvalidCastException("'foo' is not a valid number."), new InvalidCastException("Could not convert FallbackValue 'bar' to 'System.Int32'")), BindingErrorType.Error), result); + + GC.KeepAlive(data); } [Fact] @@ -244,6 +224,8 @@ namespace Avalonia.Markup.UnitTests.Data target.OnNext("foo"); Assert.Equal(5.6, data.DoubleValue); + + GC.KeepAlive(data); } [Fact] @@ -259,6 +241,8 @@ namespace Avalonia.Markup.UnitTests.Data target.OnNext("foo"); Assert.Equal(9.8, data.DoubleValue); + + GC.KeepAlive(data); } [Fact] @@ -270,6 +254,8 @@ namespace Avalonia.Markup.UnitTests.Data target.OnNext(null); Assert.Equal(0, data.DoubleValue); + + GC.KeepAlive(data); } [Fact] @@ -281,6 +267,8 @@ namespace Avalonia.Markup.UnitTests.Data target.OnNext(AvaloniaProperty.UnsetValue); Assert.Equal(0, data.DoubleValue); + + GC.KeepAlive(data); } [Fact] @@ -288,6 +276,7 @@ namespace Avalonia.Markup.UnitTests.Data { var data = new Class1 { DoubleValue = 5.6 }; var converter = new Mock(); + var target = new BindingExpression( new ExpressionObserver(data, "DoubleValue"), typeof(string), @@ -296,7 +285,9 @@ namespace Avalonia.Markup.UnitTests.Data target.Subscribe(_ => { }); - converter.Verify(x => x.Convert(5.6, typeof(string), "foo", CultureInfo.CurrentUICulture)); + converter.Verify(x => x.Convert(5.6, typeof(string), "foo", CultureInfo.CurrentCulture)); + + GC.KeepAlive(data); } [Fact] @@ -312,7 +303,9 @@ namespace Avalonia.Markup.UnitTests.Data target.OnNext("bar"); - converter.Verify(x => x.ConvertBack("bar", typeof(double), "foo", CultureInfo.CurrentUICulture)); + converter.Verify(x => x.ConvertBack("bar", typeof(double), "foo", CultureInfo.CurrentCulture)); + + GC.KeepAlive(data); } [Fact] @@ -339,6 +332,8 @@ namespace Avalonia.Markup.UnitTests.Data BindingErrorType.Error) }, result); + + GC.KeepAlive(data); } private class Class1 : NotifyingBase diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_AttachedProperty.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_AttachedProperty.cs index 349a09da2c..a8069cb75c 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_AttachedProperty.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_AttachedProperty.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Reactive.Linq; +using System.Threading.Tasks; using Avalonia.Diagnostics; using Avalonia.Markup.Data; using Xunit; @@ -18,7 +19,7 @@ namespace Avalonia.Markup.UnitTests.Data } [Fact] - public async void Should_Get_Attached_Property_Value() + public async Task Should_Get_Attached_Property_Value() { var data = new Class1(); var target = new ExpressionObserver(data, "(Owner.Foo)"); @@ -30,7 +31,7 @@ namespace Avalonia.Markup.UnitTests.Data } [Fact] - public async void Should_Get_Chained_Attached_Property_Value() + public async Task Should_Get_Chained_Attached_Property_Value() { var data = new Class1 { diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_AvaloniaProperty.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_AvaloniaProperty.cs index ece9437308..cd691daaf9 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_AvaloniaProperty.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_AvaloniaProperty.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Reactive.Linq; +using System.Threading.Tasks; using Avalonia.Diagnostics; using Avalonia.Markup.Data; using Xunit; @@ -18,7 +19,7 @@ namespace Avalonia.Markup.UnitTests.Data } [Fact] - public async void Should_Get_Simple_Property_Value() + public async Task Should_Get_Simple_Property_Value() { var data = new Class1(); var target = new ExpressionObserver(data, "Foo"); diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs index 3b5ca26db1..125bd84f3d 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs @@ -28,6 +28,8 @@ namespace Avalonia.Markup.UnitTests.Data observer.SetValue(-5); Assert.False(validationMessageFound); + + GC.KeepAlive(data); } [Fact] @@ -43,6 +45,8 @@ namespace Avalonia.Markup.UnitTests.Data observer.SetValue(-5); Assert.True(validationMessageFound); + + GC.KeepAlive(data); } [Fact] @@ -102,6 +106,8 @@ namespace Avalonia.Markup.UnitTests.Data new BindingNotification(new Exception("Must be positive"), BindingErrorType.DataValidationError, 5), new BindingNotification(5), }, result); + + GC.KeepAlive(data); } [Fact] @@ -147,6 +153,9 @@ namespace Avalonia.Markup.UnitTests.Data BindingErrorType.Error, AvaloniaProperty.UnsetValue), }, result); + + GC.KeepAlive(container); + GC.KeepAlive(inner); } public class ExceptionTest : NotifyingBase diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs index 9cc843381c..a68213baee 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Reactive.Linq; +using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Diagnostics; using Avalonia.Markup.Data; @@ -16,113 +17,135 @@ namespace Avalonia.Markup.UnitTests.Data public class ExpressionObserverTests_Indexer { [Fact] - public async void Should_Get_Array_Value() + public async Task Should_Get_Array_Value() { var data = new { Foo = new [] { "foo", "bar" } }; var target = new ExpressionObserver(data, "Foo[1]"); var result = await target.Take(1); Assert.Equal("bar", result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Get_UnsetValue_For_Invalid_Array_Index() + public async Task Should_Get_UnsetValue_For_Invalid_Array_Index() { var data = new { Foo = new[] { "foo", "bar" } }; var target = new ExpressionObserver(data, "Foo[invalid]"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Get_UnsetValue_For_Invalid_Dictionary_Index() + public async Task Should_Get_UnsetValue_For_Invalid_Dictionary_Index() { var data = new { Foo = new Dictionary { { 1, "foo" } } }; var target = new ExpressionObserver(data, "Foo[invalid]"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Get_UnsetValue_For_Object_Without_Indexer() + public async Task Should_Get_UnsetValue_For_Object_Without_Indexer() { var data = new { Foo = 5 }; var target = new ExpressionObserver(data, "Foo[noindexer]"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Get_MultiDimensional_Array_Value() + public async Task Should_Get_MultiDimensional_Array_Value() { var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } }; var target = new ExpressionObserver(data, "Foo[1, 1]"); var result = await target.Take(1); Assert.Equal("qux", result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Get_Value_For_String_Indexer() + public async Task Should_Get_Value_For_String_Indexer() { var data = new { Foo = new Dictionary { { "foo", "bar" }, { "baz", "qux" } } }; var target = new ExpressionObserver(data, "Foo[foo]"); var result = await target.Take(1); Assert.Equal("bar", result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Get_Value_For_Non_String_Indexer() + public async Task Should_Get_Value_For_Non_String_Indexer() { var data = new { Foo = new Dictionary { { 1.0, "bar" }, { 2.0, "qux" } } }; var target = new ExpressionObserver(data, "Foo[1.0]"); var result = await target.Take(1); Assert.Equal("bar", result); + + GC.KeepAlive(data); } [Fact] - public async void Array_Out_Of_Bounds_Should_Return_UnsetValue() + public async Task Array_Out_Of_Bounds_Should_Return_UnsetValue() { var data = new { Foo = new[] { "foo", "bar" } }; var target = new ExpressionObserver(data, "Foo[2]"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); } [Fact] - public async void Array_With_Wrong_Dimensions_Should_Return_UnsetValue() + public async Task Array_With_Wrong_Dimensions_Should_Return_UnsetValue() { var data = new { Foo = new[] { "foo", "bar" } }; var target = new ExpressionObserver(data, "Foo[1,2]"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); } [Fact] - public async void List_Out_Of_Bounds_Should_Return_UnsetValue() + public async Task List_Out_Of_Bounds_Should_Return_UnsetValue() { var data = new { Foo = new List { "foo", "bar" } }; var target = new ExpressionObserver(data, "Foo[2]"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Get_List_Value() + public async Task Should_Get_List_Value() { var data = new { Foo = new List { "foo", "bar" } }; var target = new ExpressionObserver(data, "Foo[1]"); var result = await target.Take(1); Assert.Equal("bar", result); + + GC.KeepAlive(data); } [Fact] @@ -139,6 +162,8 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "baz" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); + + GC.KeepAlive(data); } [Fact] @@ -155,6 +180,8 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(new[] { "foo", "bar" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); + + GC.KeepAlive(data); } [Fact] @@ -171,6 +198,8 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(new[] { "bar", "baz" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); + + GC.KeepAlive(data); } [Fact] @@ -187,6 +216,9 @@ namespace Avalonia.Markup.UnitTests.Data data.Foo.Move(0, 1); Assert.Equal(new[] { "bar", "foo" }, result); + + GC.KeepAlive(sub); + GC.KeepAlive(data); } [Fact] @@ -200,6 +232,9 @@ namespace Avalonia.Markup.UnitTests.Data data.Foo.Clear(); Assert.Equal(new[] { "bar", AvaloniaProperty.UnsetValue }, result); + + GC.KeepAlive(sub); + GC.KeepAlive(data); } [Fact] @@ -220,6 +255,8 @@ namespace Avalonia.Markup.UnitTests.Data var expected = new[] { "bar", "bar2" }; Assert.Equal(expected, result); Assert.Equal(0, data.Foo.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } [Fact] @@ -234,6 +271,8 @@ namespace Avalonia.Markup.UnitTests.Data } Assert.Equal("baz", data.Foo[1]); + + GC.KeepAlive(data); } [Fact] @@ -254,6 +293,8 @@ namespace Avalonia.Markup.UnitTests.Data } Assert.Equal(4, data.Foo["foo"]); + + GC.KeepAlive(data); } [Fact] @@ -274,6 +315,8 @@ namespace Avalonia.Markup.UnitTests.Data } Assert.Equal(4, data.Foo["bar"]); + + GC.KeepAlive(data); } [Fact] @@ -291,6 +334,8 @@ namespace Avalonia.Markup.UnitTests.Data } Assert.Equal("bar2", data.Foo["foo"]); + + GC.KeepAlive(data); } private class NonIntegerIndexer : NotifyingBase diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs index 2a2bf06bf1..04a8e30d16 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs @@ -90,7 +90,8 @@ namespace Avalonia.Markup.UnitTests.Data { var scheduler = new TestScheduler(); var update = scheduler.CreateColdObservable(); - var target = new ExpressionObserver(() => new { Foo = "foo" }, "Foo", update); + var data = new { Foo = "foo" }; + var target = new ExpressionObserver(() => data, "Foo", update); var result = new List(); using (target.Subscribe(x => result.Add(x))) @@ -101,6 +102,8 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(new[] { "foo" }, result); Assert.All(update.Subscriptions, x => Assert.NotEqual(Subscription.Infinite, x.Unsubscribe)); + + GC.KeepAlive(data); } private Recorded> OnNext(long time, object value) diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs index 6bee0d10f4..d8dc2de847 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs @@ -3,6 +3,7 @@ using System; using System.Reactive.Linq; +using System.Threading.Tasks; using Avalonia.Data; using Avalonia.Markup.Data; using Xunit; @@ -12,57 +13,67 @@ namespace Avalonia.Markup.UnitTests.Data public class ExpressionObserverTests_Negation { [Fact] - public async void Should_Negate_Boolean_Value() + public async Task Should_Negate_Boolean_Value() { var data = new { Foo = true }; var target = new ExpressionObserver(data, "!Foo"); var result = await target.Take(1); Assert.Equal(false, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Negate_0() + public async Task Should_Negate_0() { var data = new { Foo = 0 }; var target = new ExpressionObserver(data, "!Foo"); var result = await target.Take(1); Assert.Equal(true, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Negate_1() + public async Task Should_Negate_1() { var data = new { Foo = 1 }; var target = new ExpressionObserver(data, "!Foo"); var result = await target.Take(1); Assert.Equal(false, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Negate_False_String() + public async Task Should_Negate_False_String() { var data = new { Foo = "false" }; var target = new ExpressionObserver(data, "!Foo"); var result = await target.Take(1); Assert.Equal(true, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Negate_True_String() + public async Task Should_Negate_True_String() { var data = new { Foo = "True" }; var target = new ExpressionObserver(data, "!Foo"); var result = await target.Take(1); Assert.Equal(false, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean() + public async Task Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean() { var data = new { Foo = "foo" }; var target = new ExpressionObserver(data, "!Foo"); @@ -73,10 +84,12 @@ namespace Avalonia.Markup.UnitTests.Data new InvalidCastException($"Unable to convert 'foo' to bool."), BindingErrorType.Error), result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean() + public async Task Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean() { var data = new { Foo = new object() }; var target = new ExpressionObserver(data, "!Foo"); @@ -87,6 +100,8 @@ namespace Avalonia.Markup.UnitTests.Data new InvalidCastException($"Unable to convert 'System.Object' to bool."), BindingErrorType.Error), result); + + GC.KeepAlive(data); } [Fact] @@ -96,6 +111,8 @@ namespace Avalonia.Markup.UnitTests.Data var target = new ExpressionObserver(data, "!Foo"); Assert.False(target.SetValue("bar")); + + GC.KeepAlive(data); } } } diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs index 640d82fa19..62d5c28f49 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs @@ -29,6 +29,8 @@ namespace Avalonia.Markup.UnitTests.Data sync.ExecutePostedCallbacks(); Assert.Equal(new[] { source }, result); + + GC.KeepAlive(data); } } @@ -47,6 +49,8 @@ namespace Avalonia.Markup.UnitTests.Data sync.ExecutePostedCallbacks(); Assert.Equal(new[] { "foo", "bar" }, result); + + GC.KeepAlive(data); } } @@ -67,6 +71,8 @@ namespace Avalonia.Markup.UnitTests.Data sub.Dispose(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } } @@ -87,6 +93,8 @@ namespace Avalonia.Markup.UnitTests.Data // What does it mean to have data validation on an observable? Without a use-case // it's hard to know what to do here so for the moment the value is returned. Assert.Equal(new[] { "foo", "bar" }, result); + + GC.KeepAlive(data); } } @@ -107,6 +115,8 @@ namespace Avalonia.Markup.UnitTests.Data sub.Dispose(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } } @@ -132,6 +142,8 @@ namespace Avalonia.Markup.UnitTests.Data result); sub.Dispose(); + + GC.KeepAlive(data); } } diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs index bdcd39d997..4cb2061c9e 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs @@ -11,19 +11,22 @@ using Avalonia.Data; using Avalonia.Markup.Data; using Avalonia.UnitTests; using Xunit; +using System.Threading.Tasks; namespace Avalonia.Markup.UnitTests.Data { public class ExpressionObserverTests_Property { [Fact] - public async void Should_Get_Simple_Property_Value() + public async Task Should_Get_Simple_Property_Value() { var data = new { Foo = "foo" }; var target = new ExpressionObserver(data, "Foo"); var result = await target.Take(1); Assert.Equal("foo", result); + + GC.KeepAlive(data); } [Fact] @@ -35,76 +38,92 @@ namespace Avalonia.Markup.UnitTests.Data target.Subscribe(_ => { }); Assert.Equal(typeof(string), target.ResultType); + + GC.KeepAlive(data); } [Fact] - public async void Should_Get_Simple_Property_Value_Null() + public async Task Should_Get_Simple_Property_Value_Null() { var data = new { Foo = (string)null }; var target = new ExpressionObserver(data, "Foo"); var result = await target.Take(1); Assert.Null(result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Get_Simple_Property_From_Base_Class() + public async Task Should_Get_Simple_Property_From_Base_Class() { var data = new Class3 { Foo = "foo" }; var target = new ExpressionObserver(data, "Foo"); var result = await target.Take(1); Assert.Equal("foo", result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_UnsetValue_For_Root_Null() + public async Task Should_Return_UnsetValue_For_Root_Null() { var data = new Class3 { Foo = "foo" }; var target = new ExpressionObserver(default(object), "Foo"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_UnsetValue_For_Root_UnsetValue() + public async Task Should_Return_UnsetValue_For_Root_UnsetValue() { var data = new Class3 { Foo = "foo" }; var target = new ExpressionObserver(AvaloniaProperty.UnsetValue, "Foo"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_UnsetValue_For_Observable_Root_Null() + public async Task Should_Return_UnsetValue_For_Observable_Root_Null() { var data = new Class3 { Foo = "foo" }; var target = new ExpressionObserver(Observable.Return(default(object)), "Foo"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_UnsetValue_For_Observable_Root_UnsetValue() + public async Task Should_Return_UnsetValue_For_Observable_Root_UnsetValue() { var data = new Class3 { Foo = "foo" }; var target = new ExpressionObserver(Observable.Return(AvaloniaProperty.UnsetValue), "Foo"); var result = await target.Take(1); Assert.Equal(AvaloniaProperty.UnsetValue, result); + + GC.KeepAlive(data); } [Fact] - public async void Should_Get_Simple_Property_Chain() + public async Task Should_Get_Simple_Property_Chain() { var data = new { Foo = new { Bar = new { Baz = "baz" } } }; var target = new ExpressionObserver(data, "Foo.Bar.Baz"); var result = await target.Take(1); Assert.Equal("baz", result); + + GC.KeepAlive(data); } [Fact] @@ -116,10 +135,12 @@ namespace Avalonia.Markup.UnitTests.Data target.Subscribe(_ => { }); Assert.Equal(typeof(string), target.ResultType); + + GC.KeepAlive(data); } [Fact] - public async void Should_Return_BindingNotification_Error_For_Broken_Chain() + public async Task Should_Return_BindingNotification_Error_For_Broken_Chain() { var data = new { Foo = new { Bar = 1 } }; var target = new ExpressionObserver(data, "Foo.Bar.Baz"); @@ -131,6 +152,8 @@ namespace Avalonia.Markup.UnitTests.Data new BindingNotification( new MissingMemberException("Could not find CLR property 'Baz' on '1'"), BindingErrorType.Error), result); + + GC.KeepAlive(data); } [Fact] @@ -151,6 +174,8 @@ namespace Avalonia.Markup.UnitTests.Data AvaloniaProperty.UnsetValue), }, result); + + GC.KeepAlive(data); } [Fact] @@ -160,6 +185,8 @@ namespace Avalonia.Markup.UnitTests.Data var target = new ExpressionObserver(data, "Foo.Bar.Baz"); Assert.Null(target.ResultType); + + GC.KeepAlive(data); } [Fact] @@ -177,6 +204,8 @@ namespace Avalonia.Markup.UnitTests.Data sub.Dispose(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } [Fact] @@ -205,6 +234,8 @@ namespace Avalonia.Markup.UnitTests.Data sub.Dispose(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } [Fact] @@ -224,6 +255,8 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(0, data.PropertyChangedSubscriptionCount); Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } [Fact] @@ -245,6 +278,8 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(0, data.PropertyChangedSubscriptionCount); Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); Assert.Equal(0, old.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } [Fact] @@ -286,6 +321,8 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(0, data.PropertyChangedSubscriptionCount); Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); Assert.Equal(0, old.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } [Fact] @@ -318,6 +355,8 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); Assert.Equal(0, breaking.PropertyChangedSubscriptionCount); Assert.Equal(0, old.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } [Fact] @@ -334,6 +373,8 @@ namespace Avalonia.Markup.UnitTests.Data update.OnNext(Unit.Default); Assert.Equal(new[] { "foo", "bar" }, result); + + GC.KeepAlive(data); } [Fact] @@ -374,6 +415,8 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(new[] { "foo", "bar" }, result1); Assert.Equal(new[] { "foo", "bar" }, result2); Assert.Equal(new[] { "bar" }, result3); + + GC.KeepAlive(data); } [Fact] @@ -391,6 +434,8 @@ namespace Avalonia.Markup.UnitTests.Data sub2.Dispose(); Assert.Equal(0, data.PropertyChangedSubscriptionCount); + + GC.KeepAlive(data); } [Fact] @@ -405,6 +450,8 @@ namespace Avalonia.Markup.UnitTests.Data } Assert.Equal("bar", data.Foo); + + GC.KeepAlive(data); } [Fact] @@ -419,6 +466,8 @@ namespace Avalonia.Markup.UnitTests.Data } Assert.Equal("baz", ((Class2)data.Next).Bar); + + GC.KeepAlive(data); } [Fact] @@ -431,6 +480,8 @@ namespace Avalonia.Markup.UnitTests.Data { Assert.False(target.SetValue("baz")); } + + GC.KeepAlive(data); } [Fact] @@ -444,6 +495,8 @@ namespace Avalonia.Markup.UnitTests.Data target.SetValue("bar"); Assert.Equal(new[] { null, "bar" }, result); + + GC.KeepAlive(data); } [Fact] @@ -457,6 +510,8 @@ namespace Avalonia.Markup.UnitTests.Data target.SetValue("bar"); Assert.Equal(new[] { null, "bar" }, result); + + GC.KeepAlive(data); } [Fact] @@ -469,6 +524,8 @@ namespace Avalonia.Markup.UnitTests.Data { Assert.False(target.SetValue("baz")); } + + GC.KeepAlive(data); } [Fact] @@ -498,6 +555,9 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(0, first.PropertyChangedSubscriptionCount); Assert.Equal(0, second.PropertyChangedSubscriptionCount); + + GC.KeepAlive(first); + GC.KeepAlive(second); } [Fact] diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs index 61e6dcb833..c251f4398a 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs @@ -30,6 +30,8 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(1, result.Count); Assert.IsType>(result[0]); + + GC.KeepAlive(data); } } @@ -45,6 +47,8 @@ namespace Avalonia.Markup.UnitTests.Data var sub = target.Subscribe(x => result.Add(x)); Assert.Equal(new[] { "foo" }, result); + + GC.KeepAlive(data); } } @@ -63,6 +67,8 @@ namespace Avalonia.Markup.UnitTests.Data sync.ExecutePostedCallbacks(); Assert.Equal(new[] { "foo" }, result); + + GC.KeepAlive(data); } } @@ -88,6 +94,8 @@ namespace Avalonia.Markup.UnitTests.Data BindingErrorType.Error) }, result); + + GC.KeepAlive(data); } } @@ -110,6 +118,8 @@ namespace Avalonia.Markup.UnitTests.Data BindingErrorType.Error) }, result); + + GC.KeepAlive(data); } } @@ -130,6 +140,8 @@ namespace Avalonia.Markup.UnitTests.Data // What does it mean to have data validation on a Task? Without a use-case it's // hard to know what to do here so for the moment the value is returned. Assert.Equal(new [] { "foo" }, result); + + GC.KeepAlive(data); } } diff --git a/tests/Avalonia.Markup.UnitTests/Data/Plugins/ExceptionValidationPluginTests.cs b/tests/Avalonia.Markup.UnitTests/Data/Plugins/ExceptionValidationPluginTests.cs index 4a34791008..eb529a3b13 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/Plugins/ExceptionValidationPluginTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/Plugins/ExceptionValidationPluginTests.cs @@ -35,6 +35,8 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins new BindingNotification(new ArgumentOutOfRangeException("value"), BindingErrorType.DataValidationError), new BindingNotification(6), }, result); + + GC.KeepAlive(data); } public class Data : NotifyingBase diff --git a/tests/Avalonia.Markup.UnitTests/Properties/AssemblyInfo.cs b/tests/Avalonia.Markup.UnitTests/Properties/AssemblyInfo.cs index d1567d46be..4c3825ed44 100644 --- a/tests/Avalonia.Markup.UnitTests/Properties/AssemblyInfo.cs +++ b/tests/Avalonia.Markup.UnitTests/Properties/AssemblyInfo.cs @@ -37,4 +37,4 @@ using Xunit; [assembly: AssemblyFileVersion("1.0.0.0")] // Don't run tests in parallel. -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file +[assembly: CollectionBehavior(MaxParallelThreads = 1)] diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj index 0cbdc142eb..f6f8f6bcb0 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -1,6 +1,7 @@  net461;netcoreapp1.1 + Library diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs index 5d67151992..874dc18552 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs @@ -10,13 +10,14 @@ using Moq; using Avalonia.Controls; using Avalonia.Markup.Xaml.Data; using Xunit; +using System.Threading.Tasks; namespace Avalonia.Markup.Xaml.UnitTests.Data { public class MultiBindingTests { [Fact] - public async void OneWay_Binding_Should_Be_Set_Up() + public async Task OneWay_Binding_Should_Be_Set_Up() { var source = new { A = 1, B = 2, C = 3 }; var binding = new MultiBinding diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Properties/AssemblyInfo.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Properties/AssemblyInfo.cs index 034e9f74ce..24cc853318 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Properties/AssemblyInfo.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Properties/AssemblyInfo.cs @@ -7,4 +7,4 @@ using Xunit; [assembly: AssemblyTitle("Avalonia.Markup.Xaml.UnitTests")] // Don't run tests in parallel. -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file +[assembly: CollectionBehavior(MaxParallelThreads = 1)] diff --git a/tests/Avalonia.RenderTests/Media/BitmapTests.cs b/tests/Avalonia.RenderTests/Media/BitmapTests.cs index 2e6daa8554..57f1e40d12 100644 --- a/tests/Avalonia.RenderTests/Media/BitmapTests.cs +++ b/tests/Avalonia.RenderTests/Media/BitmapTests.cs @@ -43,7 +43,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media public IntPtr Address { get; } - public Size Dpi { get; } = new Size(96, 96); + public Vector Dpi { get; } = new Vector(96, 96); public PixelFormat Format { get; } @@ -81,6 +81,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media using (var target = r.CreateRenderTarget(new object[] { fb })) using (var ctx = target.CreateDrawingContext(null)) { + ctx.Clear(Colors.Transparent); ctx.PushOpacity(0.8); ctx.FillRectangle(Brushes.Chartreuse, new Rect(0, 0, 20, 100)); ctx.FillRectangle(Brushes.Crimson, new Rect(20, 0, 20, 100)); diff --git a/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs b/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs index cbf11504c1..f4e44a81f0 100644 --- a/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs @@ -28,6 +28,82 @@ namespace Avalonia.Direct2D1.RenderTests.Media get { return System.IO.Path.Combine(OutputPath, "github_icon.png"); } } + private string SmallBitmapPath + { + get { return System.IO.Path.Combine(OutputPath, "github_icon_small.png"); } + } + + [Fact] + public void ImageBrush_Tile_Fill() + { + Decorator target = new Decorator + { + Width = 200, + Height = 200, + Child = new Rectangle + { + Margin = new Thickness(8), + Fill = new ImageBrush + { + Stretch = Stretch.Fill, + TileMode = TileMode.Tile, + DestinationRect = new RelativeRect(0, 0, 25, 30, RelativeUnit.Absolute), + Source = new Bitmap(BitmapPath), + } + } + }; + + RenderToFile(target); + CompareImages(); + } + + [Fact] + public void ImageBrush_Tile_UniformToFill() + { + Decorator target = new Decorator + { + Width = 200, + Height = 200, + Child = new Rectangle + { + Margin = new Thickness(8), + Fill = new ImageBrush + { + Stretch = Stretch.Uniform, + TileMode = TileMode.Tile, + DestinationRect = new RelativeRect(0, 0, 25, 30, RelativeUnit.Absolute), + Source = new Bitmap(BitmapPath), + } + } + }; + + RenderToFile(target); + CompareImages(); + } + + [Fact] + public void ImageBrush_Tile_Small_Image() + { + Decorator target = new Decorator + { + Width = 200, + Height = 200, + Child = new Rectangle + { + Margin = new Thickness(8), + Fill = new ImageBrush + { + Stretch = Stretch.None, + TileMode = TileMode.Tile, + Source = new Bitmap(SmallBitmapPath), + } + } + }; + + RenderToFile(target); + CompareImages(); + } + [Fact] public void ImageBrush_NoStretch_NoTile_Alignment_TopLeft() { diff --git a/tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj b/tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj index d35542b51f..8dd8faf9db 100644 --- a/tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj +++ b/tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj @@ -1,6 +1,7 @@  net461;netcoreapp1.1 + Library diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs index d97cc74c95..50b4828e73 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive; using System.Reactive.Linq; +using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Data; @@ -45,7 +46,7 @@ namespace Avalonia.Styling.UnitTests } [Fact] - public async void Child_Matches_Control_When_It_Is_Child_OfType_And_Class() + public async Task Child_Matches_Control_When_It_Is_Child_OfType_And_Class() { var parent = new TestLogical1(); var child = new TestLogical2(); @@ -144,6 +145,11 @@ namespace Avalonia.Styling.UnitTests throw new NotImplementedException(); } + public void NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + throw new NotImplementedException(); + } + public void NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { throw new NotImplementedException(); diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs index b4c284e7c9..7cf8c3dd1c 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs @@ -175,6 +175,11 @@ namespace Avalonia.Styling.UnitTests throw new NotImplementedException(); } + public void NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + throw new NotImplementedException(); + } + public void NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { throw new NotImplementedException(); diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj index 5b679fe360..40023134fd 100644 --- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj +++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj @@ -2,6 +2,7 @@ net461;netcoreapp1.1 false + Library true diff --git a/tests/Avalonia.UnitTests/InvariantCultureFixture.cs b/tests/Avalonia.UnitTests/InvariantCultureFixture.cs index f27df8f599..b00b16e2bb 100644 --- a/tests/Avalonia.UnitTests/InvariantCultureFixture.cs +++ b/tests/Avalonia.UnitTests/InvariantCultureFixture.cs @@ -20,13 +20,22 @@ namespace Avalonia.UnitTests public InvariantCultureFixture() { - _restore = CultureInfo.CurrentUICulture; - CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; +#if NET461 + _restore = Thread.CurrentThread.CurrentCulture; + Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; +#else + _restore = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; +#endif } public void Dispose() { - CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture = _restore; +#if NET461 + Thread.CurrentThread.CurrentCulture = _restore; +#else + CultureInfo.CurrentCulture = _restore; +#endif } } } diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index 8a711c415e..399870aef9 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -43,7 +43,7 @@ namespace Avalonia.UnitTests public Size ClientSize => new Size(100, 100); - public Size MaxClientSize => Size.Infinity; + public Size MaxClientSize { get; set; } = Size.Infinity; public double LayoutScaling => 1; diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 0cd8d4295b..f66adec1eb 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -50,6 +50,7 @@ namespace Avalonia.UnitTests public static readonly TestServices RealFocus = new TestServices( focusManager: new FocusManager(), keyboardDevice: () => new KeyboardDevice(), + keyboardNavigation: new KeyboardNavigationHandler(), inputManager: new InputManager()); public static readonly TestServices RealLayoutManager = new TestServices( @@ -63,6 +64,7 @@ namespace Avalonia.UnitTests IFocusManager focusManager = null, IInputManager inputManager = null, Func keyboardDevice = null, + IKeyboardNavigationHandler keyboardNavigation = null, ILayoutManager layoutManager = null, IRuntimePlatform platform = null, Func renderer = null, @@ -79,6 +81,7 @@ namespace Avalonia.UnitTests FocusManager = focusManager; InputManager = inputManager; KeyboardDevice = keyboardDevice; + KeyboardNavigation = keyboardNavigation; LayoutManager = layoutManager; Platform = platform; Renderer = renderer; @@ -96,6 +99,7 @@ namespace Avalonia.UnitTests public IInputManager InputManager { get; } public IFocusManager FocusManager { get; } public Func KeyboardDevice { get; } + public IKeyboardNavigationHandler KeyboardNavigation { get; } public ILayoutManager LayoutManager { get; } public IRuntimePlatform Platform { get; } public Func Renderer { get; } @@ -113,6 +117,7 @@ namespace Avalonia.UnitTests IFocusManager focusManager = null, IInputManager inputManager = null, Func keyboardDevice = null, + IKeyboardNavigationHandler keyboardNavigation = null, ILayoutManager layoutManager = null, IRuntimePlatform platform = null, Func renderer = null, @@ -131,6 +136,7 @@ namespace Avalonia.UnitTests focusManager: focusManager ?? FocusManager, inputManager: inputManager ?? InputManager, keyboardDevice: keyboardDevice ?? KeyboardDevice, + keyboardNavigation: keyboardNavigation ?? KeyboardNavigation, layoutManager: layoutManager ?? LayoutManager, platform: platform ?? Platform, renderer: renderer ?? Renderer, diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs index c5d533486b..28577e9670 100644 --- a/tests/Avalonia.UnitTests/UnitTestApplication.cs +++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs @@ -49,6 +49,7 @@ namespace Avalonia.UnitTests .BindToSelf(this) .Bind().ToConstant(Services.InputManager) .Bind().ToConstant(Services.KeyboardDevice?.Invoke()) + .Bind().ToConstant(Services.KeyboardNavigation) .Bind().ToConstant(Services.LayoutManager) .Bind().ToConstant(Services.Platform) .Bind().ToConstant(new RendererFactory(Services.Renderer)) diff --git a/tests/TestFiles/Cairo/Media/ImageBrush/ImageBrush_Tile_Fill.expected.png b/tests/TestFiles/Cairo/Media/ImageBrush/ImageBrush_Tile_Fill.expected.png new file mode 100644 index 0000000000..c38dcfbcfd Binary files /dev/null and b/tests/TestFiles/Cairo/Media/ImageBrush/ImageBrush_Tile_Fill.expected.png differ diff --git a/tests/TestFiles/Cairo/Media/ImageBrush/ImageBrush_Tile_Small_Image.expected.png b/tests/TestFiles/Cairo/Media/ImageBrush/ImageBrush_Tile_Small_Image.expected.png new file mode 100644 index 0000000000..669da6ca1a Binary files /dev/null and b/tests/TestFiles/Cairo/Media/ImageBrush/ImageBrush_Tile_Small_Image.expected.png differ diff --git a/tests/TestFiles/Cairo/Media/ImageBrush/ImageBrush_Tile_UniformToFill.expected.png b/tests/TestFiles/Cairo/Media/ImageBrush/ImageBrush_Tile_UniformToFill.expected.png new file mode 100644 index 0000000000..59e9aea5df Binary files /dev/null and b/tests/TestFiles/Cairo/Media/ImageBrush/ImageBrush_Tile_UniformToFill.expected.png differ diff --git a/tests/TestFiles/Cairo/Media/ImageBrush/github_icon_small.png b/tests/TestFiles/Cairo/Media/ImageBrush/github_icon_small.png new file mode 100644 index 0000000000..5799bdfdce Binary files /dev/null and b/tests/TestFiles/Cairo/Media/ImageBrush/github_icon_small.png differ diff --git a/tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Tile_Fill.expected.png b/tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Tile_Fill.expected.png new file mode 100644 index 0000000000..c38dcfbcfd Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Tile_Fill.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Tile_Small_Image.expected.png b/tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Tile_Small_Image.expected.png new file mode 100644 index 0000000000..669da6ca1a Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Tile_Small_Image.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Tile_UniformToFill.expected.png b/tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Tile_UniformToFill.expected.png new file mode 100644 index 0000000000..23fc402cd8 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/ImageBrush/ImageBrush_Tile_UniformToFill.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/ImageBrush/github_icon_small.png b/tests/TestFiles/Direct2D1/Media/ImageBrush/github_icon_small.png new file mode 100644 index 0000000000..5799bdfdce Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/ImageBrush/github_icon_small.png differ diff --git a/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_Fill.expected.png b/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_Fill.expected.png new file mode 100644 index 0000000000..53e51f0424 Binary files /dev/null and b/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_Fill.expected.png differ diff --git a/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_Small_Image.expected.png b/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_Small_Image.expected.png new file mode 100644 index 0000000000..669da6ca1a Binary files /dev/null and b/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_Small_Image.expected.png differ diff --git a/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_UniformToFill.expected.png b/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_UniformToFill.expected.png new file mode 100644 index 0000000000..32bdfd44d2 Binary files /dev/null and b/tests/TestFiles/Skia/Media/ImageBrush/ImageBrush_Tile_UniformToFill.expected.png differ diff --git a/tests/TestFiles/Skia/Media/ImageBrush/github_icon_small.png b/tests/TestFiles/Skia/Media/ImageBrush/github_icon_small.png new file mode 100644 index 0000000000..5799bdfdce Binary files /dev/null and b/tests/TestFiles/Skia/Media/ImageBrush/github_icon_small.png differ diff --git a/tools/packages.config b/tools/packages.config new file mode 100644 index 0000000000..5657d953fc --- /dev/null +++ b/tools/packages.config @@ -0,0 +1,4 @@ + + + +