diff --git a/Avalonia.sln b/Avalonia.sln index 54f6f5e7e7..39396f3ab8 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -71,7 +71,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup", "src\Mark EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.UnitTests", "tests\Avalonia.Markup.UnitTests\Avalonia.Markup.UnitTests.csproj", "{8EF392D5-1416-45AA-9956-7CBBC3229E8A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BindingTest", "samples\BindingTest\BindingTest.csproj", "{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BindingDemo", "samples\BindingDemo\BindingDemo.csproj", "{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}" EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "RenderHelpers", "src\Shared\RenderHelpers\RenderHelpers.shproj", "{3C4C0CB4-0C0F-4450-A37B-148C84FF905F}" EndProject @@ -109,7 +109,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.Te EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.TestApp", "tests\Avalonia.DesignerSupport.TestApp\Avalonia.DesignerSupport.TestApp.csproj", "{F1381F98-4D24-409A-A6C5-1C5B1E08BB08}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtualizationTest", "samples\VirtualizationTest\VirtualizationTest.csproj", "{FBCAF3D0-2808-4934-8E96-3F607594517B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtualizationDemo", "samples\VirtualizationDemo\VirtualizationDemo.csproj", "{FBCAF3D0-2808-4934-8E96-3F607594517B}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Interop", "Interop", "{A0CC0258-D18C-4AB3-854F-7101680FC3F9}" EndProject @@ -117,7 +117,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsInteropTest", "sampl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DotNetFrameworkRuntime", "src\Avalonia.DotNetFrameworkRuntime\Avalonia.DotNetFrameworkRuntime.csproj", "{4A1ABB09-9047-4BD5-A4AD-A055E52C5EE0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RenderTest", "samples\RenderTest\RenderTest.csproj", "{F1FDC5B0-4654-416F-AE69-E3E9BBD87801}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RenderDemo", "samples\RenderDemo\RenderDemo.csproj", "{F1FDC5B0-4654-416F-AE69-E3E9BBD87801}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Android", "samples\ControlCatalog.Android\ControlCatalog.Android.csproj", "{29132311-1848-4FD6-AE0C-4FF841151BD3}" EndProject @@ -141,6 +141,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\Microsoft.Reactive.Testing.props = build\Microsoft.Reactive.Testing.props build\Moq.props = build\Moq.props build\NetCore.props = build\NetCore.props + build\NetFX.props = build\NetFX.props build\ReactiveUI.props = build\ReactiveUI.props build\Rx.props = build\Rx.props build\SampleApp.props = build\SampleApp.props @@ -169,7 +170,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia.RenderTests", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Remote.Protocol", "src\Avalonia.Remote.Protocol\Avalonia.Remote.Protocol.csproj", "{D78A720C-C0C6-478B-8564-F167F9BDD01B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RemoteTest", "samples\RemoteTest\RemoteTest.csproj", "{E2999E4A-9086-401F-898C-AEB0AD38E676}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RemoteDemo", "samples\RemoteDemo\RemoteDemo.csproj", "{E2999E4A-9086-401F-898C-AEB0AD38E676}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{4ED8B739-6F4E-4CD4-B993-545E6B5CE637}" EndProject @@ -183,6 +184,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.MonoMac", "src\OSX EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Designer.HostApp.NetFX", "src\tools\Avalonia.Designer.HostApp.NetFX\Avalonia.Designer.HostApp.NetFX.csproj", "{4ADA61C8-D191-428D-9066-EF4F0D86520F}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia.UnitTests", "tests\Avalonia.Skia.UnitTests\Avalonia.Skia.UnitTests.csproj", "{E1240B49-7B4B-4371-A00E-068778C5CF0B}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -2468,6 +2471,46 @@ Global {4ADA61C8-D191-428D-9066-EF4F0D86520F}.Release|NetCoreOnly.ActiveCfg = Release|Any CPU {4ADA61C8-D191-428D-9066-EF4F0D86520F}.Release|x86.ActiveCfg = Release|Any CPU {4ADA61C8-D191-428D-9066-EF4F0D86520F}.Release|x86.Build.0 = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|NetCoreOnly.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|NetCoreOnly.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhone.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|NetCoreOnly.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|NetCoreOnly.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|x86.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|x86.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhone.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|NetCoreOnly.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|NetCoreOnly.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|x86.ActiveCfg = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|x86.Build.0 = Debug|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|Any CPU.Build.0 = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhone.ActiveCfg = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhone.Build.0 = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|NetCoreOnly.ActiveCfg = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|NetCoreOnly.Build.0 = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|x86.ActiveCfg = Release|Any CPU + {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2520,6 +2563,7 @@ Global {F40FC0A2-1BC3-401C-BFC1-928EC4D4A9CE} = {9B9E3891-2366-4253-A952-D08BCEB71098} {CBFD5788-567D-401B-9DFA-74E4224025A0} = {A59C4C0A-64DF-4621-B450-2BA00D6F61E2} {4ADA61C8-D191-428D-9066-EF4F0D86520F} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} + {E1240B49-7B4B-4371-A00E-068778C5CF0B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/build.cake b/build.cake index bf3ae41b58..561a33186a 100644 --- a/build.cake +++ b/build.cake @@ -207,6 +207,7 @@ Task("Run-Unit-Tests") RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", data.Parameters, false); RunCoreTest("./tests/Avalonia.Styling.UnitTests", data.Parameters, false); RunCoreTest("./tests/Avalonia.Visuals.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Skia.UnitTests", data.Parameters, false); if (data.Parameters.IsRunningOnWindows) { RunCoreTest("./tests/Avalonia.Direct2D1.UnitTests", data.Parameters, true); diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index 04e8a3ad4f..35c979a95e 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/samples/BindingTest/App.config b/samples/BindingDemo/App.config similarity index 100% rename from samples/BindingTest/App.config rename to samples/BindingDemo/App.config diff --git a/samples/BindingTest/App.xaml b/samples/BindingDemo/App.xaml similarity index 100% rename from samples/BindingTest/App.xaml rename to samples/BindingDemo/App.xaml diff --git a/samples/BindingTest/App.xaml.cs b/samples/BindingDemo/App.xaml.cs similarity index 95% rename from samples/BindingTest/App.xaml.cs rename to samples/BindingDemo/App.xaml.cs index ccad1d0ba9..01c52a2a49 100644 --- a/samples/BindingTest/App.xaml.cs +++ b/samples/BindingDemo/App.xaml.cs @@ -5,7 +5,7 @@ using Avalonia.Logging.Serilog; using Avalonia.Markup.Xaml; using Serilog; -namespace BindingTest +namespace BindingDemo { public class App : Application { diff --git a/samples/BindingTest/BindingTest.csproj b/samples/BindingDemo/BindingDemo.csproj similarity index 100% rename from samples/BindingTest/BindingTest.csproj rename to samples/BindingDemo/BindingDemo.csproj diff --git a/samples/BindingTest/MainWindow.xaml b/samples/BindingDemo/MainWindow.xaml similarity index 98% rename from samples/BindingTest/MainWindow.xaml rename to samples/BindingDemo/MainWindow.xaml index 6b80225686..a69fb75742 100644 --- a/samples/BindingTest/MainWindow.xaml +++ b/samples/BindingDemo/MainWindow.xaml @@ -1,7 +1,7 @@ diff --git a/samples/BindingTest/MainWindow.xaml.cs b/samples/BindingDemo/MainWindow.xaml.cs similarity index 88% rename from samples/BindingTest/MainWindow.xaml.cs rename to samples/BindingDemo/MainWindow.xaml.cs index c1c3c09406..eaa57e1f5f 100644 --- a/samples/BindingTest/MainWindow.xaml.cs +++ b/samples/BindingDemo/MainWindow.xaml.cs @@ -1,9 +1,9 @@ -using BindingTest.ViewModels; +using BindingDemo.ViewModels; using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; -namespace BindingTest +namespace BindingDemo { public class MainWindow : Window { diff --git a/samples/BindingTest/TestItemView.xaml b/samples/BindingDemo/TestItemView.xaml similarity index 100% rename from samples/BindingTest/TestItemView.xaml rename to samples/BindingDemo/TestItemView.xaml diff --git a/samples/BindingTest/TestItemView.xaml.cs b/samples/BindingDemo/TestItemView.xaml.cs similarity index 93% rename from samples/BindingTest/TestItemView.xaml.cs rename to samples/BindingDemo/TestItemView.xaml.cs index 32f367ef68..8c0b592f00 100644 --- a/samples/BindingTest/TestItemView.xaml.cs +++ b/samples/BindingDemo/TestItemView.xaml.cs @@ -1,7 +1,7 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; -namespace BindingTest +namespace BindingDemo { public class TestItemView : UserControl { diff --git a/samples/BindingTest/ViewModels/DataAnnotationsErrorViewModel.cs b/samples/BindingDemo/ViewModels/DataAnnotationsErrorViewModel.cs similarity index 92% rename from samples/BindingTest/ViewModels/DataAnnotationsErrorViewModel.cs rename to samples/BindingDemo/ViewModels/DataAnnotationsErrorViewModel.cs index 634498c165..e274f9180e 100644 --- a/samples/BindingTest/ViewModels/DataAnnotationsErrorViewModel.cs +++ b/samples/BindingDemo/ViewModels/DataAnnotationsErrorViewModel.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; -namespace BindingTest.ViewModels +namespace BindingDemo.ViewModels { public class DataAnnotationsErrorViewModel { diff --git a/samples/BindingTest/ViewModels/ExceptionErrorViewModel.cs b/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs similarity index 95% rename from samples/BindingTest/ViewModels/ExceptionErrorViewModel.cs rename to samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs index e6071e0678..2ab6c26e68 100644 --- a/samples/BindingTest/ViewModels/ExceptionErrorViewModel.cs +++ b/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs @@ -4,7 +4,7 @@ using ReactiveUI; using System; -namespace BindingTest.ViewModels +namespace BindingDemo.ViewModels { public class ExceptionErrorViewModel : ReactiveObject { diff --git a/samples/BindingTest/ViewModels/IndeiErrorViewModel.cs b/samples/BindingDemo/ViewModels/IndeiErrorViewModel.cs similarity index 98% rename from samples/BindingTest/ViewModels/IndeiErrorViewModel.cs rename to samples/BindingDemo/ViewModels/IndeiErrorViewModel.cs index b4bb528abb..bb3b4d64e9 100644 --- a/samples/BindingTest/ViewModels/IndeiErrorViewModel.cs +++ b/samples/BindingDemo/ViewModels/IndeiErrorViewModel.cs @@ -6,7 +6,7 @@ using System; using System.ComponentModel; using System.Collections; -namespace BindingTest.ViewModels +namespace BindingDemo.ViewModels { public class IndeiErrorViewModel : ReactiveObject, INotifyDataErrorInfo { diff --git a/samples/BindingTest/ViewModels/MainWindowViewModel.cs b/samples/BindingDemo/ViewModels/MainWindowViewModel.cs similarity index 99% rename from samples/BindingTest/ViewModels/MainWindowViewModel.cs rename to samples/BindingDemo/ViewModels/MainWindowViewModel.cs index 1116810ccb..858fb5159a 100644 --- a/samples/BindingTest/ViewModels/MainWindowViewModel.cs +++ b/samples/BindingDemo/ViewModels/MainWindowViewModel.cs @@ -6,7 +6,7 @@ using System.Reactive.Linq; using System.Threading.Tasks; using System.Threading; -namespace BindingTest.ViewModels +namespace BindingDemo.ViewModels { public class MainWindowViewModel : ReactiveObject { diff --git a/samples/BindingTest/ViewModels/NestedCommandViewModel.cs b/samples/BindingDemo/ViewModels/NestedCommandViewModel.cs similarity index 92% rename from samples/BindingTest/ViewModels/NestedCommandViewModel.cs rename to samples/BindingDemo/ViewModels/NestedCommandViewModel.cs index 886ecbed8e..0e9139ab98 100644 --- a/samples/BindingTest/ViewModels/NestedCommandViewModel.cs +++ b/samples/BindingDemo/ViewModels/NestedCommandViewModel.cs @@ -6,7 +6,7 @@ using System.Text; using System.Threading.Tasks; using System.Windows.Input; -namespace BindingTest.ViewModels +namespace BindingDemo.ViewModels { public class NestedCommandViewModel : ReactiveObject { diff --git a/samples/BindingTest/ViewModels/TestItem.cs b/samples/BindingDemo/ViewModels/TestItem.cs similarity index 93% rename from samples/BindingTest/ViewModels/TestItem.cs rename to samples/BindingDemo/ViewModels/TestItem.cs index 2326a92b7d..5a9f192f58 100644 --- a/samples/BindingTest/ViewModels/TestItem.cs +++ b/samples/BindingDemo/ViewModels/TestItem.cs @@ -1,6 +1,6 @@ using ReactiveUI; -namespace BindingTest.ViewModels +namespace BindingDemo.ViewModels { public class TestItem : ReactiveObject { diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index b45a93455e..1f53dedc14 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -1,9 +1,9 @@ using System; using System.Diagnostics; using System.Linq; -using System.Runtime.InteropServices; using System.Threading; using Avalonia; +using Avalonia.Skia; namespace ControlCatalog.NetCore { @@ -37,7 +37,7 @@ namespace ControlCatalog.NetCore /// This method is needed for IDE previewer infrastructure /// public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure().UsePlatformDetect().UseReactiveUI(); + => AppBuilder.Configure().UsePlatformDetect().UseSkia().UseReactiveUI(); static void ConsoleSilencer() { diff --git a/samples/RemoteTest/Program.cs b/samples/RemoteDemo/Program.cs similarity index 98% rename from samples/RemoteTest/Program.cs rename to samples/RemoteDemo/Program.cs index f518e77143..0565b676fb 100644 --- a/samples/RemoteTest/Program.cs +++ b/samples/RemoteDemo/Program.cs @@ -9,7 +9,7 @@ using Avalonia.Remote.Protocol; using Avalonia.Threading; using ControlCatalog; -namespace RemoteTest +namespace RemoteDemo { class Program { diff --git a/samples/RemoteTest/RemoteTest.csproj b/samples/RemoteDemo/RemoteDemo.csproj similarity index 100% rename from samples/RemoteTest/RemoteTest.csproj rename to samples/RemoteDemo/RemoteDemo.csproj diff --git a/samples/RenderTest/App.config b/samples/RenderDemo/App.config similarity index 100% rename from samples/RenderTest/App.config rename to samples/RenderDemo/App.config diff --git a/samples/RenderTest/App.xaml b/samples/RenderDemo/App.xaml similarity index 81% rename from samples/RenderTest/App.xaml rename to samples/RenderDemo/App.xaml index c119f54915..aee75cb139 100644 --- a/samples/RenderTest/App.xaml +++ b/samples/RenderDemo/App.xaml @@ -2,6 +2,6 @@ - + \ No newline at end of file diff --git a/samples/RenderTest/App.xaml.cs b/samples/RenderDemo/App.xaml.cs similarity index 97% rename from samples/RenderTest/App.xaml.cs rename to samples/RenderDemo/App.xaml.cs index fd2b940f6a..0f627961e6 100644 --- a/samples/RenderTest/App.xaml.cs +++ b/samples/RenderDemo/App.xaml.cs @@ -5,7 +5,7 @@ using Avalonia; using Avalonia.Logging.Serilog; using Avalonia.Markup.Xaml; -namespace RenderTest +namespace RenderDemo { public class App : Application { diff --git a/samples/RenderTest/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml similarity index 95% rename from samples/RenderTest/MainWindow.xaml rename to samples/RenderDemo/MainWindow.xaml index da49054b77..df2b221423 100644 --- a/samples/RenderTest/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -1,6 +1,6 @@ diff --git a/samples/RenderTest/MainWindow.xaml.cs b/samples/RenderDemo/MainWindow.xaml.cs similarity index 94% rename from samples/RenderTest/MainWindow.xaml.cs rename to samples/RenderDemo/MainWindow.xaml.cs index 76a8e81aca..f1f974f7a1 100644 --- a/samples/RenderTest/MainWindow.xaml.cs +++ b/samples/RenderDemo/MainWindow.xaml.cs @@ -5,10 +5,10 @@ using System; using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; -using RenderTest.ViewModels; +using RenderDemo.ViewModels; using ReactiveUI; -namespace RenderTest +namespace RenderDemo { public class MainWindow : Window { diff --git a/samples/RenderTest/Pages/AnimationsPage.xaml b/samples/RenderDemo/Pages/AnimationsPage.xaml similarity index 100% rename from samples/RenderTest/Pages/AnimationsPage.xaml rename to samples/RenderDemo/Pages/AnimationsPage.xaml diff --git a/samples/RenderTest/Pages/AnimationsPage.xaml.cs b/samples/RenderDemo/Pages/AnimationsPage.xaml.cs similarity index 90% rename from samples/RenderTest/Pages/AnimationsPage.xaml.cs rename to samples/RenderDemo/Pages/AnimationsPage.xaml.cs index 2623721393..5b02fd9297 100644 --- a/samples/RenderTest/Pages/AnimationsPage.xaml.cs +++ b/samples/RenderDemo/Pages/AnimationsPage.xaml.cs @@ -7,9 +7,9 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Markup.Xaml; using Avalonia.Media; -using RenderTest.ViewModels; +using RenderDemo.ViewModels; -namespace RenderTest.Pages +namespace RenderDemo.Pages { public class AnimationsPage : UserControl { diff --git a/samples/RenderTest/Pages/ClippingPage.xaml b/samples/RenderDemo/Pages/ClippingPage.xaml similarity index 100% rename from samples/RenderTest/Pages/ClippingPage.xaml rename to samples/RenderDemo/Pages/ClippingPage.xaml diff --git a/samples/RenderTest/Pages/ClippingPage.xaml.cs b/samples/RenderDemo/Pages/ClippingPage.xaml.cs similarity index 96% rename from samples/RenderTest/Pages/ClippingPage.xaml.cs rename to samples/RenderDemo/Pages/ClippingPage.xaml.cs index 2a79076d4c..5357181838 100644 --- a/samples/RenderTest/Pages/ClippingPage.xaml.cs +++ b/samples/RenderDemo/Pages/ClippingPage.xaml.cs @@ -7,7 +7,7 @@ using Avalonia.Data; using Avalonia.Markup.Xaml; using Avalonia.Media; -namespace RenderTest.Pages +namespace RenderDemo.Pages { public class ClippingPage : UserControl { diff --git a/samples/RenderTest/Pages/DrawingPage.xaml b/samples/RenderDemo/Pages/DrawingPage.xaml similarity index 100% rename from samples/RenderTest/Pages/DrawingPage.xaml rename to samples/RenderDemo/Pages/DrawingPage.xaml diff --git a/samples/RenderTest/Pages/DrawingPage.xaml.cs b/samples/RenderDemo/Pages/DrawingPage.xaml.cs similarity index 91% rename from samples/RenderTest/Pages/DrawingPage.xaml.cs rename to samples/RenderDemo/Pages/DrawingPage.xaml.cs index 3bf9bd545d..3475e1aa07 100644 --- a/samples/RenderTest/Pages/DrawingPage.xaml.cs +++ b/samples/RenderDemo/Pages/DrawingPage.xaml.cs @@ -1,7 +1,7 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; -namespace RenderTest.Pages +namespace RenderDemo.Pages { public class DrawingPage : UserControl { diff --git a/samples/RenderTest/RenderTest.csproj b/samples/RenderDemo/RenderDemo.csproj similarity index 100% rename from samples/RenderTest/RenderTest.csproj rename to samples/RenderDemo/RenderDemo.csproj diff --git a/samples/RenderTest/SideBar.xaml b/samples/RenderDemo/SideBar.xaml similarity index 100% rename from samples/RenderTest/SideBar.xaml rename to samples/RenderDemo/SideBar.xaml diff --git a/samples/RenderTest/ViewModels/AnimationsPageViewModel.cs b/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs similarity index 97% rename from samples/RenderTest/ViewModels/AnimationsPageViewModel.cs rename to samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs index 17eee547a1..626a3e7c77 100644 --- a/samples/RenderTest/ViewModels/AnimationsPageViewModel.cs +++ b/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs @@ -2,7 +2,7 @@ using ReactiveUI; using Avalonia.Animation; -namespace RenderTest.ViewModels +namespace RenderDemo.ViewModels { public class AnimationsPageViewModel : ReactiveObject { diff --git a/samples/RenderTest/ViewModels/MainWindowViewModel.cs b/samples/RenderDemo/ViewModels/MainWindowViewModel.cs similarity index 96% rename from samples/RenderTest/ViewModels/MainWindowViewModel.cs rename to samples/RenderDemo/ViewModels/MainWindowViewModel.cs index 02a2abeb89..0cb5e1b87b 100644 --- a/samples/RenderTest/ViewModels/MainWindowViewModel.cs +++ b/samples/RenderDemo/ViewModels/MainWindowViewModel.cs @@ -1,7 +1,7 @@ using System; using ReactiveUI; -namespace RenderTest.ViewModels +namespace RenderDemo.ViewModels { public class MainWindowViewModel : ReactiveObject { diff --git a/samples/VirtualizationTest/App.config b/samples/VirtualizationDemo/App.config similarity index 100% rename from samples/VirtualizationTest/App.config rename to samples/VirtualizationDemo/App.config diff --git a/samples/VirtualizationTest/App.xaml b/samples/VirtualizationDemo/App.xaml similarity index 100% rename from samples/VirtualizationTest/App.xaml rename to samples/VirtualizationDemo/App.xaml diff --git a/samples/VirtualizationTest/App.xaml.cs b/samples/VirtualizationDemo/App.xaml.cs similarity index 92% rename from samples/VirtualizationTest/App.xaml.cs rename to samples/VirtualizationDemo/App.xaml.cs index 14ab5b3f84..b220807443 100644 --- a/samples/VirtualizationTest/App.xaml.cs +++ b/samples/VirtualizationDemo/App.xaml.cs @@ -4,7 +4,7 @@ using Avalonia; using Avalonia.Markup.Xaml; -namespace VirtualizationTest +namespace VirtualizationDemo { public class App : Application { diff --git a/samples/VirtualizationTest/MainWindow.xaml b/samples/VirtualizationDemo/MainWindow.xaml similarity index 100% rename from samples/VirtualizationTest/MainWindow.xaml rename to samples/VirtualizationDemo/MainWindow.xaml diff --git a/samples/VirtualizationTest/MainWindow.xaml.cs b/samples/VirtualizationDemo/MainWindow.xaml.cs similarity index 89% rename from samples/VirtualizationTest/MainWindow.xaml.cs rename to samples/VirtualizationDemo/MainWindow.xaml.cs index 952383dffb..271519b10b 100644 --- a/samples/VirtualizationTest/MainWindow.xaml.cs +++ b/samples/VirtualizationDemo/MainWindow.xaml.cs @@ -4,9 +4,9 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; -using VirtualizationTest.ViewModels; +using VirtualizationDemo.ViewModels; -namespace VirtualizationTest +namespace VirtualizationDemo { public class MainWindow : Window { diff --git a/samples/VirtualizationTest/Program.cs b/samples/VirtualizationDemo/Program.cs similarity index 94% rename from samples/VirtualizationTest/Program.cs rename to samples/VirtualizationDemo/Program.cs index 097f0cfdc7..98f1f08d6c 100644 --- a/samples/VirtualizationTest/Program.cs +++ b/samples/VirtualizationDemo/Program.cs @@ -7,7 +7,7 @@ using Avalonia.Controls; using Avalonia.Logging.Serilog; using Serilog; -namespace VirtualizationTest +namespace VirtualizationDemo { class Program { diff --git a/samples/VirtualizationTest/ViewModels/ItemViewModel.cs b/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs similarity index 92% rename from samples/VirtualizationTest/ViewModels/ItemViewModel.cs rename to samples/VirtualizationDemo/ViewModels/ItemViewModel.cs index 75777012c1..e883cdfeb9 100644 --- a/samples/VirtualizationTest/ViewModels/ItemViewModel.cs +++ b/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs @@ -4,7 +4,7 @@ using System; using ReactiveUI; -namespace VirtualizationTest.ViewModels +namespace VirtualizationDemo.ViewModels { internal class ItemViewModel : ReactiveObject { diff --git a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs similarity index 99% rename from samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs rename to samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs index 8eab91e06d..eb08ef9656 100644 --- a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs @@ -9,7 +9,7 @@ using Avalonia.Controls; using Avalonia.Controls.Primitives; using ReactiveUI; -namespace VirtualizationTest.ViewModels +namespace VirtualizationDemo.ViewModels { internal class MainWindowViewModel : ReactiveObject { diff --git a/samples/VirtualizationTest/VirtualizationTest.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj similarity index 100% rename from samples/VirtualizationTest/VirtualizationTest.csproj rename to samples/VirtualizationDemo/VirtualizationDemo.csproj diff --git a/src/Avalonia.Controls/Platform/InProcessDragSource.cs b/src/Avalonia.Controls/Platform/InProcessDragSource.cs index 52e09aba45..f8ae9f4249 100644 --- a/src/Avalonia.Controls/Platform/InProcessDragSource.cs +++ b/src/Avalonia.Controls/Platform/InProcessDragSource.cs @@ -60,7 +60,7 @@ namespace Avalonia.Platform { _lastPosition = pt; - RawDragEvent rawEvent = new RawDragEvent(_dragDrop, type, root, pt, _draggedData, _allowedEffects); + RawDragEvent rawEvent = new RawDragEvent(_dragDrop, type, root, pt, _draggedData, _allowedEffects, modifiers); var tl = root.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); tl.PlatformImpl?.Input(rawEvent); diff --git a/src/Avalonia.Input/DragDropDevice.cs b/src/Avalonia.Input/DragDropDevice.cs index 9fb100371f..0692b21c66 100644 --- a/src/Avalonia.Input/DragDropDevice.cs +++ b/src/Avalonia.Input/DragDropDevice.cs @@ -19,11 +19,11 @@ namespace Avalonia.Input return null; } - private DragDropEffects RaiseDragEvent(Interactive target, IInputElement inputRoot, Point point, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data) + private DragDropEffects RaiseDragEvent(Interactive target, IInputElement inputRoot, Point point, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data, InputModifiers modifiers) { if (target == null) return DragDropEffects.None; - var args = new DragEventArgs(routedEvent, data, target, inputRoot.TranslatePoint(point, target)) + var args = new DragEventArgs(routedEvent, data, target, inputRoot.TranslatePoint(point, target), modifiers) { RoutedEvent = routedEvent, DragEffects = operation @@ -32,24 +32,24 @@ namespace Avalonia.Input return args.DragEffects; } - private DragDropEffects DragEnter(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects) + private DragDropEffects DragEnter(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) { _lastTarget = GetTarget(inputRoot, point); - return RaiseDragEvent(_lastTarget, inputRoot, point, DragDrop.DragEnterEvent, effects, data); + return RaiseDragEvent(_lastTarget, inputRoot, point, DragDrop.DragEnterEvent, effects, data, modifiers); } - private DragDropEffects DragOver(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects) + private DragDropEffects DragOver(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) { var target = GetTarget(inputRoot, point); if (target == _lastTarget) - return RaiseDragEvent(target, inputRoot, point, DragDrop.DragOverEvent, effects, data); + return RaiseDragEvent(target, inputRoot, point, DragDrop.DragOverEvent, effects, data, modifiers); try { if (_lastTarget != null) _lastTarget.RaiseEvent(new RoutedEventArgs(DragDrop.DragLeaveEvent)); - return RaiseDragEvent(target, inputRoot, point, DragDrop.DragEnterEvent, effects, data); + return RaiseDragEvent(target, inputRoot, point, DragDrop.DragEnterEvent, effects, data, modifiers); } finally { @@ -71,11 +71,11 @@ namespace Avalonia.Input } } - private DragDropEffects Drop(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects) + private DragDropEffects Drop(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) { try { - return RaiseDragEvent(_lastTarget, inputRoot, point, DragDrop.DropEvent, effects, data); + return RaiseDragEvent(_lastTarget, inputRoot, point, DragDrop.DropEvent, effects, data, modifiers); } finally { @@ -94,16 +94,16 @@ namespace Avalonia.Input switch (e.Type) { case RawDragEventType.DragEnter: - e.Effects = DragEnter(e.InputRoot, e.Location, e.Data, e.Effects); + e.Effects = DragEnter(e.InputRoot, e.Location, e.Data, e.Effects, e.Modifiers); break; case RawDragEventType.DragOver: - e.Effects = DragOver(e.InputRoot, e.Location, e.Data, e.Effects); + e.Effects = DragOver(e.InputRoot, e.Location, e.Data, e.Effects, e.Modifiers); break; case RawDragEventType.DragLeave: DragLeave(e.InputRoot); break; case RawDragEventType.Drop: - e.Effects = Drop(e.InputRoot, e.Location, e.Data, e.Effects); + e.Effects = Drop(e.InputRoot, e.Location, e.Data, e.Effects, e.Modifiers); break; } } diff --git a/src/Avalonia.Input/DragEventArgs.cs b/src/Avalonia.Input/DragEventArgs.cs index 669fd846a1..915ee4ee5c 100644 --- a/src/Avalonia.Input/DragEventArgs.cs +++ b/src/Avalonia.Input/DragEventArgs.cs @@ -13,6 +13,8 @@ namespace Avalonia.Input public IDataObject Data { get; private set; } + public InputModifiers Modifiers { get; private set; } + public Point GetPosition(IVisual relativeTo) { var point = new Point(0, 0); @@ -29,12 +31,13 @@ namespace Avalonia.Input return point; } - public DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation) + public DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, InputModifiers modifiers) : base(routedEvent) { this.Data = data; this._target = target; this._targetLocation = targetLocation; + this.Modifiers = modifiers; } } diff --git a/src/Avalonia.Input/Raw/RawDragEvent.cs b/src/Avalonia.Input/Raw/RawDragEvent.cs index 49125b4c07..80653b4873 100644 --- a/src/Avalonia.Input/Raw/RawDragEvent.cs +++ b/src/Avalonia.Input/Raw/RawDragEvent.cs @@ -11,9 +11,10 @@ namespace Avalonia.Input.Raw public IDataObject Data { get; } public DragDropEffects Effects { get; set; } public RawDragEventType Type { get; } + public InputModifiers Modifiers { get; } public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type, - IInputElement inputRoot, Point location, IDataObject data, DragDropEffects effects) + IInputElement inputRoot, Point location, IDataObject data, DragDropEffects effects, InputModifiers modifiers) :base(inputDevice, 0) { Type = type; @@ -21,6 +22,7 @@ namespace Avalonia.Input.Raw Location = location; Data = data; Effects = effects; + Modifiers = modifiers; } } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/PathGeometry.cs b/src/Avalonia.Visuals/Media/PathGeometry.cs index ecda07ada1..e7a2c8577a 100644 --- a/src/Avalonia.Visuals/Media/PathGeometry.cs +++ b/src/Avalonia.Visuals/Media/PathGeometry.cs @@ -5,6 +5,7 @@ using System; using Avalonia.Collections; using Avalonia.Metadata; using Avalonia.Platform; +using Avalonia.Visuals.Platform; namespace Avalonia.Media { @@ -28,7 +29,7 @@ namespace Avalonia.Media static PathGeometry() { - FiguresProperty.Changed.AddClassHandler((s, e) => + FiguresProperty.Changed.AddClassHandler((s, e) => s.OnFiguresChanged(e.NewValue as PathFigures)); } @@ -40,6 +41,24 @@ namespace Avalonia.Media Figures = new PathFigures(); } + /// + /// Parses the specified path data to a . + /// + /// The s. + /// + public static new PathGeometry Parse(string pathData) + { + var pathGeometry = new PathGeometry(); + + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) + { + parser.Parse(pathData); + } + + return pathGeometry; + } + /// /// Gets or sets the figures. /// diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index 9e4a3cbeae..a322d404bf 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -5,50 +5,61 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Text; +using System.Text.RegularExpressions; +using Avalonia.Platform; namespace Avalonia.Media { /// /// Parses a path markup string. /// - public class PathMarkupParser + public class PathMarkupParser : IDisposable { - private static readonly Dictionary Commands = new Dictionary - { - { 'F', Command.FillRule }, - { 'M', Command.Move }, - { 'L', Command.Line }, - { 'H', Command.HorizontalLine }, - { 'V', Command.VerticalLine }, - { 'Q', Command.QuadraticBezierCurve }, - { 'T', Command.SmoothQuadraticBezierCurve }, - { 'C', Command.CubicBezierCurve }, - { 'S', Command.SmoothCubicBezierCurve }, - { 'A', Command.Arc }, - { 'Z', Command.Close }, - }; - - private static readonly Dictionary FillRules = new Dictionary + private static readonly string s_separatorPattern; + private static readonly Dictionary s_commands = + new Dictionary + { + { 'F', Command.FillRule }, + { 'M', Command.Move }, + { 'L', Command.Line }, + { 'H', Command.HorizontalLine }, + { 'V', Command.VerticalLine }, + { 'Q', Command.QuadraticBezierCurve }, + { 'T', Command.SmoothQuadraticBezierCurve }, + { 'C', Command.CubicBezierCurve }, + { 'S', Command.SmoothCubicBezierCurve }, + { 'A', Command.Arc }, + { 'Z', Command.Close }, + }; + + private IGeometryContext _geometryContext; + private Point _currentPoint; + private Point? _previousControlPoint; + private bool? _isOpen; + private bool _isDisposed; + + static PathMarkupParser() { - {'0', FillRule.EvenOdd }, - {'1', FillRule.NonZero } - }; - - private readonly StreamGeometryContext _context; + s_separatorPattern = CreatesSeparatorPattern(); + } /// /// Initializes a new instance of the class. /// - /// The context for the geometry. - public PathMarkupParser(StreamGeometryContext context) + /// The geometry context. + /// geometryContext + public PathMarkupParser(IGeometryContext geometryContext) { - _context = context; + if (geometryContext == null) + { + throw new ArgumentNullException(nameof(geometryContext)); + } + + _geometryContext = geometryContext; } - /// - /// Defines the command currently being processed. - /// private enum Command { None, @@ -62,358 +73,610 @@ namespace Avalonia.Media SmoothCubicBezierCurve, SmoothQuadraticBezierCurve, Arc, - Close, + Close } /// - /// Parses the specified markup string. + /// Parses the specified path data and writes the result to the geometryContext of this instance. /// - /// The markup string. - public void Parse(string s) + /// The path data. + public void Parse(string pathData) + { + var normalizedPathData = NormalizeWhiteSpaces(pathData); + var tokens = ParseTokens(normalizedPathData); + + CreateGeometry(tokens); + } + + void IDisposable.Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (_isDisposed) + { + return; + } + + if (disposing) + { + _geometryContext = null; + } + + _isDisposed = true; + } + + private static string NormalizeWhiteSpaces(string s) + { + int length = s.Length, + index = 0, + i = 0; + var source = s.ToCharArray(); + var skip = false; + + for (; i < length; i++) + { + var c = source[i]; + + if (char.IsWhiteSpace(c)) + { + if (skip) + { + continue; + } + + source[index++] = c; + + skip = true; + + continue; + } + + skip = false; + + source[index++] = c; + } + + if (char.IsWhiteSpace(source[index - 1])) + { + index--; + } + + return char.IsWhiteSpace(source[0]) ? new string(source, 1, index) : new string(source, 0, index); + } + + private static string CreatesSeparatorPattern() { - bool openFigure = false; + var stringBuilder = new StringBuilder(); - using (StringReader reader = new StringReader(s)) + foreach (var command in s_commands.Keys) { - Command command = Command.None; - Point point = new Point(); - bool relative = false; - Point? previousControlPoint = null; + stringBuilder.Append(command); + + stringBuilder.Append(char.ToLower(command)); + } - while (ReadCommand(reader, ref command, ref relative)) + return @"(?=[" + stringBuilder + "])"; + } + + private static IEnumerable ParseTokens(string s) + { + var expressions = Regex.Split(s, s_separatorPattern).Where(t => !string.IsNullOrEmpty(t)); + + return expressions.Select(CommandToken.Parse); + } + + private static Point MirrorControlPoint(Point controlPoint, Point center) + { + var dir = controlPoint - center; + + return center + -dir; + } + + private void CreateGeometry(IEnumerable commandTokens) + { + _currentPoint = new Point(); + + foreach (var commandToken in commandTokens) + { + try { - switch (command) + while (true) { - case Command.FillRule: - _context.SetFillRule(ReadFillRule(reader)); - previousControlPoint = null; - break; - - case Command.Move: - if (openFigure) - { - _context.EndFigure(false); - } - - point = ReadPoint(reader, point, relative); - _context.BeginFigure(point, true); - openFigure = true; - previousControlPoint = null; - break; - - case Command.Line: - point = ReadPoint(reader, point, relative); - _context.LineTo(point); - previousControlPoint = null; - break; - - case Command.HorizontalLine: - if (!relative) - { - point = point.WithX(ReadDouble(reader)); - } - else - { - point = new Point(point.X + ReadDouble(reader), point.Y); - } - - _context.LineTo(point); - previousControlPoint = null; - break; - - case Command.VerticalLine: - if (!relative) - { - point = point.WithY(ReadDouble(reader)); - } - else - { - point = new Point(point.X, point.Y + ReadDouble(reader)); - } - - _context.LineTo(point); - previousControlPoint = null; - break; - - case Command.QuadraticBezierCurve: - { - Point handle = ReadPoint(reader, point, relative); - previousControlPoint = handle; - ReadSeparator(reader); - point = ReadPoint(reader, point, relative); - _context.QuadraticBezierTo(handle, point); + switch (commandToken.Command) + { + case Command.None: + break; + case Command.FillRule: + SetFillRule(commandToken); + break; + case Command.Move: + AddMove(commandToken); break; - } - - case Command.SmoothQuadraticBezierCurve: - { - Point end = ReadPoint(reader, point, relative); - - if(previousControlPoint != null) - previousControlPoint = MirrorControlPoint((Point)previousControlPoint, point); - - _context.QuadraticBezierTo(previousControlPoint ?? point, end); - point = end; + case Command.Line: + AddLine(commandToken); break; - } - - case Command.CubicBezierCurve: - { - Point point1 = ReadPoint(reader, point, relative); - ReadSeparator(reader); - Point point2 = ReadPoint(reader, point, relative); - previousControlPoint = point2; - ReadSeparator(reader); - point = ReadPoint(reader, point, relative); - _context.CubicBezierTo(point1, point2, point); + case Command.HorizontalLine: + AddHorizontalLine(commandToken); break; - } - - case Command.SmoothCubicBezierCurve: - { - Point point2 = ReadPoint(reader, point, relative); - ReadSeparator(reader); - Point end = ReadPoint(reader, point, relative); - - if(previousControlPoint != null) - previousControlPoint = MirrorControlPoint((Point)previousControlPoint, point); - - _context.CubicBezierTo(previousControlPoint ?? point, point2, end); - previousControlPoint = point2; - point = end; + case Command.VerticalLine: + AddVerticalLine(commandToken); break; - } - - case Command.Arc: - { - Size size = ReadSize(reader); - ReadSeparator(reader); - double rotationAngle = ReadDouble(reader); - ReadSeparator(reader); - bool isLargeArc = ReadBool(reader); - ReadSeparator(reader); - SweepDirection sweepDirection = ReadBool(reader) ? SweepDirection.Clockwise : SweepDirection.CounterClockwise; - ReadSeparator(reader); - point = ReadPoint(reader, point, relative); - - _context.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection); - previousControlPoint = null; + case Command.CubicBezierCurve: + AddCubicBezierCurve(commandToken); break; - } + case Command.QuadraticBezierCurve: + AddQuadraticBezierCurve(commandToken); + break; + case Command.SmoothCubicBezierCurve: + AddSmoothCubicBezierCurve(commandToken); + break; + case Command.SmoothQuadraticBezierCurve: + AddSmoothQuadraticBezierCurve(commandToken); + break; + case Command.Arc: + AddArc(commandToken); + break; + case Command.Close: + CloseFigure(); + break; + default: + throw new NotSupportedException("Unsupported command"); + } - case Command.Close: - _context.EndFigure(true); - openFigure = false; - previousControlPoint = null; - break; + if (commandToken.HasImplicitCommands) + { + continue; + } - default: - throw new NotSupportedException("Unsupported command"); + break; } } - - if (openFigure) + catch (InvalidDataException) + { + break; + } + catch (NotSupportedException) { - _context.EndFigure(false); + break; } } + + if (_isOpen != null) + { + _geometryContext.EndFigure(false); + } } - private Point MirrorControlPoint(Point controlPoint, Point center) + private void CreateFigure() { - Point dir = (controlPoint - center); - return center + -dir; + if (_isOpen != null) + { + _geometryContext.EndFigure(false); + } + + _geometryContext.BeginFigure(_currentPoint); + + _isOpen = true; + } + + private void SetFillRule(CommandToken commandToken) + { + var fillRule = commandToken.ReadFillRule(); + + _geometryContext.SetFillRule(fillRule); + } + + private void CloseFigure() + { + if (_isOpen == true) + { + _geometryContext.EndFigure(true); + } + + _previousControlPoint = null; + + _isOpen = null; } - private static bool ReadCommand( - StringReader reader, - ref Command command, - ref bool relative) + private void AddMove(CommandToken commandToken) { - ReadWhitespace(reader); + var currentPoint = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); - int i = reader.Peek(); + _currentPoint = currentPoint; - if (i == -1) + CreateFigure(); + + if (!commandToken.HasImplicitCommands) { - return false; + return; } - else + + while (commandToken.HasImplicitCommands) { - char c = (char)i; - Command next = Command.None; + AddLine(commandToken); - if (!Commands.TryGetValue(char.ToUpperInvariant(c), out next)) + if (commandToken.IsRelative) { - if ((char.IsDigit(c) || c == '.' || c == '+' || c == '-') && - (command != Command.None)) - { - return true; - } - else - { - throw new InvalidDataException("Unexpected path command '" + c + "'."); - } + continue; } - command = next; - relative = char.IsLower(c); - reader.Read(); - return true; + _currentPoint = currentPoint; + + CreateFigure(); } } - private static FillRule ReadFillRule(StringReader reader) + private void AddLine(CommandToken commandToken) { - int i = reader.Read(); - if (i == -1) + _currentPoint = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); + + if (_isOpen == null) { - throw new InvalidDataException("Invalid fill rule"); + CreateFigure(); } - char c = (char)i; - FillRule rule; - if (!FillRules.TryGetValue(c, out rule)) + _geometryContext.LineTo(_currentPoint); + } + + private void AddHorizontalLine(CommandToken commandToken) + { + _currentPoint = commandToken.IsRelative + ? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y) + : _currentPoint.WithX(commandToken.ReadDouble()); + + if (_isOpen == null) { - throw new InvalidDataException("Invalid fill rule"); + CreateFigure(); } - return rule; + _geometryContext.LineTo(_currentPoint); } - private static double ReadDouble(StringReader reader) + private void AddVerticalLine(CommandToken commandToken) { - ReadWhitespace(reader); + _currentPoint = commandToken.IsRelative + ? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble()) + : _currentPoint.WithY(commandToken.ReadDouble()); - // TODO: Handle Infinity, NaN and scientific notation. - StringBuilder b = new StringBuilder(); - bool readSign = false; - bool readPoint = false; - bool readExponent = false; - int i; - - while ((i = reader.Peek()) != -1) + if (_isOpen == null) { - char c = char.ToUpperInvariant((char)i); + CreateFigure(); + } - if (((c == '+' || c == '-') && !readSign) || - (c == '.' && !readPoint) || - (c == 'E' && !readExponent) || - char.IsDigit(c)) - { - if (b.Length != 0 && !readExponent && c == '-') - break; - - b.Append(c); - reader.Read(); + _geometryContext.LineTo(_currentPoint); + } - if (!readSign) - { - readSign = c == '+' || c == '-'; - } + private void AddCubicBezierCurve(CommandToken commandToken) + { + var point1 = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); - if (!readPoint) - { - readPoint = c == '.'; - } + var point2 = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); - if (c == 'E') - { - readSign = false; - readExponent = true; - } - } - else - { - break; - } + _previousControlPoint = point2; + + var point3 = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); + + if (_isOpen == null) + { + CreateFigure(); } - return double.Parse(b.ToString(), CultureInfo.InvariantCulture); + _geometryContext.CubicBezierTo(point1, point2, point3); + + _currentPoint = point3; } - private static Point ReadPoint(StringReader reader, Point current, bool relative) + private void AddQuadraticBezierCurve(CommandToken commandToken) { - if (!relative) + var start = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); + + _previousControlPoint = start; + + var end = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); + + if (_isOpen == null) { - current = new Point(); + CreateFigure(); } - ReadWhitespace(reader); - double x = current.X + ReadDouble(reader); - ReadSeparator(reader); - double y = current.Y + ReadDouble(reader); - return new Point(x, y); + _geometryContext.QuadraticBezierTo(start, end); + + _currentPoint = end; } - private static Size ReadSize(StringReader reader) + private void AddSmoothCubicBezierCurve(CommandToken commandToken) { - ReadWhitespace(reader); - double x = ReadDouble(reader); - ReadSeparator(reader); - double y = ReadDouble(reader); - return new Size(x, y); + var point2 = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); + + var end = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); + + if (_previousControlPoint != null) + { + _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); + } + + if (_isOpen == null) + { + CreateFigure(); + } + + _geometryContext.CubicBezierTo(_previousControlPoint ?? _currentPoint, point2, end); + + _previousControlPoint = point2; + + _currentPoint = end; } - private static bool ReadBool(StringReader reader) + private void AddSmoothQuadraticBezierCurve(CommandToken commandToken) { - return ReadDouble(reader) != 0; + var end = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); + + if (_previousControlPoint != null) + { + _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); + } + + if (_isOpen == null) + { + CreateFigure(); + } + + _geometryContext.QuadraticBezierTo(_previousControlPoint ?? _currentPoint, end); + + _currentPoint = end; } - private static Point ReadRelativePoint(StringReader reader, Point lastPoint) + private void AddArc(CommandToken commandToken) { - ReadWhitespace(reader); - double x = ReadDouble(reader); - ReadSeparator(reader); - double y = ReadDouble(reader); - return new Point(lastPoint.X + x, lastPoint.Y + y); + var size = commandToken.ReadSize(); + + var rotationAngle = commandToken.ReadDouble(); + + var isLargeArc = commandToken.ReadBool(); + + var sweepDirection = commandToken.ReadBool() ? SweepDirection.Clockwise : SweepDirection.CounterClockwise; + + var end = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); + + if (_isOpen == null) + { + CreateFigure(); + } + + _geometryContext.ArcTo(end, size, rotationAngle, isLargeArc, sweepDirection); + + _currentPoint = end; + + _previousControlPoint = null; } - private static void ReadSeparator(StringReader reader) + private class CommandToken { - int i; - bool readComma = false; + private const string ArgumentExpression = @"-?[0-9]*\.?\d+"; - while ((i = reader.Peek()) != -1) + private CommandToken(Command command, bool isRelative, IEnumerable arguments) { - char c = (char)i; + Command = command; - if (char.IsWhiteSpace(c)) + IsRelative = isRelative; + + Arguments = new List(arguments); + } + + public Command Command { get; } + + public bool IsRelative { get; } + + public bool HasImplicitCommands + { + get { - reader.Read(); + if (CurrentPosition == 0 && Arguments.Count > 0) + { + return true; + } + + return CurrentPosition < Arguments.Count - 1; } - else if (c == ',') + } + + private int CurrentPosition { get; set; } + + private List Arguments { get; } + + public static CommandToken Parse(string s) + { + using (var reader = new StringReader(s)) { - if (readComma) + var command = Command.None; + + var isRelative = false; + + if (!ReadCommand(reader, ref command, ref isRelative)) + { + throw new InvalidDataException("No path command declared."); + } + + var commandArguments = reader.ReadToEnd(); + + var argumentMatches = Regex.Matches(commandArguments, ArgumentExpression); + + var arguments = new List(); + + foreach (Match match in argumentMatches) { - throw new InvalidDataException("Unexpected ','."); + arguments.Add(match.Value); } - readComma = true; - reader.Read(); + return new CommandToken(command, isRelative, arguments); } - else + } + + public FillRule ReadFillRule() + { + if (CurrentPosition == Arguments.Count) { - break; + throw new InvalidDataException("Invalid fill rule"); + } + + var value = Arguments[CurrentPosition]; + + CurrentPosition++; + + switch (value) + { + case "0": + { + return FillRule.EvenOdd; + } + + case "1": + { + return FillRule.NonZero; + } + + default: + throw new InvalidDataException("Invalid fill rule"); } } - } - private static void ReadWhitespace(StringReader reader) - { - int i; + public bool ReadBool() + { + if (CurrentPosition == Arguments.Count) + { + throw new InvalidDataException("Invalid boolean value"); + } + + var value = Arguments[CurrentPosition]; + + CurrentPosition++; + + switch (value) + { + case "1": + { + return true; + } + + case "0": + { + return false; + } + + default: + throw new InvalidDataException("Invalid boolean value"); + } + } + + public double ReadDouble() + { + if (CurrentPosition == Arguments.Count) + { + throw new InvalidDataException("Invalid double value"); + } + + var value = Arguments[CurrentPosition]; - while ((i = reader.Peek()) != -1) + CurrentPosition++; + + return double.Parse(value, CultureInfo.InvariantCulture); + } + + public Size ReadSize() { - char c = (char)i; + var width = ReadDouble(); - if (char.IsWhiteSpace(c)) + var height = ReadDouble(); + + return new Size(width, height); + } + + public Point ReadPoint() + { + var x = ReadDouble(); + + var y = ReadDouble(); + + return new Point(x, y); + } + + public Point ReadRelativePoint(Point origin) + { + var x = ReadDouble(); + + var y = ReadDouble(); + + return new Point(origin.X + x, origin.Y + y); + } + + private static bool ReadCommand(TextReader reader, ref Command command, ref bool relative) + { + ReadWhitespace(reader); + + var i = reader.Peek(); + + if (i == -1) { - reader.Read(); + return false; } - else + + var c = (char)i; + + if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out var next)) { - break; + throw new InvalidDataException("Unexpected path command '" + c + "'."); + } + + command = next; + + relative = char.IsLower(c); + + reader.Read(); + + return true; + } + + private static void ReadWhitespace(TextReader reader) + { + int i; + + while ((i = reader.Peek()) != -1) + { + var c = (char)i; + + if (char.IsWhiteSpace(c)) + { + reader.Read(); + } + else + { + break; + } } } } diff --git a/src/Avalonia.Visuals/Media/StreamGeometry.cs b/src/Avalonia.Visuals/Media/StreamGeometry.cs index 9848a649aa..d3cb788486 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometry.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometry.cs @@ -35,14 +35,15 @@ namespace Avalonia.Media /// A . public static new StreamGeometry Parse(string s) { - StreamGeometry result = new StreamGeometry(); + var streamGeometry = new StreamGeometry(); - using (StreamGeometryContext ctx = result.Open()) - { - PathMarkupParser parser = new PathMarkupParser(ctx); + using (var context = streamGeometry.Open()) + using (var parser = new PathMarkupParser(context)) + { parser.Parse(s); - return result; } + + return streamGeometry; } /// diff --git a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs index 70b889f817..7521582067 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs @@ -15,7 +15,7 @@ namespace Avalonia.Media /// . /// /// TODO: This class is just a wrapper around IStreamGeometryContextImpl: is it needed? - public class StreamGeometryContext : IDisposable + public class StreamGeometryContext : IGeometryContext { private readonly IStreamGeometryContextImpl _impl; diff --git a/src/Avalonia.Visuals/Platform/IGeometryContext.cs b/src/Avalonia.Visuals/Platform/IGeometryContext.cs new file mode 100644 index 0000000000..ac63837428 --- /dev/null +++ b/src/Avalonia.Visuals/Platform/IGeometryContext.cs @@ -0,0 +1,66 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Media; + +namespace Avalonia.Platform +{ + /// + /// Describes a geometry using drawing commands. + /// + public interface IGeometryContext : IDisposable + { + /// + /// Draws an arc to the specified point. + /// + /// The destination point. + /// The radii of an oval whose perimeter is used to draw the angle. + /// The rotation angle of the oval that specifies the curve. + /// true to draw the arc greater than 180 degrees; otherwise, false. + /// + /// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction. + /// + void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection); + + /// + /// Begins a new figure. + /// + /// The starting point for the figure. + /// Whether the figure is filled. + void BeginFigure(Point startPoint, bool isFilled = true); + + /// + /// Draws a Bezier curve to the specified point. + /// + /// The first control point used to specify the shape of the curve. + /// The second control point used to specify the shape of the curve. + /// The destination point for the end of the curve. + void CubicBezierTo(Point point1, Point point2, Point point3); + + /// + /// Draws a quadratic Bezier curve to the specified point + /// + /// Control point + /// DestinationPoint + void QuadraticBezierTo(Point control, Point endPoint); + + /// + /// Draws a line to the specified point. + /// + /// The destination point. + void LineTo(Point point); + + /// + /// Ends the figure started by . + /// + /// Whether the figure is closed. + void EndFigure(bool isClosed); + + /// + /// Sets the fill rule. + /// + /// The fill rule. + void SetFillRule(FillRule fillRule); + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs b/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs index 386560c6b6..da9505cd2d 100644 --- a/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs @@ -1,62 +1,12 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using Avalonia.Media; - namespace Avalonia.Platform { /// /// Describes a geometry using drawing commands. /// - public interface IStreamGeometryContextImpl : IDisposable - { - /// - /// Draws an arc to the specified point. - /// - /// The destination point. - /// The radii of an oval whose perimeter is used to draw the angle. - /// The rotation angle of the oval that specifies the curve. - /// true to draw the arc greater than 180 degrees; otherwise, false. - /// - /// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction. - /// - void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection); - - /// - /// Begins a new figure. - /// - /// The starting point for the figure. - /// Whether the figure is filled. - void BeginFigure(Point startPoint, bool isFilled); - - /// - /// Draws a Bezier curve to the specified point. - /// - /// The first control point used to specify the shape of the curve. - /// The second control point used to specify the shape of the curve. - /// The destination point for the end of the curve. - void CubicBezierTo(Point point1, Point point2, Point point3); - - /// - /// Draws a quadratic Bezier curve to the specified point - /// - /// Control point - /// DestinationPoint - void QuadraticBezierTo(Point control, Point endPoint); - - /// - /// Draws a line to the specified point. - /// - /// The destination point. - void LineTo(Point point); - - /// - /// Ends the figure started by . - /// - /// Whether the figure is closed. - void EndFigure(bool isClosed); - - void SetFillRule(FillRule fillRule); + public interface IStreamGeometryContextImpl : IGeometryContext + { } } diff --git a/src/Avalonia.Visuals/Platform/PathGeometryContext.cs b/src/Avalonia.Visuals/Platform/PathGeometryContext.cs new file mode 100644 index 0000000000..cc881094fd --- /dev/null +++ b/src/Avalonia.Visuals/Platform/PathGeometryContext.cs @@ -0,0 +1,85 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Media; +using Avalonia.Platform; +using System; + +namespace Avalonia.Visuals.Platform +{ + public class PathGeometryContext : IGeometryContext + { + private PathFigure _currentFigure; + private PathGeometry _pathGeometry; + + public PathGeometryContext(PathGeometry pathGeometry) + { + _pathGeometry = pathGeometry ?? throw new ArgumentNullException(nameof(pathGeometry)); + } + + public void Dispose() + { + _pathGeometry = null; + } + + public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) + { + var arcSegment = new ArcSegment + { + Size = size, + RotationAngle = rotationAngle, + IsLargeArc = isLargeArc, + SweepDirection = sweepDirection, + Point = point + }; + + _currentFigure.Segments.Add(arcSegment); + } + + public void BeginFigure(Point startPoint, bool isFilled) + { + _currentFigure = new PathFigure { StartPoint = startPoint, IsClosed = false, IsFilled = isFilled }; + + _pathGeometry.Figures.Add(_currentFigure); + } + + public void CubicBezierTo(Point point1, Point point2, Point point3) + { + var bezierSegment = new BezierSegment { Point1 = point1, Point2 = point2, Point3 = point3 }; + + _currentFigure.Segments.Add(bezierSegment); + } + + public void QuadraticBezierTo(Point control, Point endPoint) + { + var quadraticBezierSegment = new QuadraticBezierSegment { Point1 = control, Point2 = endPoint }; + + _currentFigure.Segments.Add(quadraticBezierSegment); + } + + public void LineTo(Point point) + { + var lineSegment = new LineSegment + { + Point = point + }; + + _currentFigure.Segments.Add(lineSegment); + } + + public void EndFigure(bool isClosed) + { + if (_currentFigure != null) + { + _currentFigure.IsClosed = isClosed; + } + + _currentFigure = null; + } + + public void SetFillRule(FillRule fillRule) + { + _pathGeometry.FillRule = fillRule; + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index fd6b149837..a14923b410 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -117,6 +117,9 @@ namespace Avalonia.Rendering var scene = Interlocked.Exchange(ref _scene, null); scene?.Dispose(); Stop(); + + Layers.Clear(); + RenderTarget?.Dispose(); } /// diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index e830d5c313..2118b66de2 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs @@ -133,6 +133,7 @@ namespace Avalonia.Rendering /// public void Dispose() { + _renderTarget?.Dispose(); } /// diff --git a/src/Avalonia.Visuals/Rendering/RenderLayers.cs b/src/Avalonia.Visuals/Rendering/RenderLayers.cs index bafd644603..6a45ecd912 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLayers.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLayers.cs @@ -11,11 +11,7 @@ namespace Avalonia.Rendering { private List _inner = new List(); private Dictionary _index = new Dictionary(); - - public RenderLayers() - { - } - + public int Count => _inner.Count; public RenderLayer this[IVisual layerRoot] => _index[layerRoot]; @@ -51,6 +47,16 @@ namespace Avalonia.Rendering } } + public void Clear() + { + foreach (var layer in _index.Values) + { + layer.Bitmap.Dispose(); + } + + _index.Clear(); + } + public bool TryGetValue(IVisual layerRoot, out RenderLayer value) { return _index.TryGetValue(layerRoot, out value); diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index acde49a84a..c2db17cd86 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -3,7 +3,7 @@ using System; using System.Globalization; -using System.Xml.Linq; +using JetBrains.Annotations; namespace Avalonia { @@ -122,6 +122,56 @@ namespace Avalonia return new Vector(a._x - b._x, a._y - b._y); } + /// + /// Check if two vectors are equal (bitwise). + /// + /// + /// + public bool Equals(Vector other) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + return _x == other._x && _y == other._y; + // ReSharper restore CompareOfFloatsByEqualityOperator + } + + /// + /// Check if two vectors are nearly equal (numerically). + /// + /// The other vector. + /// True if vectors are nearly equal. + [Pure] + public bool NearlyEquals(Vector other) + { + const float tolerance = float.Epsilon; + + return Math.Abs(_x - other._x) < tolerance && Math.Abs(_y - other._y) < tolerance; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + + return obj is Vector vector && Equals(vector); + } + + public override int GetHashCode() + { + unchecked + { + return (_x.GetHashCode() * 397) ^ _y.GetHashCode(); + } + } + + public static bool operator ==(Vector left, Vector right) + { + return left.Equals(right); + } + + public static bool operator !=(Vector left, Vector right) + { + return !left.Equals(right); + } + /// /// Returns the string representation of the point. /// diff --git a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs index db7f29f05b..b528d84e4c 100644 --- a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs +++ b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs @@ -164,8 +164,9 @@ namespace Avalonia.MonoMac var dragOp = DraggingInfo.ConvertDragOperation(sender.DraggingSourceOperationMask); DraggingInfo info = new DraggingInfo(sender); + var pt = TranslateLocalPoint(info.Location); - var args = new RawDragEvent(dragDevice, type, root, pt, info, dragOp); + var args = new RawDragEvent(dragDevice, type, root, pt, info, dragOp, GetModifiers(NSEvent.CurrentModifierFlags)); input(args); return DraggingInfo.ConvertDragOperation(args.Effects); } diff --git a/src/Skia/Avalonia.Skia/BitmapImpl.cs b/src/Skia/Avalonia.Skia/BitmapImpl.cs deleted file mode 100644 index ccc5a37105..0000000000 --- a/src/Skia/Avalonia.Skia/BitmapImpl.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.IO; -using Avalonia.Platform; -using Avalonia.Rendering; -using SkiaSharp; - -namespace Avalonia.Skia -{ - class BitmapImpl : IRenderTargetBitmapImpl, IWriteableBitmapImpl - { - private Vector _dpi; - - public SKBitmap Bitmap { get; private set; } - - public BitmapImpl(SKBitmap bm) - { - Bitmap = bm; - PixelHeight = bm.Height; - PixelWidth = bm.Width; - _dpi = new Vector(96, 96); - } - - static void ReleaseProc(IntPtr address, object ctx) - { - ((IUnmanagedBlob) ctx).Dispose(); - } - - private static readonly SKBitmapReleaseDelegate ReleaseDelegate = ReleaseProc; - - public BitmapImpl(int width, int height, Vector dpi, PixelFormat? fmt = null) - { - PixelHeight = height; - PixelWidth = width; - _dpi = dpi; - var colorType = fmt?.ToSkColorType() ?? SKImageInfo.PlatformColorType; - var runtimePlatform = AvaloniaLocator.Current?.GetService(); - var runtime = runtimePlatform?.GetRuntimeInfo(); - if (runtime?.IsDesktop == true && runtime?.OperatingSystem == OperatingSystemType.Linux) - colorType = SKColorType.Bgra8888; - - if (runtimePlatform != null) - { - Bitmap = new SKBitmap(); - var nfo = new SKImageInfo(width, height, colorType, SKAlphaType.Premul); - var plat = AvaloniaLocator.Current.GetService(); - var blob = plat.AllocBlob(nfo.BytesSize); - Bitmap.InstallPixels(nfo, blob.Address, nfo.RowBytes, null, ReleaseDelegate, blob); - - } - else - Bitmap = new SKBitmap(width, height, colorType, SKAlphaType.Premul); - Bitmap.Erase(SKColor.Empty); - } - - public void Dispose() - { - Bitmap.Dispose(); - } - - public int PixelWidth { get; private set; } - public int PixelHeight { get; private set; } - - class BitmapDrawingContext : DrawingContextImpl - { - private readonly SKSurface _surface; - - public BitmapDrawingContext(SKBitmap bitmap, Vector dpi, IVisualBrushRenderer visualBrushRenderer) - : this(CreateSurface(bitmap), dpi, visualBrushRenderer) - { - CanUseLcdRendering = false; - } - - private static SKSurface CreateSurface(SKBitmap bitmap) - { - IntPtr length; - var rv = SKSurface.Create(bitmap.Info, bitmap.GetPixels(out length), bitmap.RowBytes); - if (rv == null) - throw new Exception("Unable to create Skia surface"); - return rv; - } - - public BitmapDrawingContext(SKSurface surface, Vector dpi, IVisualBrushRenderer visualBrushRenderer) - : base(surface.Canvas, dpi, visualBrushRenderer) - { - _surface = surface; - } - - public override void Dispose() - { - base.Dispose(); - _surface.Dispose(); - } - } - - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) - { - return new BitmapDrawingContext(Bitmap, _dpi, visualBrushRenderer); - } - - public void Save(Stream stream) - { - IntPtr length; - using (var image = SKImage.FromPixels(Bitmap.Info, Bitmap.GetPixels(out length), Bitmap.RowBytes)) - using (var data = image.Encode()) - { - data.SaveTo(stream); - } - } - - public void Save(string fileName) - { - using (var stream = File.Create(fileName)) - Save(stream); - } - - class BitmapFramebuffer : ILockedFramebuffer - { - private SKBitmap _bmp; - - public BitmapFramebuffer(SKBitmap bmp) - { - _bmp = bmp; - _bmp.LockPixels(); - } - - public void Dispose() - { - _bmp.UnlockPixels(); - _bmp = null; - } - - public IntPtr Address => _bmp.GetPixels(); - public int Width => _bmp.Width; - public int Height => _bmp.Height; - public int RowBytes => _bmp.RowBytes; - public Vector Dpi { get; } = new Vector(96, 96); - public PixelFormat Format => _bmp.ColorType.ToPixelFormat(); - } - - public ILockedFramebuffer Lock() => new BitmapFramebuffer(Bitmap); - } -} diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 22e5652cfb..b7ce6eedc4 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -1,57 +1,114 @@ -using Avalonia.Media; -using SkiaSharp; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Utilities; using Avalonia.Utilities; +using SkiaSharp; namespace Avalonia.Skia { - internal class DrawingContextImpl : IDrawingContextImpl + /// + /// Skia based drawing context. + /// + public class DrawingContextImpl : IDrawingContextImpl { + private readonly IDisposable[] _disposables; private readonly Vector _dpi; + private readonly Stack _maskStack = new Stack(); + private readonly Stack _opacityStack = new Stack(); private readonly Matrix? _postTransform; - private readonly IDisposable[] _disposables; private readonly IVisualBrushRenderer _visualBrushRenderer; - private Stack maskStack = new Stack(); - protected bool CanUseLcdRendering = true; - public SKCanvas Canvas { get; private set; } - - public DrawingContextImpl( - SKCanvas canvas, - Vector dpi, - IVisualBrushRenderer visualBrushRenderer, - params IDisposable[] disposables) + private double _currentOpacity = 1.0f; + private readonly bool _canTextUseLcdRendering; + private Matrix _currentTransform; + + /// + /// Context create info. + /// + public struct CreateInfo + { + /// + /// Canvas to draw to. + /// + public SKCanvas Canvas; + + /// + /// Dpi of drawings. + /// + public Vector Dpi; + + /// + /// Visual brush renderer. + /// + public IVisualBrushRenderer VisualBrushRenderer; + + /// + /// Render text without Lcd rendering. + /// + public bool DisableTextLcdRendering; + } + + /// + /// Create new drawing context. + /// + /// Create info. + /// Array of elements to dispose after drawing has finished. + public DrawingContextImpl(CreateInfo createInfo, params IDisposable[] disposables) { - _dpi = dpi; - if (dpi.X != 96 || dpi.Y != 96) - _postTransform = Matrix.CreateScale(dpi.X / 96, dpi.Y / 96); - _visualBrushRenderer = visualBrushRenderer; + _dpi = createInfo.Dpi; + _visualBrushRenderer = createInfo.VisualBrushRenderer; _disposables = disposables; - Canvas = canvas; + _canTextUseLcdRendering = !createInfo.DisableTextLcdRendering; + + Canvas = createInfo.Canvas; + + if (Canvas == null) + { + throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo)); + } + + if (!_dpi.NearlyEquals(SkiaPlatform.DefaultDpi)) + { + _postTransform = + Matrix.CreateScale(_dpi.X / SkiaPlatform.DefaultDpi.X, _dpi.Y / SkiaPlatform.DefaultDpi.Y); + } + Transform = Matrix.Identity; } + + /// + /// Skia canvas. + /// + public SKCanvas Canvas { get; } + /// public void Clear(Color color) { Canvas.Clear(color.ToSKColor()); } + /// public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect) { - var impl = (BitmapImpl)source.Item; + var drawableImage = (IDrawableBitmapImpl) source.Item; var s = sourceRect.ToSKRect(); var d = destRect.ToSKRect(); - using (var paint = new SKPaint() - { Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity)) }) + + using (var paint = + new SKPaint {Color = new SKColor(255, 255, 255, (byte) (255 * opacity * _currentOpacity))}) { - Canvas.DrawBitmap(impl.Bitmap, s, d, paint); + drawableImage.Draw(this, s, d, paint); } } + /// public void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { PushOpacityMask(opacityMask, opacityMaskRect); @@ -59,17 +116,19 @@ namespace Avalonia.Skia PopOpacityMask(); } + /// public void DrawLine(Pen pen, Point p1, Point p2) { using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) { - Canvas.DrawLine((float)p1.X, (float)p1.Y, (float)p2.X, (float)p2.Y, paint.Paint); + Canvas.DrawLine((float) p1.X, (float) p1.Y, (float) p2.X, (float) p2.Y, paint.Paint); } } + /// public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) { - var impl = (GeometryImpl)geometry; + var impl = (GeometryImpl) geometry; var size = geometry.Bounds.Size; using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper)) @@ -79,6 +138,7 @@ namespace Avalonia.Skia { Canvas.DrawPath(impl.EffectivePath, fill.Paint); } + if (stroke.Paint != null) { Canvas.DrawPath(impl.EffectivePath, stroke.Paint); @@ -86,227 +146,424 @@ namespace Avalonia.Skia } } - private struct PaintState : IDisposable + /// + public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0) { - private readonly SKColor _color; - private readonly SKShader _shader; - private readonly SKPaint _paint; + using (var paint = CreatePaint(pen, rect.Size)) + { + var rc = rect.ToSKRect(); - public PaintState(SKPaint paint, SKColor color, SKShader shader) + if (Math.Abs(cornerRadius) < float.Epsilon) + { + Canvas.DrawRect(rc, paint.Paint); + } + else + { + Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); + } + } + } + + /// + public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0) + { + using (var paint = CreatePaint(brush, rect.Size)) { - _paint = paint; - _color = color; - _shader = shader; + var rc = rect.ToSKRect(); + + if (Math.Abs(cornerRadius) < float.Epsilon) + { + Canvas.DrawRect(rc, paint.Paint); + } + else + { + Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); + } } + } - public void Dispose() + /// + public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) + { + using (var paint = CreatePaint(foreground, text.Size)) { - _paint.Color = _color; - _paint.Shader = _shader; + var textImpl = (FormattedTextImpl) text; + textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering); } } - internal struct PaintWrapper : IDisposable + /// + public IRenderTargetBitmapImpl CreateLayer(Size size) { - //We are saving memory allocations there - //TODO: add more disposable fields if needed - public readonly SKPaint Paint; + var normalizedDpi = new Vector(_dpi.X / SkiaPlatform.DefaultDpi.X, _dpi.Y / SkiaPlatform.DefaultDpi.Y); + var pixelSize = size * normalizedDpi; - private IDisposable _disposable1; - private IDisposable _disposable2; + return CreateRenderTarget((int) pixelSize.Width, (int) pixelSize.Height, _dpi); + } - public IDisposable ApplyTo(SKPaint paint) - { - var state = new PaintState(paint, paint.Color, paint.Shader); + /// + public void PushClip(Rect clip) + { + Canvas.Save(); + Canvas.ClipRect(clip.ToSKRect()); + } - paint.Color = Paint.Color; - paint.Shader = Paint.Shader; + /// + public void PopClip() + { + Canvas.Restore(); + } - return state; - } + /// + public void PushOpacity(double opacity) + { + _opacityStack.Push(_currentOpacity); + _currentOpacity *= opacity; + } - public void AddDisposable(IDisposable disposable) - { - if (_disposable1 == null) - _disposable1 = disposable; - else if (_disposable2 == null) - _disposable2 = disposable; - else - throw new InvalidOperationException(); - } + /// + public void PopOpacity() + { + _currentOpacity = _opacityStack.Pop(); + } - public PaintWrapper(SKPaint paint) + /// + public virtual void Dispose() + { + if (_disposables == null) { - Paint = paint; - _disposable1 = null; - _disposable2 = null; + return; } - public void Dispose() + foreach (var disposable in _disposables) { - Paint?.Dispose(); - _disposable1?.Dispose(); - _disposable2?.Dispose(); + disposable?.Dispose(); } } - internal PaintWrapper CreatePaint(IBrush brush, Size targetSize) + /// + public void PushGeometryClip(IGeometryImpl clip) { - SKPaint paint = new SKPaint(); - var rv = new PaintWrapper(paint); - paint.IsStroke = false; + Canvas.Save(); + Canvas.ClipPath(((GeometryImpl)clip).EffectivePath); + } - - double opacity = brush.Opacity * _currentOpacity; - paint.IsAntialias = true; + /// + public void PopGeometryClip() + { + Canvas.Restore(); + } + + /// + public void PushOpacityMask(IBrush mask, Rect bounds) + { + // TODO: This should be disposed + var paint = new SKPaint(); + + Canvas.SaveLayer(paint); + _maskStack.Push(CreatePaint(mask, bounds.Size)); + } - var solid = brush as ISolidColorBrush; - if (solid != null) + /// + public void PopOpacityMask() + { + using (var paint = new SKPaint { BlendMode = SKBlendMode.DstIn }) { - paint.Color = new SKColor(solid.Color.R, solid.Color.G, solid.Color.B, (byte) (solid.Color.A * opacity)); - return rv; + Canvas.SaveLayer(paint); + using (var paintWrapper = _maskStack.Pop()) + { + Canvas.DrawPaint(paintWrapper.Paint); + } + Canvas.Restore(); } - paint.Color = (new SKColor(255, 255, 255, (byte)(255 * opacity))); - var gradient = brush as IGradientBrush; - if (gradient != null) + Canvas.Restore(); + } + + /// + public Matrix Transform + { + get { return _currentTransform; } + set { - var tileMode = gradient.SpreadMethod.ToSKShaderTileMode(); - var stopColors = gradient.GradientStops.Select(s => s.Color.ToSKColor()).ToArray(); - var stopOffsets = gradient.GradientStops.Select(s => (float)s.Offset).ToArray(); + if (_currentTransform == value) + return; + + _currentTransform = value; + + var transform = value; - var linearGradient = brush as ILinearGradientBrush; - if (linearGradient != null) + if (_postTransform.HasValue) + { + transform *= _postTransform.Value; + } + + Canvas.SetMatrix(transform.ToSKMatrix()); + } + } + + /// + /// Configure paint wrapper for using gradient brush. + /// + /// Paint wrapper. + /// Target size. + /// Gradient brush. + private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Size targetSize, IGradientBrush gradientBrush) + { + var tileMode = gradientBrush.SpreadMethod.ToSKShaderTileMode(); + var stopColors = gradientBrush.GradientStops.Select(s => s.Color.ToSKColor()).ToArray(); + var stopOffsets = gradientBrush.GradientStops.Select(s => (float)s.Offset).ToArray(); + + switch (gradientBrush) + { + case ILinearGradientBrush linearGradient: { var start = linearGradient.StartPoint.ToPixels(targetSize).ToSKPoint(); var end = linearGradient.EndPoint.ToPixels(targetSize).ToSKPoint(); // would be nice to cache these shaders possibly? - using (var shader = SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode)) - paint.Shader = shader; + using (var shader = + SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode)) + { + paintWrapper.Paint.Shader = shader; + } + break; } - else + case IRadialGradientBrush radialGradient: { - var radialGradient = brush as IRadialGradientBrush; - if (radialGradient != null) - { - var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint(); - var radius = (float)radialGradient.Radius; + var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint(); + var radius = (float)(radialGradient.Radius * targetSize.Width); - // TODO: There is no SetAlpha in SkiaSharp - //paint.setAlpha(128); - - // would be nice to cache these shaders possibly? - using (var shader = SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode)) - paint.Shader = shader; + // TODO: There is no SetAlpha in SkiaSharp + //paint.setAlpha(128); + // would be nice to cache these shaders possibly? + using (var shader = + SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode)) + { + paintWrapper.Paint.Shader = shader; } + + break; } + } + } + + /// + /// Configure paint wrapper for using tile brush. + /// + /// Paint wrapper. + /// Target size. + /// Tile brush to use. + /// Tile brush image. + private void ConfigureTileBrush(ref PaintWrapper paintWrapper, Size targetSize, ITileBrush tileBrush, IDrawableBitmapImpl tileBrushImage) + { + var calc = new TileBrushCalculator(tileBrush, + new Size(tileBrushImage.PixelWidth, tileBrushImage.PixelHeight), targetSize); + + var intermediate = CreateRenderTarget( + (int)calc.IntermediateSize.Width, + (int)calc.IntermediateSize.Height, _dpi); + + paintWrapper.AddDisposable(intermediate); + + using (var context = intermediate.CreateDrawingContext(null)) + { + var rect = new Rect(0, 0, tileBrushImage.PixelWidth, tileBrushImage.PixelHeight); - return rv; + context.Clear(Colors.Transparent); + context.PushClip(calc.IntermediateClip); + context.Transform = calc.IntermediateTransform; + context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect); + context.PopClip(); } - var tileBrush = brush as ITileBrush; - var visualBrush = brush as IVisualBrush; - var tileBrushImage = default(BitmapImpl); + var tileTransform = + tileBrush.TileMode != TileMode.None + ? SKMatrix.MakeTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y) + : SKMatrix.MakeIdentity(); - if (visualBrush != null) + SKShaderTileMode tileX = + tileBrush.TileMode == TileMode.None + ? SKShaderTileMode.Clamp + : tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY + ? SKShaderTileMode.Mirror + : SKShaderTileMode.Repeat; + + SKShaderTileMode tileY = + tileBrush.TileMode == TileMode.None + ? SKShaderTileMode.Clamp + : tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY + ? SKShaderTileMode.Mirror + : SKShaderTileMode.Repeat; + + + var image = intermediate.SnapshotImage(); + paintWrapper.AddDisposable(image); + + using (var shader = image.ToShader(tileX, tileY, tileTransform)) { - if (_visualBrushRenderer != null) - { - var intermediateSize = _visualBrushRenderer.GetRenderTargetSize(visualBrush); + paintWrapper.Paint.Shader = shader; + } + } - if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) - { - var intermediate = new BitmapImpl((int)intermediateSize.Width, (int)intermediateSize.Height, _dpi); + /// + /// Configure paint wrapper to use visual brush. + /// + /// Paint wrapper. + /// Visual brush. + /// Visual brush renderer. + /// Tile brush image. + private void ConfigureVisualBrush(ref PaintWrapper paintWrapper, IVisualBrush visualBrush, IVisualBrushRenderer visualBrushRenderer, ref IDrawableBitmapImpl tileBrushImage) + { + if (_visualBrushRenderer == null) + { + throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl."); + } - using (var ctx = intermediate.CreateDrawingContext(_visualBrushRenderer)) - { - ctx.Clear(Colors.Transparent); - _visualBrushRenderer.RenderVisualBrush(ctx, visualBrush); - } + var intermediateSize = visualBrushRenderer.GetRenderTargetSize(visualBrush); - tileBrushImage = intermediate; - rv.AddDisposable(tileBrushImage); - } - } - else + if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) + { + var intermediate = CreateRenderTarget((int)intermediateSize.Width, (int)intermediateSize.Height, _dpi); + + using (var ctx = intermediate.CreateDrawingContext(visualBrushRenderer)) { - throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl."); + ctx.Clear(Colors.Transparent); + + visualBrushRenderer.RenderVisualBrush(ctx, visualBrush); } + + tileBrushImage = intermediate; + paintWrapper.AddDisposable(tileBrushImage); } - else + } + + /// + /// Creates paint wrapper for given brush. + /// + /// Source brush. + /// Target size. + /// Paint wrapper for given brush. + internal PaintWrapper CreatePaint(IBrush brush, Size targetSize) + { + var paint = new SKPaint + { + IsStroke = false, + IsAntialias = true + }; + + var paintWrapper = new PaintWrapper(paint); + + double opacity = brush.Opacity * _currentOpacity; + + if (brush is ISolidColorBrush solid) { - tileBrushImage = (BitmapImpl)((tileBrush as IImageBrush)?.Source?.PlatformImpl.Item); + paint.Color = new SKColor(solid.Color.R, solid.Color.G, solid.Color.B, (byte) (solid.Color.A * opacity)); + + return paintWrapper; } - if (tileBrush != null && tileBrushImage != null) + paint.Color = new SKColor(255, 255, 255, (byte) (255 * opacity)); + + if (brush is IGradientBrush gradient) { - var calc = new TileBrushCalculator(tileBrush, new Size(tileBrushImage.PixelWidth, tileBrushImage.PixelHeight), targetSize); - var bitmap = new BitmapImpl((int)calc.IntermediateSize.Width, (int)calc.IntermediateSize.Height, _dpi); - rv.AddDisposable(bitmap); - using (var context = bitmap.CreateDrawingContext(null)) - { - var rect = new Rect(0, 0, tileBrushImage.PixelWidth, tileBrushImage.PixelHeight); + ConfigureGradientBrush(ref paintWrapper, targetSize, gradient); - context.Clear(Colors.Transparent); - context.PushClip(calc.IntermediateClip); - context.Transform = calc.IntermediateTransform; - context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect); - context.PopClip(); - } + return paintWrapper; + } + + var tileBrush = brush as ITileBrush; + var visualBrush = brush as IVisualBrush; + var tileBrushImage = default(IDrawableBitmapImpl); - SKMatrix translation = SKMatrix.MakeTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y); - SKShaderTileMode tileX = - tileBrush.TileMode == TileMode.None - ? SKShaderTileMode.Clamp - : tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY - ? SKShaderTileMode.Mirror - : SKShaderTileMode.Repeat; - - SKShaderTileMode tileY = - tileBrush.TileMode == TileMode.None - ? SKShaderTileMode.Clamp - : tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY - ? SKShaderTileMode.Mirror - : SKShaderTileMode.Repeat; - using (var shader = SKShader.CreateBitmap(bitmap.Bitmap, tileX, tileY, translation)) - paint.Shader = shader; + if (visualBrush != null) + { + ConfigureVisualBrush(ref paintWrapper, visualBrush, _visualBrushRenderer, ref tileBrushImage); + } + else + { + tileBrushImage = (IDrawableBitmapImpl) (tileBrush as IImageBrush)?.Source?.PlatformImpl.Item; } - return rv; + if (tileBrush != null && tileBrushImage != null) + { + ConfigureTileBrush(ref paintWrapper, targetSize, tileBrush, tileBrushImage); + } + + return paintWrapper; } + /// + /// Creates paint wrapper for given pen. + /// + /// Source pen. + /// Target size. + /// private PaintWrapper CreatePaint(Pen pen, Size targetSize) { var rv = CreatePaint(pen.Brush, targetSize); var paint = rv.Paint; paint.IsStroke = true; - paint.StrokeWidth = (float)pen.Thickness; + paint.StrokeWidth = (float) pen.Thickness; - if (pen.StartLineCap == PenLineCap.Round) - paint.StrokeCap = SKStrokeCap.Round; - else if (pen.StartLineCap == PenLineCap.Square) - paint.StrokeCap = SKStrokeCap.Square; - else - paint.StrokeCap = SKStrokeCap.Butt; + // Need to modify dashes due to Skia modifying their lengths + // https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/paths/dots + // TODO: Still something is off, dashes are now present, but don't look the same as D2D ones. + float dashLengthModifier; + float gapLengthModifier; - if (pen.LineJoin == PenLineJoin.Miter) - paint.StrokeJoin = SKStrokeJoin.Miter; - else if (pen.LineJoin == PenLineJoin.Round) - paint.StrokeJoin = SKStrokeJoin.Round; - else - paint.StrokeJoin = SKStrokeJoin.Bevel; + switch (pen.StartLineCap) + { + case PenLineCap.Round: + paint.StrokeCap = SKStrokeCap.Round; + dashLengthModifier = -paint.StrokeWidth; + gapLengthModifier = paint.StrokeWidth; + break; + case PenLineCap.Square: + paint.StrokeCap = SKStrokeCap.Square; + dashLengthModifier = -paint.StrokeWidth; + gapLengthModifier = paint.StrokeWidth; + break; + default: + paint.StrokeCap = SKStrokeCap.Butt; + dashLengthModifier = 0.0f; + gapLengthModifier = 0.0f; + break; + } + + switch (pen.LineJoin) + { + case PenLineJoin.Miter: + paint.StrokeJoin = SKStrokeJoin.Miter; + break; + case PenLineJoin.Round: + paint.StrokeJoin = SKStrokeJoin.Round; + break; + default: + paint.StrokeJoin = SKStrokeJoin.Bevel; + break; + } - paint.StrokeMiter = (float)pen.MiterLimit; + paint.StrokeMiter = (float) pen.MiterLimit; if (pen.DashStyle?.Dashes != null && pen.DashStyle.Dashes.Count > 0) { - var pe = SKPathEffect.CreateDash( - pen.DashStyle?.Dashes.Select(x => (float)x).ToArray(), - (float)pen.DashStyle.Offset); + var srcDashes = pen.DashStyle.Dashes; + var dashesArray = new float[srcDashes.Count]; + + for (var i = 0; i < srcDashes.Count; ++i) + { + var lengthModifier = i % 2 == 0 ? dashLengthModifier : gapLengthModifier; + + // Avalonia dash lengths are relative, but Skia takes absolute sizes - need to scale + dashesArray[i] = (float) srcDashes[i] * paint.StrokeWidth + lengthModifier; + } + + var pe = SKPathEffect.CreateDash(dashesArray, (float) pen.DashStyle.Offset); + paint.PathEffect = pe; rv.AddDisposable(pe); } @@ -314,128 +571,118 @@ namespace Avalonia.Skia return rv; } - public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0) + /// + /// Create new render target compatible with this drawing context. + /// + /// Width. + /// Height. + /// Drawing dpi. + /// Pixel format. + /// + private SurfaceRenderTarget CreateRenderTarget(int width, int height, Vector dpi, PixelFormat? format = null) { - using (var paint = CreatePaint(pen, rect.Size)) + var createInfo = new SurfaceRenderTarget.CreateInfo { - var rc = rect.ToSKRect(); - if (cornerRadius == 0) - { - Canvas.DrawRect(rc, paint.Paint); - } - else - { - Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); - } - } + Width = width, + Height = height, + Dpi = dpi, + Format = format, + DisableTextLcdRendering = !_canTextUseLcdRendering + }; + + return new SurfaceRenderTarget(createInfo); } - public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0) + /// + /// Skia cached paint state. + /// + private struct PaintState : IDisposable { - using (var paint = CreatePaint(brush, rect.Size)) + private readonly SKColor _color; + private readonly SKShader _shader; + private readonly SKPaint _paint; + + public PaintState(SKPaint paint, SKColor color, SKShader shader) { - var rc = rect.ToSKRect(); - if (cornerRadius == 0) - { - Canvas.DrawRect(rc, paint.Paint); - } - else - { - Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint); - } + _paint = paint; + _color = color; + _shader = shader; } - } - public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) - { - using (var paint = CreatePaint(foreground, text.Size)) + /// + public void Dispose() { - var textImpl = (FormattedTextImpl)text; - textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, CanUseLcdRendering); + _paint.Color = _color; + _paint.Shader = _shader; } } - public IRenderTargetBitmapImpl CreateLayer(Size size) - { - var pixelSize = size * (_dpi / 96); - return new BitmapImpl((int)pixelSize.Width, (int)pixelSize.Height, _dpi); - } - - public void PushClip(Rect clip) - { - Canvas.Save(); - Canvas.ClipRect(clip.ToSKRect()); - } - - public void PopClip() - { - Canvas.Restore(); - } - - private double _currentOpacity = 1.0f; - private readonly Stack _opacityStack = new Stack(); - - public void PushOpacity(double opacity) + /// + /// Skia paint wrapper. + /// + internal struct PaintWrapper : IDisposable { - _opacityStack.Push(_currentOpacity); - _currentOpacity *= opacity; - } + //We are saving memory allocations there + public readonly SKPaint Paint; - public void PopOpacity() - { - _currentOpacity = _opacityStack.Pop(); - } + private IDisposable _disposable1; + private IDisposable _disposable2; + private IDisposable _disposable3; - public virtual void Dispose() - { - if(_disposables!=null) - foreach (var disposable in _disposables) - disposable?.Dispose(); - } + public PaintWrapper(SKPaint paint) + { + Paint = paint; - public void PushGeometryClip(IGeometryImpl clip) - { - Canvas.Save(); - Canvas.ClipPath(((StreamGeometryImpl)clip).EffectivePath); - } + _disposable1 = null; + _disposable2 = null; + _disposable3 = null; + } - public void PopGeometryClip() - { - Canvas.Restore(); - } + public IDisposable ApplyTo(SKPaint paint) + { + var state = new PaintState(paint, paint.Color, paint.Shader); - public void PushOpacityMask(IBrush mask, Rect bounds) - { - Canvas.SaveLayer(new SKPaint()); - maskStack.Push(CreatePaint(mask, bounds.Size)); - } + paint.Color = Paint.Color; + paint.Shader = Paint.Shader; - public void PopOpacityMask() - { - Canvas.SaveLayer(new SKPaint { BlendMode = SKBlendMode.DstIn }); - using (var paintWrapper = maskStack.Pop()) - { - Canvas.DrawPaint(paintWrapper.Paint); + return state; } - Canvas.Restore(); - Canvas.Restore(); - } - private Matrix _currentTransform; - - public Matrix Transform - { - get { return _currentTransform; } - set + /// + /// Add new disposable to a wrapper. + /// + /// Disposable to add. + public void AddDisposable(IDisposable disposable) { - if (_currentTransform == value) - return; + if (_disposable1 == null) + { + _disposable1 = disposable; + } + else if (_disposable2 == null) + { + _disposable2 = disposable; + } + else if (_disposable3 == null) + { + _disposable3 = disposable; + } + else + { + Debug.Assert(false); - _currentTransform = value; - var transform = value; - if (_postTransform.HasValue) - transform *= _postTransform.Value; - Canvas.SetMatrix(transform.ToSKMatrix()); + // ReSharper disable once HeuristicUnreachableCode + throw new InvalidOperationException( + "PaintWrapper disposable object limit reached. You need to add extra struct fields to support more disposables."); + } + } + + /// + public void Dispose() + { + Paint?.Dispose(); + _disposable1?.Dispose(); + _disposable2?.Dispose(); + _disposable3?.Dispose(); } } } diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 13dcd9669d..d835c83aa6 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -10,6 +10,9 @@ using System.Linq; namespace Avalonia.Skia { + /// + /// Skia formatted text implementation. + /// public class FormattedTextImpl : IFormattedTextImpl { public FormattedTextImpl( @@ -21,7 +24,7 @@ namespace Avalonia.Skia IReadOnlyList spans) { Text = text ?? string.Empty; - + // Replace 0 characters with zero-width spaces (200B) Text = Text.Replace((char)0, (char)0x200B); @@ -250,7 +253,26 @@ namespace Avalonia.Skia { float currX = x; string subStr; + float measure; int len; + float factor; + switch (paint.TextAlign) + { + case SKTextAlign.Left: + factor = 0; + break; + case SKTextAlign.Center: + factor = 0.5f; + break; + case SKTextAlign.Right: + factor = 1; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + var textLine = Text.Substring(line.Start, line.Length); + currX -= paint.MeasureText(textLine) * factor; for (int i = line.Start; i < line.Start + line.Length;) { @@ -268,13 +290,15 @@ namespace Avalonia.Skia } subStr = Text.Substring(i, len); + measure = paint.MeasureText(subStr); + currX += measure * factor; ApplyWrapperTo(ref currentPaint, currentWrapper, ref currd, paint, canUseLcdRendering); canvas.DrawText(subStr, currX, origin.Y + line.Top + _lineOffset, paint); i += len; - currX += paint.MeasureText(subStr); + currX += measure * (1 - factor); } } } @@ -331,7 +355,7 @@ namespace Avalonia.Skia { float measuredWidth; string subText = textInput.Substring(textIndex, stop - textIndex); - lengthBreak = (int)paint.BreakText(subText, maxWidth, out measuredWidth) / 2; + lengthBreak = (int)paint.BreakText(subText, maxWidth, out measuredWidth); } //Check for white space or line breakers before the lengthBreak @@ -430,7 +454,6 @@ namespace Avalonia.Skia private void BuildRects() { // Build character rects - var fm = _paint.FontMetrics; SKTextAlign align = _paint.TextAlign; for (int li = 0; li < _skiaLines.Count; li++) @@ -538,18 +561,16 @@ namespace Avalonia.Skia string subString; - float widthConstraint = (_constraint.Width != double.PositiveInfinity) - ? (float)_constraint.Width - : -1; - - for (int c = 0; curOff < length; c++) + float widthConstraint = double.IsPositiveInfinity(_constraint.Width) + ? -1 + : (float)_constraint.Width; + + while(curOff < length) { float lineWidth = -1; int measured; int trailingnumber = 0; - - subString = Text.Substring(curOff); - + float constraint = -1; if (_wrapping == TextWrapping.Wrap) @@ -561,8 +582,8 @@ namespace Avalonia.Skia measured = LineBreak(Text, curOff, length, _paint, constraint, out trailingnumber); AvaloniaFormattedTextLine line = new AvaloniaFormattedTextLine(); - line.TextLength = measured; line.Start = curOff; + line.TextLength = measured; subString = Text.Substring(line.Start, line.TextLength); lineWidth = _paint.MeasureText(subString); line.Length = measured - trailingnumber; diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index 1956f02d1b..99dbbefd4d 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -1,82 +1,199 @@ -using System; -using System.Collections.Generic; -using System.Text; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Disposables; using Avalonia.Controls.Platform.Surfaces; -using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Skia.Helpers; using SkiaSharp; namespace Avalonia.Skia { + /// + /// Skia render target that renders to a framebuffer surface. No gpu acceleration available. + /// public class FramebufferRenderTarget : IRenderTarget { - private readonly IFramebufferPlatformSurface _surface; + private readonly IFramebufferPlatformSurface _platformSurface; + private SKImageInfo _currentImageInfo; + private IntPtr _currentFramebufferAddress; + private SKSurface _framebufferSurface; + private PixelFormatConversionShim _conversionShim; + private IDisposable _preFramebufferCopyHandler; - public FramebufferRenderTarget(IFramebufferPlatformSurface surface) + /// + /// Create new framebuffer render target using a target surface. + /// + /// Target surface. + public FramebufferRenderTarget(IFramebufferPlatformSurface platformSurface) { - _surface = surface; + _platformSurface = platformSurface; } + /// public void Dispose() { - //Nothing to do here, since we don't own framebuffer + FreeSurface(); + } + + /// + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + { + var framebuffer = _platformSurface.Lock(); + var framebufferImageInfo = new SKImageInfo(framebuffer.Width, framebuffer.Height, + framebuffer.Format.ToSkColorType(), SKAlphaType.Premul); + + CreateSurface(framebufferImageInfo, framebuffer); + + var canvas = _framebufferSurface.Canvas; + + canvas.RestoreToCount(-1); + canvas.Save(); + canvas.ResetMatrix(); + + var createInfo = new DrawingContextImpl.CreateInfo + { + Canvas = canvas, + Dpi = framebuffer.Dpi, + VisualBrushRenderer = visualBrushRenderer, + DisableTextLcdRendering = true + }; + + return new DrawingContextImpl(createInfo, _preFramebufferCopyHandler, framebuffer); + } + + /// + /// Check if two images info are compatible. + /// + /// Current. + /// Desired. + /// True, if images are compatible. + private static bool AreImageInfosCompatible(SKImageInfo currentImageInfo, SKImageInfo desiredImageInfo) + { + return currentImageInfo.Width == desiredImageInfo.Width && + currentImageInfo.Height == desiredImageInfo.Height && + currentImageInfo.ColorType == desiredImageInfo.ColorType; + } + + /// + /// Create Skia surface backed by given framebuffer. + /// + /// Desired image info. + /// Backing framebuffer. + private void CreateSurface(SKImageInfo desiredImageInfo, ILockedFramebuffer framebuffer) + { + if (_framebufferSurface != null && AreImageInfosCompatible(_currentImageInfo, desiredImageInfo) && _currentFramebufferAddress == framebuffer.Address) + { + return; + } + + FreeSurface(); + + _currentFramebufferAddress = framebuffer.Address; + + var surface = SKSurface.Create(desiredImageInfo, _currentFramebufferAddress, framebuffer.RowBytes); + + // If surface cannot be created - try to create a compatibilty shim first + if (surface == null) + { + _conversionShim = new PixelFormatConversionShim(desiredImageInfo, framebuffer.Address); + _preFramebufferCopyHandler = _conversionShim.SurfaceCopyHandler; + + surface = _conversionShim.Surface; + } + + _framebufferSurface = surface ?? throw new Exception("Unable to create a surface for pixel format " + + framebuffer.Format + + " or pixel format translator"); + _currentImageInfo = desiredImageInfo; + } + + /// + /// Free Skia surface. + /// + private void FreeSurface() + { + _conversionShim?.Dispose(); + _conversionShim = null; + _preFramebufferCopyHandler = null; + + if (_conversionShim != null) + { + _framebufferSurface?.Dispose(); + } + + _framebufferSurface = null; + _currentFramebufferAddress = IntPtr.Zero; } - class PixelFormatShim : IDisposable + /// + /// Converts non-compatible pixel formats using bitmap copies. + /// + private class PixelFormatConversionShim : IDisposable { - private readonly SKImageInfo _nfo; - private readonly IntPtr _fb; - private readonly int _rowBytes; - private SKBitmap _bitmap; + private readonly SKBitmap _bitmap; + private readonly SKImageInfo _destinationInfo; + private readonly IntPtr _framebufferAddress; - public PixelFormatShim(SKImageInfo nfo, IntPtr fb, int rowBytes) + public PixelFormatConversionShim(SKImageInfo destinationInfo, IntPtr framebufferAddress) { - _nfo = nfo; - _fb = fb; - _rowBytes = rowBytes; + _destinationInfo = destinationInfo; + _framebufferAddress = framebufferAddress; + + // Create bitmap using default platform settings + _bitmap = new SKBitmap(destinationInfo.Width, destinationInfo.Height); + + if (!_bitmap.CanCopyTo(destinationInfo.ColorType)) + { + _bitmap.Dispose(); + + throw new Exception( + $"Unable to create pixel format shim for conversion from {_bitmap.ColorType} to {destinationInfo.ColorType}"); + } + + Surface = SKSurface.Create(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes); - - _bitmap = new SKBitmap(nfo.Width, nfo.Height); - if (!_bitmap.CanCopyTo(nfo.ColorType)) + if (Surface == null) { _bitmap.Dispose(); + throw new Exception( - $"Unable to create pixel format shim for conversion from {_bitmap.ColorType} to {nfo.ColorType}"); + $"Unable to create pixel format shim surface for conversion from {_bitmap.ColorType} to {destinationInfo.ColorType}"); } + + SurfaceCopyHandler = Disposable.Create(CopySurface); } - public SKSurface CreateSurface() => SKSurface.Create(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes); + /// + /// Skia surface. + /// + public SKSurface Surface { get; } + /// + /// Handler to start conversion via surface copy. + /// + public IDisposable SurfaceCopyHandler { get; } + + /// public void Dispose() { - using (var tmp = _bitmap.Copy(_nfo.ColorType)) - tmp.CopyPixelsTo(_fb, _nfo.BytesPerPixel * _nfo.Height * _rowBytes, _rowBytes); + Surface.Dispose(); _bitmap.Dispose(); } - - } - - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) - { - var fb = _surface.Lock(); - PixelFormatShim shim = null; - SKImageInfo framebuffer = new SKImageInfo(fb.Width, fb.Height, fb.Format.ToSkColorType(), - SKAlphaType.Premul); - var surface = SKSurface.Create(framebuffer, fb.Address, fb.RowBytes) ?? - (shim = new PixelFormatShim(framebuffer, fb.Address, fb.RowBytes)) - .CreateSurface(); - if (surface == null) - throw new Exception("Unable to create a surface for pixel format " + fb.Format + - " or pixel format translator"); - var canvas = surface.Canvas; - - - canvas.RestoreToCount(0); - canvas.Save(); - canvas.ResetMatrix(); - return new DrawingContextImpl(canvas, fb.Dpi, visualBrushRenderer, canvas, surface, shim, fb); + /// + /// Convert and copy surface to a framebuffer. + /// + private void CopySurface() + { + using (var snapshot = Surface.Snapshot()) + { + snapshot.ReadPixels(_destinationInfo, _framebufferAddress, _destinationInfo.RowBytes, 0, 0, + SKImageCachingHint.Disallow); + } + } } } -} +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index fb134b728c..af4cdb8056 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using Avalonia.Media; using Avalonia.Platform; @@ -5,14 +8,169 @@ using SkiaSharp; namespace Avalonia.Skia { - abstract class GeometryImpl : IGeometryImpl + /// + /// A Skia implementation of . + /// + public abstract class GeometryImpl : IGeometryImpl { + private PathCache _pathCache; + + /// public abstract Rect Bounds { get; } public abstract SKPath EffectivePath { get; } - public abstract bool FillContains(Point point); - public abstract Rect GetRenderBounds(Pen pen); - public abstract IGeometryImpl Intersect(IGeometryImpl geometry); - public abstract bool StrokeContains(Pen pen, Point point); - public abstract ITransformedGeometryImpl WithTransform(Matrix transform); + + /// + public bool FillContains(Point point) + { + return PathContainsCore(EffectivePath, point); + } + + /// + public bool StrokeContains(Pen pen, Point point) + { + // Skia requires to compute stroke path to check for point containment. + // Due to that we are caching using stroke width. + // Usually this function is being called with same stroke width per path, so this saves a lot of Skia traffic. + + var strokeWidth = (float)(pen?.Thickness ?? 0); + + if (!_pathCache.HasCacheFor(strokeWidth)) + { + UpdatePathCache(strokeWidth); + } + + return PathContainsCore(_pathCache.CachedStrokePath, point); + } + + /// + /// Update path cache for given stroke width. + /// + /// Stroke width. + private void UpdatePathCache(float strokeWidth) + { + var strokePath = new SKPath(); + + // For stroke widths close to 0 simply use empty path. Render bounds are cached from fill path. + if (Math.Abs(strokeWidth) < float.Epsilon) + { + _pathCache.Cache(strokePath, strokeWidth, Bounds); + } + else + { + using (var paint = new SKPaint()) + { + paint.IsStroke = true; + paint.StrokeWidth = strokeWidth; + + paint.GetFillPath(EffectivePath, strokePath); + + _pathCache.Cache(strokePath, strokeWidth, strokePath.TightBounds.ToAvaloniaRect()); + } + } + } + + /// + /// Check Skia path if it contains a point. + /// + /// Path to check. + /// Point. + /// True, if point is contained in a path. + private static bool PathContainsCore(SKPath path, Point point) + { + return path.Contains((float)point.X, (float)point.Y); + } + + /// + public IGeometryImpl Intersect(IGeometryImpl geometry) + { + var result = EffectivePath.Op(((GeometryImpl) geometry).EffectivePath, SKPathOp.Intersect); + + return result == null ? null : new StreamGeometryImpl(result); + } + + /// + public Rect GetRenderBounds(Pen pen) + { + var strokeWidth = (float)(pen?.Thickness ?? 0); + + if (!_pathCache.HasCacheFor(strokeWidth)) + { + UpdatePathCache(strokeWidth); + } + + return _pathCache.CachedGeometryRenderBounds.Inflate(strokeWidth / 2.0); + } + + /// + public ITransformedGeometryImpl WithTransform(Matrix transform) + { + return new TransformedGeometryImpl(this, transform); + } + + /// + /// Invalidate all caches. Call after chaning path contents. + /// + protected void InvalidateCaches() + { + _pathCache.Invalidate(); + } + + private struct PathCache + { + private float _cachedStrokeWidth; + + /// + /// Tolerance for two stroke widths to be deemed equal + /// + public const float Tolerance = float.Epsilon; + + /// + /// Cached contour path. + /// + public SKPath CachedStrokePath { get; private set; } + + /// + /// Cached geometry render bounds. + /// + public Rect CachedGeometryRenderBounds { get; private set; } + + /// + /// Is cached valid for given stroke width. + /// + /// Stroke width to check. + /// True, if CachedStrokePath can be used for given stroke width. + public bool HasCacheFor(float strokeWidth) + { + return CachedStrokePath != null && Math.Abs(_cachedStrokeWidth - strokeWidth) < Tolerance; + } + + /// + /// Cache path for given stroke width. Takes ownership of a passed path. + /// + /// Path to cache. + /// Stroke width to cache. + /// Render bounds to use. + public void Cache(SKPath path, float strokeWidth, Rect geometryRenderBounds) + { + if (CachedStrokePath != path) + { + CachedStrokePath?.Dispose(); + } + + CachedStrokePath = path; + CachedGeometryRenderBounds = geometryRenderBounds; + _cachedStrokeWidth = strokeWidth; + } + + /// + /// Invalidate cache state. + /// + public void Invalidate() + { + CachedStrokePath?.Dispose(); + CachedGeometryRenderBounds = Rect.Empty; + _cachedStrokeWidth = default(float); + } + } } } diff --git a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs new file mode 100644 index 0000000000..d587a989cc --- /dev/null +++ b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs @@ -0,0 +1,47 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.IO; +using SkiaSharp; + +namespace Avalonia.Skia.Helpers +{ + /// + /// Helps with saving images to stream/file. + /// + public static class ImageSavingHelper + { + /// + /// Save Skia image to a file. + /// + /// Image to save + /// Target file. + public static void SaveImage(SKImage image, string fileName) + { + if (image == null) throw new ArgumentNullException(nameof(image)); + if (fileName == null) throw new ArgumentNullException(nameof(fileName)); + + using (var stream = File.Create(fileName)) + { + SaveImage(image, stream); + } + } + + /// + /// Save Skia image to a stream. + /// + /// Image to save + /// Target stream. + public static void SaveImage(SKImage image, Stream stream) + { + if (image == null) throw new ArgumentNullException(nameof(image)); + if (stream == null) throw new ArgumentNullException(nameof(stream)); + + using (var data = image.Encode()) + { + data.SaveTo(stream); + } + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs b/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs new file mode 100644 index 0000000000..307af708af --- /dev/null +++ b/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs @@ -0,0 +1,35 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Platform; +using SkiaSharp; + +namespace Avalonia.Skia.Helpers +{ + /// + /// Helps with resolving pixel formats to Skia color types. + /// + public static class PixelFormatHelper + { + /// + /// Resolve given format to Skia color type. + /// + /// Format to resolve. + /// Resolved color type. + public static SKColorType ResolveColorType(PixelFormat? format) + { + var colorType = format?.ToSkColorType() ?? SKImageInfo.PlatformColorType; + + // TODO: This looks like some leftover hack + var runtimePlatform = AvaloniaLocator.Current?.GetService(); + var runtime = runtimePlatform?.GetRuntimeInfo(); + + if (runtime?.IsDesktop == true && runtime.Value.OperatingSystem == OperatingSystemType.Linux) + { + colorType = SKColorType.Bgra8888; + } + + return colorType; + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs b/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs new file mode 100644 index 0000000000..5aa5de2abc --- /dev/null +++ b/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs @@ -0,0 +1,23 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Platform; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Extended bitmap implementation that allows for drawing it's contents. + /// + internal interface IDrawableBitmapImpl : IBitmapImpl + { + /// + /// Draw bitmap to a drawing context. + /// + /// Drawing context. + /// Source rect. + /// Destination rect. + /// Paint to use. + void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint); + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs new file mode 100644 index 0000000000..332b8547bf --- /dev/null +++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs @@ -0,0 +1,92 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.IO; +using Avalonia.Platform; +using Avalonia.Skia.Helpers; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Immutable Skia bitmap. + /// + public class ImmutableBitmap : IDrawableBitmapImpl + { + private readonly SKImage _image; + + /// + /// Create immutable bitmap from given stream. + /// + /// Stream containing encoded data. + public ImmutableBitmap(Stream stream) + { + using (var skiaStream = new SKManagedStream(stream)) + { + _image = SKImage.FromEncodedData(SKData.Create(skiaStream)); + + if (_image == null) + { + throw new ArgumentException("Unable to load bitmap from provided data"); + } + + PixelWidth = _image.Width; + PixelHeight = _image.Height; + } + } + + /// + /// Create immutable bitmap from given pixel data copy. + /// + /// Width of data pixels. + /// Height of data pixels. + /// Stride of data pixels. + /// Format of data pixels. + /// Data pixels. + public ImmutableBitmap(int width, int height, int stride, PixelFormat format, IntPtr data) + { + var imageInfo = new SKImageInfo(width, height, format.ToSkColorType(), SKAlphaType.Premul); + + _image = SKImage.FromPixelCopy(imageInfo, data, stride); + + if (_image == null) + { + throw new ArgumentException("Unable to create bitmap from provided data"); + } + + PixelWidth = width; + PixelHeight = height; + } + + /// + public int PixelWidth { get; } + + /// + public int PixelHeight { get; } + + /// + public void Dispose() + { + _image.Dispose(); + } + + /// + public void Save(string fileName) + { + ImageSavingHelper.SaveImage(_image, fileName); + } + + /// + public void Save(Stream stream) + { + ImageSavingHelper.SaveImage(_image, stream); + } + + /// + public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) + { + context.Canvas.DrawImage(_image, sourceRect, destRect, paint); + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 50e65f45dc..d4e6403dc9 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -1,21 +1,21 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using System.Collections.Generic; using System.IO; -using System.Linq; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Media; using Avalonia.Platform; -using SkiaSharp; namespace Avalonia.Skia { - public partial class PlatformRenderInterface : IPlatformRenderInterface + /// + /// Skia platform render interface. + /// + public class PlatformRenderInterface : IPlatformRenderInterface { - public IBitmapImpl CreateBitmap(int width, int height) - { - return CreateRenderTargetBitmap(width, height, 96, 96); - } - + /// public IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, @@ -27,27 +27,19 @@ namespace Avalonia.Skia return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans); } + /// public IStreamGeometryImpl CreateStreamGeometry() { return new StreamGeometryImpl(); } - public IBitmapImpl LoadBitmap(System.IO.Stream stream) + /// + public IBitmapImpl LoadBitmap(Stream stream) { - using (var s = new SKManagedStream(stream)) - { - var bitmap = SKBitmap.Decode(s); - if (bitmap != null) - { - return new BitmapImpl(bitmap); - } - else - { - throw new ArgumentException("Unable to load bitmap from provided data"); - } - } + return new ImmutableBitmap(stream); } + /// public IBitmapImpl LoadBitmap(string fileName) { using (var stream = File.OpenRead(fileName)) @@ -56,16 +48,13 @@ namespace Avalonia.Skia } } + /// public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride) { - using (var tmp = new SKBitmap()) - { - tmp.InstallPixels(new SKImageInfo(width, height, format.ToSkColorType(), SKAlphaType.Premul) - , data, stride); - return new BitmapImpl(tmp.Copy()); - } + return new ImmutableBitmap(width, height, stride, format, data); } + /// public IRenderTargetBitmapImpl CreateRenderTargetBitmap( int width, int height, @@ -73,24 +62,47 @@ namespace Avalonia.Skia double dpiY) { if (width < 1) + { throw new ArgumentException("Width can't be less than 1", nameof(width)); + } + if (height < 1) + { throw new ArgumentException("Height can't be less than 1", nameof(height)); + } + + var dpi = new Vector(dpiX, dpiY); - return new BitmapImpl(width, height, new Vector(dpiX, dpiY)); + var createInfo = new SurfaceRenderTarget.CreateInfo + { + Width = width, + Height = height, + Dpi = dpi, + DisableTextLcdRendering = false + }; + + return new SurfaceRenderTarget(createInfo); } + /// public virtual IRenderTarget CreateRenderTarget(IEnumerable surfaces) { - var fb = surfaces?.OfType().FirstOrDefault(); - if (fb == null) - throw new Exception("Skia backend currently only supports framebuffer render target"); - return new FramebufferRenderTarget(fb); + foreach (var surface in surfaces) + { + if (surface is IFramebufferPlatformSurface framebufferSurface) + { + return new FramebufferRenderTarget(framebufferSurface); + } + } + + throw new NotSupportedException( + "Don't know how to create a Skia render target from any of provided surfaces"); } + /// public IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? format = null) { - return new BitmapImpl(width, height, new Vector(96, 96), format); + return new WriteableBitmapImpl(width, height, format); } } -} +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs b/src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs new file mode 100644 index 0000000000..0086671880 --- /dev/null +++ b/src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs @@ -0,0 +1,28 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Controls; +using Avalonia.Skia; + +// ReSharper disable once CheckNamespace +namespace Avalonia +{ + /// + /// Skia appication extensions. + /// + public static class SkiaApplicationExtensions + { + /// + /// Enable Skia renderer. + /// + /// Builder type. + /// Builder. + /// Preferred backend type. + /// Configure builder. + public static T UseSkia(this T builder) where T : AppBuilderBase, new() + { + builder.UseRenderingSubsystem(() => SkiaPlatform.Initialize(), "Skia"); + return builder; + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/SkiaPlatform.cs b/src/Skia/Avalonia.Skia/SkiaPlatform.cs index d3083d3d33..06679478cb 100644 --- a/src/Skia/Avalonia.Skia/SkiaPlatform.cs +++ b/src/Skia/Avalonia.Skia/SkiaPlatform.cs @@ -1,47 +1,31 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; -using System.Collections.Generic; -using System.Text; -using Avalonia.Controls; +using Avalonia.Logging; using Avalonia.Platform; -using Avalonia.Rendering; - -namespace Avalonia -{ - public static class SkiaApplicationExtensions - { - public static T UseSkia(this T builder) where T : AppBuilderBase, new() - { - builder.UseRenderingSubsystem(Skia.SkiaPlatform.Initialize, "Skia"); - return builder; - } - } -} namespace Avalonia.Skia { + /// + /// Skia platform initializer. + /// public static class SkiaPlatform { - private static bool s_forceSoftwareRendering; - + /// + /// Initialize Skia platform. + /// public static void Initialize() { var renderInterface = new PlatformRenderInterface(); + AvaloniaLocator.CurrentMutable .Bind().ToConstant(renderInterface); } - public static bool ForceSoftwareRendering - { - get { return s_forceSoftwareRendering; } - set - { - s_forceSoftwareRendering = value; - - // TODO: I left this property here as place holder. Do we still need the ability to Force software rendering? - // Is it even possible with SkiaSharp? Perhaps kekekes can answer as part of the HW accel work. - // - throw new NotImplementedException(); - } - } + /// + /// Default DPI. + /// + public static Vector DefaultDpi => new Vector(96.0f, 96.0f); } } diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs index 6a1aed8d79..9196ace4d8 100644 --- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs +++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs @@ -1,10 +1,12 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using Avalonia.Media; using Avalonia.Platform; using SkiaSharp; - -namespace Avalonia +namespace Avalonia.Skia { public static class SkiaSharpExtensions { diff --git a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs index 935d6d5e5b..c19ff79d87 100644 --- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs @@ -1,90 +1,103 @@ -using System; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using Avalonia.Media; using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia { - class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl + /// + /// A Skia implementation of a . + /// + public class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl { - Rect _bounds; - SKPath _path; - - public override SKPath EffectivePath => _path; - - public override Rect GetRenderBounds(Pen pen) + private Rect _bounds; + private readonly SKPath _effectivePath; + + /// + /// Initializes a new instance of the class. + /// + /// An existing Skia . + /// Precomputed path bounds. + public StreamGeometryImpl(SKPath path, Rect bounds) { - return GetRenderBounds(pen?.Thickness ?? 0); + _effectivePath = path; + _bounds = bounds; } - public override Rect Bounds => _bounds; - - public IStreamGeometryImpl Clone() + /// + /// Initializes a new instance of the class. + /// + /// An existing Skia . + public StreamGeometryImpl(SKPath path) : this(path, path.TightBounds.ToAvaloniaRect()) { - return new StreamGeometryImpl - { - _path = _path?.Clone(), - _bounds = Bounds - }; } - public IStreamGeometryContextImpl Open() + /// + /// Initializes a new instance of the class. + /// + public StreamGeometryImpl() : this(CreateEmptyPath(), Rect.Empty) { - _path = new SKPath(); - _path.FillType = SKPathFillType.EvenOdd; - - return new StreamContext(this); } + + /// + public override SKPath EffectivePath => _effectivePath; - public override bool FillContains(Point point) - { - // TODO: Not supported by SkiaSharp yet, so use expanded Rect - // return EffectivePath.Contains(point.X, point.Y); - return GetRenderBounds(0).Contains(point); - } - - public override bool StrokeContains(Pen pen, Point point) - { - // TODO: Not supported by SkiaSharp yet, so use expanded Rect - // return EffectivePath.Contains(point.X, point.Y); - return GetRenderBounds(0).Contains(point); - } + /// + public override Rect Bounds => _bounds; - public override IGeometryImpl Intersect(IGeometryImpl geometry) + /// + public IStreamGeometryImpl Clone() { - throw new NotImplementedException(); + return new StreamGeometryImpl(_effectivePath?.Clone(), Bounds); } - public override ITransformedGeometryImpl WithTransform(Matrix transform) + /// + public IStreamGeometryContextImpl Open() { - return new TransformedGeometryImpl(this, transform); + return new StreamContext(this); } - private Rect GetRenderBounds(double strokeThickness) + /// + /// Create new empty . + /// + /// Empty + private static SKPath CreateEmptyPath() { - // TODO: Calculate properly. - return Bounds.Inflate(strokeThickness); + return new SKPath + { + FillType = SKPathFillType.EvenOdd + }; } - class StreamContext : IStreamGeometryContextImpl + /// + /// A Skia implementation of a . + /// + private class StreamContext : IStreamGeometryContextImpl { private readonly StreamGeometryImpl _geometryImpl; - private SKPath _path; + private readonly SKPath _path; - Point _currentPoint; + /// + /// Initializes a new instance of the class. + /// Geometry to operate on. + /// public StreamContext(StreamGeometryImpl geometryImpl) { _geometryImpl = geometryImpl; - _path = _geometryImpl._path; + _path = _geometryImpl._effectivePath; } - + + /// + /// Will update bounds of passed geometry. public void Dispose() { - SKRect rc; - _path.GetBounds(out rc); - _geometryImpl._bounds = rc.ToAvaloniaRect(); + _geometryImpl._bounds = _path.TightBounds.ToAvaloniaRect(); + _geometryImpl.InvalidateCaches(); } + /// public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) { _path.ArcTo( @@ -95,33 +108,33 @@ namespace Avalonia.Skia sweepDirection == SweepDirection.Clockwise ? SKPathDirection.Clockwise : SKPathDirection.CounterClockwise, (float)point.X, (float)point.Y); - _currentPoint = point; } + /// public void BeginFigure(Point startPoint, bool isFilled) { _path.MoveTo((float)startPoint.X, (float)startPoint.Y); - _currentPoint = startPoint; } + /// public void CubicBezierTo(Point point1, Point point2, Point point3) { _path.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (float)point3.X, (float)point3.Y); - _currentPoint = point3; } + /// public void QuadraticBezierTo(Point point1, Point point2) { _path.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y); - _currentPoint = point2; } + /// public void LineTo(Point point) { _path.LineTo((float)point.X, (float)point.Y); - _currentPoint = point; } + /// public void EndFigure(bool isClosed) { if (isClosed) @@ -130,6 +143,7 @@ namespace Avalonia.Skia } } + /// public void SetFillRule(FillRule fillRule) { _path.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding; diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs new file mode 100644 index 0000000000..88200dcfbe --- /dev/null +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -0,0 +1,169 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.IO; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Skia.Helpers; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Skia render target that writes to a surface. + /// + public class SurfaceRenderTarget : IRenderTargetBitmapImpl, IDrawableBitmapImpl + { + private readonly Vector _dpi; + private readonly SKSurface _surface; + private readonly SKCanvas _canvas; + private readonly bool _disableLcdRendering; + + /// + /// Create new surface render target. + /// + /// Create info. + public SurfaceRenderTarget(CreateInfo createInfo) + { + PixelWidth = createInfo.Width; + PixelHeight = createInfo.Height; + _dpi = createInfo.Dpi; + _disableLcdRendering = createInfo.DisableTextLcdRendering; + + _surface = CreateSurface(PixelWidth, PixelHeight, createInfo.Format); + + _canvas = _surface?.Canvas; + + if (_surface == null || _canvas == null) + { + throw new InvalidOperationException("Failed to create Skia render target surface"); + } + } + + /// + /// Create backing Skia surface. + /// + /// Width. + /// Height. + /// Format. + /// + private static SKSurface CreateSurface(int width, int height, PixelFormat? format) + { + var imageInfo = MakeImageInfo(width, height, format); + + return SKSurface.Create(imageInfo); + } + + /// + public void Dispose() + { + _canvas.Dispose(); + _surface.Dispose(); + } + + /// + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + { + _canvas.RestoreToCount(-1); + _canvas.ResetMatrix(); + + var createInfo = new DrawingContextImpl.CreateInfo + { + Canvas = _canvas, + Dpi = _dpi, + VisualBrushRenderer = visualBrushRenderer, + DisableTextLcdRendering = _disableLcdRendering + }; + + return new DrawingContextImpl(createInfo); + } + + /// + public int PixelWidth { get; } + + /// + public int PixelHeight { get; } + + /// + public void Save(string fileName) + { + using (var image = SnapshotImage()) + { + ImageSavingHelper.SaveImage(image, fileName); + } + } + + /// + public void Save(Stream stream) + { + using (var image = SnapshotImage()) + { + ImageSavingHelper.SaveImage(image, stream); + } + } + + /// + public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) + { + using (var image = SnapshotImage()) + { + context.Canvas.DrawImage(image, sourceRect, destRect, paint); + } + } + + /// + /// Create Skia image snapshot from a surface. + /// + /// Image snapshot. + public SKImage SnapshotImage() + { + return _surface.Snapshot(); + } + + /// + /// Create image info for given parameters. + /// + /// Width. + /// Height. + /// Format. + /// + private static SKImageInfo MakeImageInfo(int width, int height, PixelFormat? format) + { + var colorType = PixelFormatHelper.ResolveColorType(format); + + return new SKImageInfo(width, height, colorType, SKAlphaType.Premul); + } + + /// + /// Create info of a surface render target. + /// + public struct CreateInfo + { + /// + /// Width of a render target. + /// + public int Width; + + /// + /// Height of a render target. + /// + public int Height; + + /// + /// Dpi used when rendering to a surface. + /// + public Vector Dpi; + + /// + /// Pixel format of a render target. + /// + public PixelFormat? Format; + + /// + /// Render text without Lcd rendering. + /// + public bool DisableTextLcdRendering; + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs index e14d3f04be..e95069eef3 100644 --- a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs @@ -1,59 +1,43 @@ -using System; -using Avalonia.Media; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia { - class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl + /// + /// A Skia implementation of a . + /// + public class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl { + /// + /// Initializes a new instance of the class. + /// + /// Source geometry. + /// Transform of new geometry. public TransformedGeometryImpl(GeometryImpl source, Matrix transform) { SourceGeometry = source; Transform = transform; - EffectivePath = source.EffectivePath.Clone(); - EffectivePath.Transform(transform.ToSKMatrix()); + + var transformedPath = source.EffectivePath.Clone(); + transformedPath.Transform(transform.ToSKMatrix()); + + EffectivePath = transformedPath; + Bounds = transformedPath.TightBounds.ToAvaloniaRect(); } + /// public override SKPath EffectivePath { get; } + /// public IGeometryImpl SourceGeometry { get; } + /// public Matrix Transform { get; } - public override Rect Bounds => SourceGeometry.Bounds.TransformToAABB(Transform); - - public override bool FillContains(Point point) - { - // TODO: Not supported by SkiaSharp yet, so use expanded Rect - return GetRenderBounds(0).Contains(point); - } - - public override Rect GetRenderBounds(Pen pen) - { - return GetRenderBounds(pen.Thickness); - } - - public override IGeometryImpl Intersect(IGeometryImpl geometry) - { - throw new NotImplementedException(); - } - - public override bool StrokeContains(Pen pen, Point point) - { - // TODO: Not supported by SkiaSharp yet, so use expanded Rect - return GetRenderBounds(0).Contains(point); - } - - public override ITransformedGeometryImpl WithTransform(Matrix transform) - { - return new TransformedGeometryImpl(this, transform); - } - - public Rect GetRenderBounds(double strokeThickness) - { - // TODO: Calculate properly. - return Bounds.Inflate(strokeThickness); - } + /// + public override Rect Bounds { get; } } } diff --git a/src/Skia/Avalonia.Skia/TypefaceCache.cs b/src/Skia/Avalonia.Skia/TypefaceCache.cs index 24674f3b22..f85dd84055 100644 --- a/src/Skia/Avalonia.Skia/TypefaceCache.cs +++ b/src/Skia/Avalonia.Skia/TypefaceCache.cs @@ -1,3 +1,7 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; using System.Collections.Generic; using System.Linq; using Avalonia.Media; @@ -5,7 +9,10 @@ using SkiaSharp; namespace Avalonia.Skia { - static class TypefaceCache + /// + /// Cache for Skia typefaces. + /// + internal static class TypefaceCache { public static SKTypeface Default = SKTypeface.FromFamilyName(FontFamily.Default.Name); static readonly Dictionary> Cache = new Dictionary>(); diff --git a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs new file mode 100644 index 0000000000..ab6c399ff4 --- /dev/null +++ b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs @@ -0,0 +1,151 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.IO; +using Avalonia.Platform; +using Avalonia.Skia.Helpers; +using SkiaSharp; + +namespace Avalonia.Skia +{ + /// + /// Skia based writeable bitmap. + /// + public class WriteableBitmapImpl : IWriteableBitmapImpl, IDrawableBitmapImpl + { + private static readonly SKBitmapReleaseDelegate s_releaseDelegate = ReleaseProc; + private readonly SKBitmap _bitmap; + + /// + /// Create new writeable bitmap. + /// + /// Width. + /// Height. + /// Format. + public WriteableBitmapImpl(int width, int height, PixelFormat? format = null) + { + PixelHeight = height; + PixelWidth = width; + + var colorType = PixelFormatHelper.ResolveColorType(format); + + var runtimePlatform = AvaloniaLocator.Current?.GetService(); + + if (runtimePlatform != null) + { + _bitmap = new SKBitmap(); + + var nfo = new SKImageInfo(width, height, colorType, SKAlphaType.Premul); + var blob = runtimePlatform.AllocBlob(nfo.BytesSize); + + _bitmap.InstallPixels(nfo, blob.Address, nfo.RowBytes, null, s_releaseDelegate, blob); + } + else + { + _bitmap = new SKBitmap(width, height, colorType, SKAlphaType.Premul); + } + + _bitmap.Erase(SKColor.Empty); + } + + /// + public int PixelWidth { get; } + + /// + public int PixelHeight { get; } + + /// + public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) + { + context.Canvas.DrawBitmap(_bitmap, sourceRect, destRect, paint); + } + + /// + public void Dispose() + { + _bitmap.Dispose(); + } + + /// + public void Save(Stream stream) + { + using (var image = GetSnapshot()) + { + ImageSavingHelper.SaveImage(image, stream); + } + } + + /// + public void Save(string fileName) + { + using (var image = GetSnapshot()) + { + ImageSavingHelper.SaveImage(image, fileName); + } + } + + /// + public ILockedFramebuffer Lock() => new BitmapFramebuffer(_bitmap); + + /// + /// Get snapshot as image. + /// + /// Image snapshot. + public SKImage GetSnapshot() + { + return SKImage.FromPixels(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes); + } + + /// + /// Release given unmanaged blob. + /// + /// Blob address. + /// Blob. + private static void ReleaseProc(IntPtr address, object ctx) + { + ((IUnmanagedBlob)ctx).Dispose(); + } + + /// + /// Framebuffer for bitmap. + /// + private class BitmapFramebuffer : ILockedFramebuffer + { + private SKBitmap _bitmap; + + /// + /// Create framebuffer from given bitmap. + /// + /// Bitmap. + public BitmapFramebuffer(SKBitmap bitmap) + { + _bitmap = bitmap; + } + + /// + public void Dispose() + { + _bitmap = null; + } + + /// + public IntPtr Address => _bitmap.GetPixels(); + + /// + public int Width => _bitmap.Width; + + /// + public int Height => _bitmap.Height; + + /// + public int RowBytes => _bitmap.RowBytes; + + /// + public Vector Dpi { get; } = SkiaPlatform.DefaultDpi; + + /// + public PixelFormat Format => _bitmap.ColorType.ToPixelFormat(); + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/readme.md b/src/Skia/Avalonia.Skia/readme.md index 3defe9c58c..7ed92c5453 100644 --- a/src/Skia/Avalonia.Skia/readme.md +++ b/src/Skia/Avalonia.Skia/readme.md @@ -1,42 +1,22 @@ -TODO: - -BitmapImpl -- constructor from Width/Height -- Save - -StreamGeometryImpl -- Hit testing in Geometry missing as SkiaSharp does not expose this - DrawingContextImpl - Alpha support missing as SkiaSharp does not expose this - Gradient Shader caching? -- TileBrushes - Pen Dash styles Formatted Text Rendering -- minor polish +- Minor polish -RenderTarget -- Figure out a cleaner implementation across all platforms -- HW acceleration +Linux +- Need gpu platform implementation -App Bootstrapping -- Cleanup the testapplications across all platforms -- Add a cleaner Fluent API for the subsystems - - ie. app.UseDirect2D() (via platform specific extension methods) +macOS +- Need gpu platform implementation Android - Not tested at all yet iOS -- Get GLView working again. See HW above - -Win32 -- Cleanup the unmanaged methods (BITMAPINFO) if possible +- Not tested at all yet General -- Cleanup/eliminate obsolete files -- Finish cleanup of the many Test Applications -- Get Skia Unit Tests passing - - +- Get Skia Unit Tests passing (most of the issues are related to antialiasing) \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index b83002f54b..f89086ccb7 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -226,6 +226,8 @@ namespace Avalonia.Win32.Interop MK_SHIFT = 0x0004, + MK_ALT = 0x0020, + MK_XBUTTON1 = 0x0020, MK_XBUTTON2 = 0x0040 @@ -319,6 +321,24 @@ namespace Avalonia.Win32.Interop WS_EX_NOACTIVATE = 0x08000000 } + [Flags] + public enum ClassStyles : uint + { + CS_VREDRAW = 0x0001, + CS_HREDRAW = 0x0002, + CS_DBLCLKS = 0x0008, + CS_OWNDC = 0x0020, + CS_CLASSDC = 0x0040, + CS_PARENTDC = 0x0080, + CS_NOCLOSE = 0x0200, + CS_SAVEBITS = 0x0800, + CS_BYTEALIGNCLIENT = 0x1000, + CS_BYTEALIGNWINDOW = 0x2000, + CS_GLOBALCLASS = 0x4000, + CS_IME = 0x00010000, + CS_DROPSHADOW = 0x00020000 + } + public enum WindowsMessage : uint { WM_NULL = 0x0000, @@ -1201,7 +1221,7 @@ namespace Avalonia.Win32.Interop OFN_NOREADONLYRETURN = 0x00008000, OFN_OVERWRITEPROMPT = 0x00000002 } - + public enum HRESULT : uint { S_FALSE = 0x0001, @@ -1221,7 +1241,7 @@ namespace Avalonia.Win32.Interop public const uint SIGDN_FILESYSPATH = 0x80058000; [Flags] - internal enum FOS : uint + public enum FOS : uint { FOS_OVERWRITEPROMPT = 0x00000002, FOS_STRICTFILETYPES = 0x00000004, @@ -1245,135 +1265,246 @@ namespace Avalonia.Win32.Interop FOS_DEFAULTNOMINIMODE = 0x20000000 } - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public struct OpenFileName + public static class ShellIds { - public int lStructSize; - public IntPtr hwndOwner; - public IntPtr hInstance; - public IntPtr lpstrFilter; - public IntPtr lpstrCustomFilter; - public int nMaxCustFilter; - public int nFilterIndex; - public IntPtr lpstrFile; - public int nMaxFile; - public IntPtr lpstrFileTitle; - public int nMaxFileTitle; - public IntPtr lpstrInitialDir; - public IntPtr lpstrTitle; - public OpenFileNameFlags Flags; - private readonly ushort Unused; - private readonly ushort Unused2; - public IntPtr lpstrDefExt; - public IntPtr lCustData; - public IntPtr lpfnHook; - public IntPtr lpTemplateName; - public IntPtr reservedPtr; - public int reservedInt; - public int flagsEx; - } - } + public static readonly Guid OpenFileDialog = Guid.Parse("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7"); + public static readonly Guid SaveFileDialog = Guid.Parse("C0B4E2F3-BA21-4773-8DBA-335EC946EB8B"); + public static readonly Guid IFileDialog = Guid.Parse("42F85136-DB7E-439C-85F1-E4075D135FC8"); + public static readonly Guid IShellItem = Guid.Parse("43826D1E-E718-42EE-BC55-A1E261C37BFE"); + } - [ComImport(), Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - internal interface IFileDialog - { - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - [PreserveSig()] - uint Show([In, Optional] IntPtr hwndOwner); //IModalWindow + [ComImport(), Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IFileDialog + { + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig()] + uint Show([In, Optional] IntPtr hwndOwner); //IModalWindow - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFileTypes([In] uint cFileTypes, [In, MarshalAs(UnmanagedType.LPArray)] IntPtr rgFilterSpec); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileTypes(uint cFileTypes, [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] COMDLG_FILTERSPEC[] rgFilterSpec); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFileTypeIndex([In] uint iFileType); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileTypeIndex([In] uint iFileType); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetFileTypeIndex(out uint piFileType); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetFileTypeIndex(out uint piFileType); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint Advise([In, MarshalAs(UnmanagedType.Interface)] IntPtr pfde, out uint pdwCookie); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Advise([In, MarshalAs(UnmanagedType.Interface)] IntPtr pfde, out uint pdwCookie); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint Unadvise([In] uint dwCookie); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Unadvise([In] uint dwCookie); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetOptions([In] uint fos); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetOptions([In] uint fos); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetOptions(out uint fos); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetOptions(out uint fos); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, uint fdap); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, uint fdap); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint Close([MarshalAs(UnmanagedType.Error)] uint hr); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Close([MarshalAs(UnmanagedType.Error)] uint hr); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetClientGuid([In] ref Guid guid); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetClientGuid([In] ref Guid guid); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint ClearClientData(); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint ClearClientData(); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter); - } + } + [ComImport, Guid("d57c7288-d4ad-4768-be02-9d969532d960"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IFileOpenDialog + { + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig()] + uint Show([In, Optional] IntPtr hwndOwner); //IModalWindow - [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - internal interface IShellItem - { - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint BindToHandler([In] IntPtr pbc, [In] ref Guid rbhid, [In] ref Guid riid, [Out, MarshalAs(UnmanagedType.Interface)] out IntPtr ppvOut); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileTypes([In] uint cFileTypes, [In, MarshalAs(UnmanagedType.LPArray)] IntPtr rgFilterSpec); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetParent([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFileTypeIndex([In] uint iFileType); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetDisplayName([In] uint sigdnName, out IntPtr ppszName); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetFileTypeIndex(out uint piFileType); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetAttributes([In] uint sfgaoMask, out uint psfgaoAttribs); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Advise([In, MarshalAs(UnmanagedType.Interface)] IntPtr pfde, out uint pdwCookie); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint Compare([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] uint hint, out int piOrder); - + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void Unadvise([In] uint dwCookie); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetOptions([In] uint fos); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetOptions(out uint fos); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, uint fdap); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void Close([MarshalAs(UnmanagedType.Error)] int hr); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetClientGuid([In] ref Guid guid); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void ClearClientData(); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetResults([MarshalAs(UnmanagedType.Interface)] out IShellItemArray ppenum); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetSelectedItems([MarshalAs(UnmanagedType.Interface)] out IShellItemArray ppsai); + } + + [ComImport, Guid("B63EA76D-1F85-456F-A19C-48159EFA858B"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IShellItemArray + { + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void BindToHandler([In, MarshalAs(UnmanagedType.Interface)] IntPtr pbc, [In] ref Guid rbhid, + [In] ref Guid riid, out IntPtr ppvOut); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetPropertyStore([In] int Flags, [In] ref Guid riid, out IntPtr ppv); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetPropertyDescriptionList([In] ref PROPERTYKEY keyType, [In] ref Guid riid, out IntPtr ppv); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetAttributes([In] SIATTRIBFLAGS dwAttribFlags, [In] uint sfgaoMask, out uint psfgaoAttribs); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetCount(out uint pdwNumItems); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetItemAt([In] uint dwIndex, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void EnumItems([MarshalAs(UnmanagedType.Interface)] out IntPtr ppenumShellItems); + } + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + public struct PROPERTYKEY + { + public Guid fmtid; + public uint pid; + } + + public enum SIATTRIBFLAGS + { + SIATTRIBFLAGS_AND = 1, + SIATTRIBFLAGS_APPCOMPAT = 3, + SIATTRIBFLAGS_OR = 2 + } + + [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IShellItem + { + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint BindToHandler([In] IntPtr pbc, [In] ref Guid rbhid, [In] ref Guid riid, [Out, MarshalAs(UnmanagedType.Interface)] out IntPtr ppvOut); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetParent([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetDisplayName([In] uint sigdnName, out IntPtr ppszName); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetAttributes([In] uint sfgaoMask, out uint psfgaoAttribs); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Compare([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] uint hint, out int piOrder); + + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct COMDLG_FILTERSPEC + { + [MarshalAs(UnmanagedType.LPWStr)] + public string pszName; + [MarshalAs(UnmanagedType.LPWStr)] + public string pszSpec; + } } - + [Flags] internal enum DropEffect : int { @@ -1383,9 +1514,7 @@ namespace Avalonia.Win32.Interop Link = 4, Scroll = -2147483648, } - - - + [ComImport] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("00000122-0000-0000-C000-000000000046")] diff --git a/src/Windows/Avalonia.Win32/OleDragSource.cs b/src/Windows/Avalonia.Win32/OleDragSource.cs index 522014abc0..a87995952a 100644 --- a/src/Windows/Avalonia.Win32/OleDragSource.cs +++ b/src/Windows/Avalonia.Win32/OleDragSource.cs @@ -11,10 +11,11 @@ namespace Avalonia.Win32 private const int DRAGDROP_S_DROP = 0x00040100; private const int DRAGDROP_S_CANCEL = 0x00040101; - private const int KEYSTATE_LEFTMB = 1; - private const int KEYSTATE_MIDDLEMB = 16; - private const int KEYSTATE_RIGHTMB = 2; - private static readonly int[] MOUSE_BUTTONS = new int[] { KEYSTATE_LEFTMB, KEYSTATE_MIDDLEMB, KEYSTATE_RIGHTMB }; + private static readonly int[] MOUSE_BUTTONS = new int[] { + (int)UnmanagedMethods.ModifierKeys.MK_LBUTTON, + (int)UnmanagedMethods.ModifierKeys.MK_MBUTTON, + (int)UnmanagedMethods.ModifierKeys.MK_RBUTTON + }; public int QueryContinueDrag(int fEscapePressed, int grfKeyState) { diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index 973564a3d1..6a10cc7e98 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -45,6 +45,26 @@ namespace Avalonia.Win32 return result; } + private static InputModifiers ConvertKeyState(int grfKeyState) + { + InputModifiers modifiers = InputModifiers.None; + var state = (UnmanagedMethods.ModifierKeys)grfKeyState; + + if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_LBUTTON)) + modifiers |= InputModifiers.LeftMouseButton; + if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_MBUTTON)) + modifiers |= InputModifiers.MiddleMouseButton; + if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_RBUTTON)) + modifiers |= InputModifiers.RightMouseButton; + if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_SHIFT)) + modifiers |= InputModifiers.Shift; + if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_CONTROL)) + modifiers |= InputModifiers.Control; + if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_ALT)) + modifiers |= InputModifiers.Alt; + return modifiers; + } + UnmanagedMethods.HRESULT IDropTarget.DragEnter(IOleDataObject pDataObj, int grfKeyState, long pt, ref DropEffect pdwEffect) { var dispatch = _tl?.Input; @@ -56,13 +76,15 @@ namespace Avalonia.Win32 _currentDrag = pDataObj as IDataObject; if (_currentDrag == null) _currentDrag = new OleDataObject(pDataObj); + var args = new RawDragEvent( _dragDevice, RawDragEventType.DragEnter, _target, GetDragLocation(pt), _currentDrag, - ConvertDropEffect(pdwEffect) + ConvertDropEffect(pdwEffect), + ConvertKeyState(grfKeyState) ); dispatch(args); pdwEffect = ConvertDropEffect(args.Effects); @@ -85,7 +107,8 @@ namespace Avalonia.Win32 _target, GetDragLocation(pt), _currentDrag, - ConvertDropEffect(pdwEffect) + ConvertDropEffect(pdwEffect), + ConvertKeyState(grfKeyState) ); dispatch(args); pdwEffect = ConvertDropEffect(args.Effects); @@ -98,12 +121,13 @@ namespace Avalonia.Win32 try { _tl?.Input(new RawDragEvent( - _dragDevice, - RawDragEventType.DragLeave, - _target, - default(Point), - null, - DragDropEffects.None + _dragDevice, + RawDragEventType.DragLeave, + _target, + default(Point), + null, + DragDropEffects.None, + InputModifiers.None )); return UnmanagedMethods.HRESULT.S_OK; } @@ -134,7 +158,8 @@ namespace Avalonia.Win32 _target, GetDragLocation(pt), _currentDrag, - ConvertDropEffect(pdwEffect) + ConvertDropEffect(pdwEffect), + ConvertKeyState(grfKeyState) ); dispatch(args); pdwEffect = ConvertDropEffect(args.Effects); diff --git a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs index 9b6bf57e92..d555e93a88 100644 --- a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs +++ b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs @@ -1,152 +1,94 @@ +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Platform; +using Avalonia.Win32.Interop; using System; using System.Collections.Generic; -using System.Diagnostics; -using System.IO; using System.Linq; using System.Runtime.InteropServices; -using System.Text; using System.Threading.Tasks; -using Avalonia.Controls; -using Avalonia.Controls.Platform; -using Avalonia.Platform; -using Avalonia.Win32.Interop; namespace Avalonia.Win32 { class SystemDialogImpl : ISystemDialogImpl { - static char[] ToChars(string s) - { - if (s == null) - return null; - var chars = new char[s.Length]; - for (int c = 0; c < s.Length; c++) - chars[c] = s[c]; - return chars; - } - public unsafe Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) { var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero; return Task.Factory.StartNew(() => { - var filters = new StringBuilder(); - foreach (var filter in dialog.Filters) - { - var extMask = string.Join(";", filter.Extensions.Select(e => "*." + e)); - filters.Append(filter.Name); - filters.Append(" ("); - filters.Append(extMask); - filters.Append(")"); - filters.Append('\0'); - filters.Append(extMask); - filters.Append('\0'); - } - if (filters.Length == 0) - filters.Append("All files\0*.*\0"); - filters.Append('\0'); + var result = new string[0]; - var filterBuffer = new char[filters.Length]; - filters.CopyTo(0, filterBuffer, 0, filterBuffer.Length); - - var defExt = ToChars((dialog as SaveFileDialog)?.DefaultExtension); - var fileBuffer = new char[256]; - dialog.InitialFileName?.CopyTo(0, fileBuffer, 0, dialog.InitialFileName.Length); + Guid clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog; + Guid iid = UnmanagedMethods.ShellIds.IFileDialog; + UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk); + var frm = (UnmanagedMethods.IFileDialog)unk; - string userSelectedExt = string.Empty; + var openDialog = dialog as OpenFileDialog; + uint options; + frm.GetOptions(out options); + options |= (uint)(UnmanagedMethods.FOS.FOS_NOVALIDATE | UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT); + if (openDialog?.AllowMultiple == true) + options |= (uint)UnmanagedMethods.FOS.FOS_ALLOWMULTISELECT; + frm.SetOptions(options); - var title = ToChars(dialog.Title); - var initialDir = ToChars(dialog.InitialDirectory); + var defaultExtension = (dialog as SaveFileDialog)?.DefaultExtension ?? ""; + frm.SetDefaultExtension(defaultExtension); + frm.SetFileName(dialog.InitialFileName ?? ""); + frm.SetTitle(dialog.Title); - fixed (char* pFileBuffer = fileBuffer) - fixed (char* pFilterBuffer = filterBuffer) - fixed (char* pDefExt = defExt) - fixed (char* pInitDir = initialDir) - fixed (char* pTitle = title) + var filters = new List(); + foreach (var filter in dialog.Filters) { - var ofn = new UnmanagedMethods.OpenFileName() - { - hwndOwner = hWnd, - hInstance = IntPtr.Zero, - lCustData = IntPtr.Zero, - nFilterIndex = 0, - Flags = - UnmanagedMethods.OpenFileNameFlags.OFN_EXPLORER | - UnmanagedMethods.OpenFileNameFlags.OFN_HIDEREADONLY, - nMaxCustFilter = 0, - nMaxFile = fileBuffer.Length - 1, - nMaxFileTitle = 0, - lpTemplateName = IntPtr.Zero, - lpfnHook = IntPtr.Zero, - lpstrCustomFilter = IntPtr.Zero, - lpstrDefExt = new IntPtr(pDefExt), - lpstrFile = new IntPtr(pFileBuffer), - lpstrFileTitle = IntPtr.Zero, - lpstrFilter = new IntPtr(pFilterBuffer), - lpstrInitialDir = new IntPtr(pInitDir), - lpstrTitle = new IntPtr(pTitle), - - }; - ofn.lStructSize = Marshal.SizeOf(ofn); - if ((dialog as OpenFileDialog)?.AllowMultiple == true) - ofn.Flags |= UnmanagedMethods.OpenFileNameFlags.OFN_ALLOWMULTISELECT; - - if (dialog is SaveFileDialog) - ofn.Flags |= UnmanagedMethods.OpenFileNameFlags.OFN_NOREADONLYRETURN | - UnmanagedMethods.OpenFileNameFlags.OFN_OVERWRITEPROMPT; - - var pofn = &ofn; - - // We should save the current directory to restore it later. - var currentDirectory = Environment.CurrentDirectory; - - var res = dialog is OpenFileDialog - ? UnmanagedMethods.GetOpenFileName(new IntPtr(pofn)) - : UnmanagedMethods.GetSaveFileName(new IntPtr(pofn)); - - // Restore the old current directory, since GetOpenFileName and GetSaveFileName change it after they're called - Environment.CurrentDirectory = currentDirectory; - - if (!res) - return null; - if (dialog?.Filters.Count > 0) - userSelectedExt = dialog.Filters[ofn.nFilterIndex - 1].Extensions.FirstOrDefault(); + var extMask = string.Join(";", filter.Extensions.Select(e => "*." + e)); + filters.Add(new UnmanagedMethods.COMDLG_FILTERSPEC { pszName = filter.Name, pszSpec = extMask }); } - var cStart = 0; - string dir = null; - var files = new List(); - for (var c = 0; c < fileBuffer.Length; c++) + if (filters.Count == 0) + filters.Add(new UnmanagedMethods.COMDLG_FILTERSPEC { pszName = "All files", pszSpec = "*.*" }); + + frm.SetFileTypes((uint)filters.Count, filters.ToArray()); + frm.SetFileTypeIndex(0); + + if (dialog.InitialDirectory != null) { - if (fileBuffer[c] == 0) + UnmanagedMethods.IShellItem directoryShellItem; + Guid riid = UnmanagedMethods.ShellIds.IShellItem; + if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.InitialDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) { - //Encountered double zero char - if (cStart == c) - break; - - var s = new string(fileBuffer, cStart, c - cStart); - if (dir == null) - dir = s; - else - files.Add(s); - cStart = c + 1; + frm.SetFolder(directoryShellItem); + frm.SetDefaultFolder(directoryShellItem); } } - if (files.Count == 0) + + if (frm.Show(hWnd) == (uint)UnmanagedMethods.HRESULT.S_OK) { - if (dialog is SaveFileDialog) + if (openDialog?.AllowMultiple == true) { - if (string.IsNullOrWhiteSpace(Path.GetExtension(dir)) && - !string.IsNullOrWhiteSpace(userSelectedExt) && - !userSelectedExt.Contains("*")) - dir = Path.ChangeExtension(dir, userSelectedExt); + UnmanagedMethods.IShellItemArray shellItemArray; + ((UnmanagedMethods.IFileOpenDialog)frm).GetResults(out shellItemArray); + uint count; + shellItemArray.GetCount(out count); + result = new string[count]; + for (uint i = 0; i < count; i++) + { + UnmanagedMethods.IShellItem shellItem; + shellItemArray.GetItemAt(i, out shellItem); + result[i] = GetAbsoluteFilePath(shellItem); + } + } + else + { + UnmanagedMethods.IShellItem shellItem; + if (frm.GetResult(out shellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) + { + result = new string[] { GetAbsoluteFilePath(shellItem) }; + } } - - return new[] { dir }; } - return files.Select(f => Path.Combine(dir, f)).ToArray(); + return result; }); } @@ -157,11 +99,11 @@ namespace Avalonia.Win32 string result = string.Empty; var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero; - var clsid = Guid.Parse("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7"); - var iid = Guid.Parse("42F85136-DB7E-439C-85F1-E4075D135FC8"); + Guid clsid = UnmanagedMethods.ShellIds.OpenFileDialog; + Guid iid = UnmanagedMethods.ShellIds.IFileDialog; UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk); - var frm = (IFileDialog)unk; + var frm = (UnmanagedMethods.IFileDialog)unk; uint options; frm.GetOptions(out options); options |= (uint)(UnmanagedMethods.FOS.FOS_PICKFOLDERS | UnmanagedMethods.FOS.FOS_FORCEFILESYSTEM | UnmanagedMethods.FOS.FOS_NOVALIDATE | UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT); @@ -169,8 +111,8 @@ namespace Avalonia.Win32 if (dialog.InitialDirectory != null) { - IShellItem directoryShellItem; - var riid = new Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"); //IShellItem + UnmanagedMethods.IShellItem directoryShellItem; + Guid riid = UnmanagedMethods.ShellIds.IShellItem; if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.InitialDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) { frm.SetFolder(directoryShellItem); @@ -179,8 +121,8 @@ namespace Avalonia.Win32 if (dialog.DefaultDirectory != null) { - IShellItem directoryShellItem; - var riid = new Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"); //IShellItem + UnmanagedMethods.IShellItem directoryShellItem; + Guid riid = UnmanagedMethods.ShellIds.IShellItem; if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.DefaultDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) { frm.SetDefaultFolder(directoryShellItem); @@ -189,29 +131,35 @@ namespace Avalonia.Win32 if (frm.Show(hWnd) == (uint)UnmanagedMethods.HRESULT.S_OK) { - IShellItem shellItem; + UnmanagedMethods.IShellItem shellItem; if (frm.GetResult(out shellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) { - IntPtr pszString; - if (shellItem.GetDisplayName(UnmanagedMethods.SIGDN_FILESYSPATH, out pszString) == (uint)UnmanagedMethods.HRESULT.S_OK) - { - if (pszString != IntPtr.Zero) - { - try - { - result = Marshal.PtrToStringAuto(pszString); - } - finally - { - Marshal.FreeCoTaskMem(pszString); - } - } - } + result = GetAbsoluteFilePath(shellItem); } } return result; }); } + + private string GetAbsoluteFilePath(UnmanagedMethods.IShellItem shellItem) + { + IntPtr pszString; + if (shellItem.GetDisplayName(UnmanagedMethods.SIGDN_FILESYSPATH, out pszString) == (uint)UnmanagedMethods.HRESULT.S_OK) + { + if (pszString != IntPtr.Zero) + { + try + { + return Marshal.PtrToStringAuto(pszString); + } + finally + { + Marshal.FreeCoTaskMem(pszString); + } + } + } + return ""; + } } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 9f67f97252..292d8a8138 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -724,7 +724,7 @@ namespace Avalonia.Win32 UnmanagedMethods.WNDCLASSEX wndClassEx = new UnmanagedMethods.WNDCLASSEX { cbSize = Marshal.SizeOf(), - style = 0, + style = (int)(ClassStyles.CS_OWNDC | ClassStyles.CS_HREDRAW | ClassStyles.CS_VREDRAW), // Unique DC helps with performance when using Gpu based rendering lpfnWndProc = _wndProcDelegate, hInstance = UnmanagedMethods.GetModuleHandle(null), hCursor = DefaultCursor, diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index 9e92baf0ff..a460ab5bf3 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -1,6 +1,7 @@  - netcoreapp2.0 + netcoreapp2.0 + Exe @@ -15,7 +16,7 @@ - + diff --git a/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs b/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs new file mode 100644 index 0000000000..c4106340b2 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs @@ -0,0 +1,45 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Media; +using Avalonia.UnitTests; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Visuals.Media +{ + [MemoryDiagnoser] + public class PathMarkupParserTests : IDisposable + { + private IDisposable _app; + + public PathMarkupParserTests() + { + _app = UnitTestApplication.Start(TestServices.StyledWindow); + } + + public void Dispose() + { + _app.Dispose(); + } + + [Benchmark] + public void Parse_Large_Path() + { + const string PathData = "F1 M 16.6309 18.6563C 17.1309 8.15625 29.8809 14.1563 29.8809 14.1563C 30.8809 11.1563 34.1308 11.4063" + + " 34.1308 11.4063C 33.5 12 34.6309 13.1563 34.6309 13.1563C 32.1309 13.1562 31.1309 14.9062 31.1309 14.9" + + "062C 41.1309 23.9062 32.6309 27.9063 32.6309 27.9062C 24.6309 24.9063 21.1309 22.1562 16.6309 18.6563 Z" + + " M 16.6309 19.9063C 21.6309 24.1563 25.1309 26.1562 31.6309 28.6562C 31.6309 28.6562 26.3809 39.1562 18" + + ".3809 36.1563C 18.3809 36.1563 18 38 16.3809 36.9063C 15 36 16.3809 34.9063 16.3809 34.9063C 16.3809 34" + + ".9063 10.1309 30.9062 16.6309 19.9063 Z "; + + var streamGeometry = new StreamGeometry(); + + using (var context = streamGeometry.Open()) + using (var parser = new PathMarkupParser(context)) + { + parser.Parse(PathData); + } + } + } +} diff --git a/tests/Avalonia.RenderTests/Media/BitmapTests.cs b/tests/Avalonia.RenderTests/Media/BitmapTests.cs index 089579a0a0..4cee05a0d9 100644 --- a/tests/Avalonia.RenderTests/Media/BitmapTests.cs +++ b/tests/Avalonia.RenderTests/Media/BitmapTests.cs @@ -67,7 +67,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media [Theory] [InlineData(PixelFormat.Rgba8888), InlineData(PixelFormat.Bgra8888), -#if SKIA +#if AVALONIA_SKIA InlineData(PixelFormat.Rgb565) #endif ] diff --git a/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs b/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs index 6381bceadc..0107002274 100644 --- a/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/ImageBrushTests.cs @@ -281,12 +281,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media await RenderToFile(target); CompareImages(); } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task ImageBrush_NoStretch_NoTile_BottomRightQuarterDest() { Decorator target = new Decorator @@ -309,12 +305,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media await RenderToFile(target); CompareImages(); } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task ImageBrush_NoStretch_NoTile_BottomRightQuarterSource_BottomRightQuarterDest() { Decorator target = new Decorator diff --git a/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs index 062c7d88f5..656e77fc31 100644 --- a/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs @@ -21,12 +21,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media public LinearGradientBrushTests() : base(@"Media\LinearGradientBrush") { } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task LinearGradientBrush_RedBlue_Horizontal_Fill() { Decorator target = new Decorator @@ -52,12 +48,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media await RenderToFile(target); CompareImages(); } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task LinearGradientBrush_RedBlue_Vertical_Fill() { Decorator target = new Decorator diff --git a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs index b3e214f863..0017feb106 100644 --- a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs @@ -21,12 +21,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media public RadialGradientBrushTests() : base(@"Media\RadialGradientBrush") { } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task RadialGradientBrush_RedBlue() { Decorator target = new Decorator diff --git a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs index f69d336271..3a1f7adb2d 100644 --- a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs @@ -270,12 +270,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media await RenderToFile(target); CompareImages(); } - -#if AVALONIA_SKIA_SKIP_FAIL - [Fact(Skip = "FIXME")] -#else + [Fact] -#endif public async Task VisualBrush_NoStretch_NoTile_BottomRightQuarterSource_BottomRightQuarterDest() { Decorator target = new Decorator diff --git a/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj new file mode 100644 index 0000000000..0473355fcd --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj @@ -0,0 +1,22 @@ + + + netcoreapp2.0 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Avalonia.Skia.UnitTests/HitTesting.cs b/tests/Avalonia.Skia.UnitTests/HitTesting.cs new file mode 100644 index 0000000000..544a09e50f --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/HitTesting.cs @@ -0,0 +1,79 @@ +using Avalonia.Controls.Shapes; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Rendering; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Skia.UnitTests +{ + public class HitTesting + { + [Fact] + public void Hit_Test_Should_Respect_Fill() + { + using (AvaloniaLocator.EnterScope()) + { + SkiaPlatform.Initialize(); + + var root = new TestRoot + { + Width = 100, + Height = 100, + Child = new Ellipse + { + Width = 100, + Height = 100, + Fill = Brushes.Red, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }; + + root.Renderer = new DeferredRenderer(root, null); + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + + var outsideResult = root.Renderer.HitTest(new Point(10, 10), root, null); + var insideResult = root.Renderer.HitTest(new Point(50, 50), root, null); + + Assert.Empty(outsideResult); + Assert.Equal(new[] {root.Child}, insideResult); + } + } + + [Fact] + public void Hit_Test_Should_Respect_Stroke() + { + using (AvaloniaLocator.EnterScope()) + { + SkiaPlatform.Initialize(); + + var root = new TestRoot + { + Width = 100, + Height = 100, + Child = new Ellipse + { + Width = 100, + Height = 100, + Stroke = Brushes.Red, + StrokeThickness = 5, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }; + + root.Renderer = new DeferredRenderer(root, null); + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + + var outsideResult = root.Renderer.HitTest(new Point(50, 50), root, null); + var insideResult = root.Renderer.HitTest(new Point(1, 50), root, null); + + Assert.Empty(outsideResult); + Assert.Equal(new[] { root.Child }, insideResult); + } + } + } +} diff --git a/tests/Avalonia.Skia.UnitTests/Properties/AssemblyInfo.cs b/tests/Avalonia.Skia.UnitTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..a462e5b079 --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Reflection; +using Xunit; + +// Don't run tests in parallel. +[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs index e63d23283c..35ec38789e 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs @@ -2,60 +2,147 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using Avalonia.Media; -using Avalonia.Platform; -using Moq; +using Avalonia.Visuals.Platform; using Xunit; namespace Avalonia.Visuals.UnitTests.Media { + using System.IO; + public class PathMarkupParserTests { [Fact] public void Parses_Move() { - using (AvaloniaLocator.EnterScope()) - { - var result = new Mock(); - - var parser = PrepareParser(result); - + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) + { parser.Parse("M10 10"); - result.Verify(x => x.BeginFigure(new Point(10, 10), true)); + var figure = pathGeometry.Figures[0]; + + Assert.Equal(new Point(10, 10), figure.StartPoint); } } [Fact] public void Parses_Line() { - using (AvaloniaLocator.EnterScope()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var result = new Mock(); + parser.Parse("M0 0L10 10"); - var parser = PrepareParser(result); + var figure = pathGeometry.Figures[0]; - parser.Parse("M0 0L10 10"); + var segment = figure.Segments[0]; - result.Verify(x => x.LineTo(new Point(10, 10))); + Assert.IsType(segment); + + var lineSegment = (LineSegment)segment; + + Assert.Equal(new Point(10, 10), lineSegment.Point); } } [Fact] public void Parses_Close() { - using (AvaloniaLocator.EnterScope()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var result = new Mock(); + parser.Parse("M0 0L10 10z"); - var parser = PrepareParser(result); + var figure = pathGeometry.Figures[0]; - parser.Parse("M0 0L10 10z"); + Assert.True(figure.IsClosed); + } + } + + [Fact] + public void Parses_FillMode_Before_Move() + { + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) + { + parser.Parse("F 1M0,0"); + + Assert.Equal(FillRule.NonZero, pathGeometry.FillRule); + } + } + + [Theory] + [InlineData("M0 0 10 10 20 20")] + [InlineData("M0,0 10,10 20,20")] + [InlineData("M0,0,10,10,20,20")] + public void Parses_Implicit_Line_Command_After_Move(string pathData) + { + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) + { + parser.Parse(pathData); + + var figure = pathGeometry.Figures[0]; + + var segment = figure.Segments[0]; - result.Verify(x => x.EndFigure(true)); + Assert.IsType(segment); + + var lineSegment = (LineSegment)segment; + + Assert.Equal(new Point(10, 10), lineSegment.Point); + + figure = pathGeometry.Figures[1]; + + segment = figure.Segments[0]; + + Assert.IsType(segment); + + lineSegment = (LineSegment)segment; + + Assert.Equal(new Point(20, 20), lineSegment.Point); } } [Theory] + [InlineData("m0 0 10 10 20 20")] + [InlineData("m0,0 10,10 20,20")] + [InlineData("m0,0,10,10,20,20")] + public void Parses_Implicit_Line_Command_After_Relative_Move(string pathData) + { + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) + { + parser.Parse(pathData); + + var figure = pathGeometry.Figures[0]; + + var segment = figure.Segments[0]; + + Assert.IsType(segment); + + var lineSegment = (LineSegment)segment; + + Assert.Equal(new Point(10, 10), lineSegment.Point); + + segment = figure.Segments[1]; + + Assert.IsType(segment); + + lineSegment = (LineSegment)segment; + + Assert.Equal(new Point(30, 30), lineSegment.Point); + } + } + + [Theory] + [InlineData(" M0 0")] [InlineData("F1 M24,14 A2,2,0,1,1,20,14 A2,2,0,1,1,24,14 z")] // issue #1107 [InlineData("M0 0L10 10z")] [InlineData("M50 50 L100 100 L150 50")] @@ -75,29 +162,32 @@ namespace Avalonia.Visuals.UnitTests.Media ".3809 36.1563C 18.3809 36.1563 18 38 16.3809 36.9063C 15 36 16.3809 34.9063 16.3809 34.9063C 16.3809 34" + ".9063 10.1309 30.9062 16.6309 19.9063 Z ")] [InlineData( - "F1M16,12C16,14.209 14.209,16 12,16 9.791,16 8,14.209 8,12 8,11.817 8.03,11.644 8.054,11.467L6.585,10 4,10 " + - "4,6.414 2.5,7.914 0,5.414 0,3.586 3.586,0 4.414,0 7.414,3 7.586,3 9,1.586 11.914,4.5 10.414,6 " + + "F1M16,12C16,14.209 14.209,16 12,16 9.791,16 8,14.209 8,12 8,11.817 8.03,11.644 8.054,11.467L6.585,10 4,10 " + + "4,6.414 2.5,7.914 0,5.414 0,3.586 3.586,0 4.414,0 7.414,3 7.586,3 9,1.586 11.914,4.5 10.414,6 " + "12.461,8.046C14.45,8.278,16,9.949,16,12")] public void Should_Parse(string pathData) { - using (AvaloniaLocator.EnterScope()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var parser = PrepareParser(); - parser.Parse(pathData); Assert.True(true); } } - private static PathMarkupParser PrepareParser(Mock implMock = null) + [Theory] + [InlineData("0 0")] + [InlineData("j")] + public void Throws_InvalidDataException_On_None_Defined_Command(string pathData) { - AvaloniaLocator.CurrentMutable - .Bind() - .ToConstant(Mock.Of()); - - return new PathMarkupParser( - new StreamGeometryContext(implMock != null ? implMock.Object : Mock.Of())); + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) + { + Assert.Throws(() => parser.Parse(pathData)); + } } } } \ No newline at end of file diff --git a/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Horizontal_Fill.expected.png b/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Horizontal_Fill.expected.png index 744a314256..89668d19f4 100644 Binary files a/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Horizontal_Fill.expected.png and b/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Horizontal_Fill.expected.png differ diff --git a/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Vertical_Fill.expected.png b/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Vertical_Fill.expected.png index 3a3d7df5d4..2b882874dc 100644 Binary files a/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Vertical_Fill.expected.png and b/tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Vertical_Fill.expected.png differ diff --git a/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue.expected.png b/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue.expected.png new file mode 100644 index 0000000000..fce116d98d Binary files /dev/null and b/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png new file mode 100644 index 0000000000..3725f2acbc Binary files /dev/null and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png differ diff --git a/tests/TestFiles/Skia/Shapes/Path/Path_With_PenLineCap.expected.png b/tests/TestFiles/Skia/Shapes/Path/Path_With_PenLineCap.expected.png new file mode 100644 index 0000000000..d33068d62c Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/Path_With_PenLineCap.expected.png differ