Browse Source

Merge branch 'master' into extract-layout-manager

pull/1014/head
Jeremy Koritzinsky 8 years ago
committed by GitHub
parent
commit
aa68fdcc2c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 52
      Avalonia.sln
  2. 1
      build.cake
  3. 4
      build/SkiaSharp.props
  4. 0
      samples/BindingDemo/App.config
  5. 0
      samples/BindingDemo/App.xaml
  6. 2
      samples/BindingDemo/App.xaml.cs
  7. 0
      samples/BindingDemo/BindingDemo.csproj
  8. 4
      samples/BindingDemo/MainWindow.xaml
  9. 4
      samples/BindingDemo/MainWindow.xaml.cs
  10. 0
      samples/BindingDemo/TestItemView.xaml
  11. 2
      samples/BindingDemo/TestItemView.xaml.cs
  12. 2
      samples/BindingDemo/ViewModels/DataAnnotationsErrorViewModel.cs
  13. 2
      samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs
  14. 2
      samples/BindingDemo/ViewModels/IndeiErrorViewModel.cs
  15. 2
      samples/BindingDemo/ViewModels/MainWindowViewModel.cs
  16. 2
      samples/BindingDemo/ViewModels/NestedCommandViewModel.cs
  17. 2
      samples/BindingDemo/ViewModels/TestItem.cs
  18. 4
      samples/ControlCatalog.NetCore/Program.cs
  19. 2
      samples/RemoteDemo/Program.cs
  20. 0
      samples/RemoteDemo/RemoteDemo.csproj
  21. 0
      samples/RenderDemo/App.config
  22. 2
      samples/RenderDemo/App.xaml
  23. 2
      samples/RenderDemo/App.xaml.cs
  24. 2
      samples/RenderDemo/MainWindow.xaml
  25. 4
      samples/RenderDemo/MainWindow.xaml.cs
  26. 0
      samples/RenderDemo/Pages/AnimationsPage.xaml
  27. 4
      samples/RenderDemo/Pages/AnimationsPage.xaml.cs
  28. 0
      samples/RenderDemo/Pages/ClippingPage.xaml
  29. 2
      samples/RenderDemo/Pages/ClippingPage.xaml.cs
  30. 0
      samples/RenderDemo/Pages/DrawingPage.xaml
  31. 2
      samples/RenderDemo/Pages/DrawingPage.xaml.cs
  32. 0
      samples/RenderDemo/RenderDemo.csproj
  33. 0
      samples/RenderDemo/SideBar.xaml
  34. 2
      samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs
  35. 2
      samples/RenderDemo/ViewModels/MainWindowViewModel.cs
  36. 0
      samples/VirtualizationDemo/App.config
  37. 0
      samples/VirtualizationDemo/App.xaml
  38. 2
      samples/VirtualizationDemo/App.xaml.cs
  39. 0
      samples/VirtualizationDemo/MainWindow.xaml
  40. 4
      samples/VirtualizationDemo/MainWindow.xaml.cs
  41. 2
      samples/VirtualizationDemo/Program.cs
  42. 2
      samples/VirtualizationDemo/ViewModels/ItemViewModel.cs
  43. 2
      samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
  44. 0
      samples/VirtualizationDemo/VirtualizationDemo.csproj
  45. 2
      src/Avalonia.Controls/Platform/InProcessDragSource.cs
  46. 24
      src/Avalonia.Input/DragDropDevice.cs
  47. 5
      src/Avalonia.Input/DragEventArgs.cs
  48. 4
      src/Avalonia.Input/Raw/RawDragEvent.cs
  49. 21
      src/Avalonia.Visuals/Media/PathGeometry.cs
  50. 831
      src/Avalonia.Visuals/Media/PathMarkupParser.cs
  51. 11
      src/Avalonia.Visuals/Media/StreamGeometry.cs
  52. 2
      src/Avalonia.Visuals/Media/StreamGeometryContext.cs
  53. 66
      src/Avalonia.Visuals/Platform/IGeometryContext.cs
  54. 54
      src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs
  55. 85
      src/Avalonia.Visuals/Platform/PathGeometryContext.cs
  56. 3
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  57. 1
      src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
  58. 16
      src/Avalonia.Visuals/Rendering/RenderLayers.cs
  59. 52
      src/Avalonia.Visuals/Vector.cs
  60. 3
      src/OSX/Avalonia.MonoMac/TopLevelImpl.cs
  61. 142
      src/Skia/Avalonia.Skia/BitmapImpl.cs
  62. 783
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  63. 47
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  64. 211
      src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs
  65. 170
      src/Skia/Avalonia.Skia/GeometryImpl.cs
  66. 47
      src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs
  67. 35
      src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs
  68. 23
      src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs
  69. 92
      src/Skia/Avalonia.Skia/ImmutableBitmap.cs
  70. 80
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  71. 28
      src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs
  72. 46
      src/Skia/Avalonia.Skia/SkiaPlatform.cs
  73. 6
      src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
  74. 124
      src/Skia/Avalonia.Skia/StreamGeometryImpl.cs
  75. 169
      src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs
  76. 62
      src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs
  77. 9
      src/Skia/Avalonia.Skia/TypefaceCache.cs
  78. 151
      src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs
  79. 34
      src/Skia/Avalonia.Skia/readme.md
  80. 329
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  81. 9
      src/Windows/Avalonia.Win32/OleDragSource.cs
  82. 43
      src/Windows/Avalonia.Win32/OleDropTarget.cs
  83. 228
      src/Windows/Avalonia.Win32/SystemDialogImpl.cs
  84. 2
      src/Windows/Avalonia.Win32/WindowImpl.cs
  85. 5
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  86. 45
      tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs
  87. 2
      tests/Avalonia.RenderTests/Media/BitmapTests.cs
  88. 12
      tests/Avalonia.RenderTests/Media/ImageBrushTests.cs
  89. 12
      tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs
  90. 6
      tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs
  91. 6
      tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
  92. 22
      tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj
  93. 79
      tests/Avalonia.Skia.UnitTests/HitTesting.cs
  94. 8
      tests/Avalonia.Skia.UnitTests/Properties/AssemblyInfo.cs
  95. 152
      tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs
  96. BIN
      tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Horizontal_Fill.expected.png
  97. BIN
      tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Vertical_Fill.expected.png
  98. BIN
      tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue.expected.png
  99. BIN
      tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png
  100. BIN
      tests/TestFiles/Skia/Shapes/Path/Path_With_PenLineCap.expected.png

52
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}

1
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);

4
build/SkiaSharp.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="1.57.1" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.57.1.3" />
<PackageReference Include="SkiaSharp" Version="1.60.0" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.60.0.1" />
</ItemGroup>
</Project>

0
samples/BindingTest/App.config → samples/BindingDemo/App.config

0
samples/BindingTest/App.xaml → samples/BindingDemo/App.xaml

2
samples/BindingTest/App.xaml.cs → 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
{

0
samples/BindingTest/BindingTest.csproj → samples/BindingDemo/BindingDemo.csproj

4
samples/BindingTest/MainWindow.xaml → samples/BindingDemo/MainWindow.xaml

@ -1,7 +1,7 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:vm="clr-namespace:BindingTest.ViewModels"
xmlns:local="clr-namespace:BindingTest"
xmlns:vm="clr-namespace:BindingDemo.ViewModels"
xmlns:local="clr-namespace:BindingDemo"
Title="AvaloniaUI Bindings Test"
Width="800"
Height="600">

4
samples/BindingTest/MainWindow.xaml.cs → 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
{

0
samples/BindingTest/TestItemView.xaml → samples/BindingDemo/TestItemView.xaml

2
samples/BindingTest/TestItemView.xaml.cs → samples/BindingDemo/TestItemView.xaml.cs

@ -1,7 +1,7 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace BindingTest
namespace BindingDemo
{
public class TestItemView : UserControl
{

2
samples/BindingTest/ViewModels/DataAnnotationsErrorViewModel.cs → samples/BindingDemo/ViewModels/DataAnnotationsErrorViewModel.cs

@ -3,7 +3,7 @@
using System.ComponentModel.DataAnnotations;
namespace BindingTest.ViewModels
namespace BindingDemo.ViewModels
{
public class DataAnnotationsErrorViewModel
{

2
samples/BindingTest/ViewModels/ExceptionErrorViewModel.cs → samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs

@ -4,7 +4,7 @@
using ReactiveUI;
using System;
namespace BindingTest.ViewModels
namespace BindingDemo.ViewModels
{
public class ExceptionErrorViewModel : ReactiveObject
{

2
samples/BindingTest/ViewModels/IndeiErrorViewModel.cs → 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
{

2
samples/BindingTest/ViewModels/MainWindowViewModel.cs → 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
{

2
samples/BindingTest/ViewModels/NestedCommandViewModel.cs → 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
{

2
samples/BindingTest/ViewModels/TestItem.cs → samples/BindingDemo/ViewModels/TestItem.cs

@ -1,6 +1,6 @@
using ReactiveUI;
namespace BindingTest.ViewModels
namespace BindingDemo.ViewModels
{
public class TestItem : ReactiveObject
{

4
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
/// </summary>
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>().UsePlatformDetect().UseReactiveUI();
=> AppBuilder.Configure<App>().UsePlatformDetect().UseSkia().UseReactiveUI();
static void ConsoleSilencer()
{

2
samples/RemoteTest/Program.cs → samples/RemoteDemo/Program.cs

@ -9,7 +9,7 @@ using Avalonia.Remote.Protocol;
using Avalonia.Threading;
using ControlCatalog;
namespace RemoteTest
namespace RemoteDemo
{
class Program
{

0
samples/RemoteTest/RemoteTest.csproj → samples/RemoteDemo/RemoteDemo.csproj

0
samples/RenderTest/App.config → samples/RenderDemo/App.config

2
samples/RenderTest/App.xaml → samples/RenderDemo/App.xaml

@ -2,6 +2,6 @@
<Application.Styles>
<StyleInclude Source="resm:Avalonia.Themes.Default.DefaultTheme.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:RenderTest.SideBar.xaml"/>
<StyleInclude Source="resm:RenderDemo.SideBar.xaml"/>
</Application.Styles>
</Application>

2
samples/RenderTest/App.xaml.cs → 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
{

2
samples/RenderTest/MainWindow.xaml → samples/RenderDemo/MainWindow.xaml

@ -1,6 +1,6 @@
<Window xmlns="https://github.com/avaloniaui"
Title="AvaloniaUI Rendering Test"
xmlns:pages="clr-namespace:RenderTest.Pages"
xmlns:pages="clr-namespace:RenderDemo.Pages"
Width="800"
Height="600">
<DockPanel>

4
samples/RenderTest/MainWindow.xaml.cs → 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
{

0
samples/RenderTest/Pages/AnimationsPage.xaml → samples/RenderDemo/Pages/AnimationsPage.xaml

4
samples/RenderTest/Pages/AnimationsPage.xaml.cs → 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
{

0
samples/RenderTest/Pages/ClippingPage.xaml → samples/RenderDemo/Pages/ClippingPage.xaml

2
samples/RenderTest/Pages/ClippingPage.xaml.cs → 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
{

0
samples/RenderTest/Pages/DrawingPage.xaml → samples/RenderDemo/Pages/DrawingPage.xaml

2
samples/RenderTest/Pages/DrawingPage.xaml.cs → 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
{

0
samples/RenderTest/RenderTest.csproj → samples/RenderDemo/RenderDemo.csproj

0
samples/RenderTest/SideBar.xaml → samples/RenderDemo/SideBar.xaml

2
samples/RenderTest/ViewModels/AnimationsPageViewModel.cs → samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs

@ -2,7 +2,7 @@
using ReactiveUI;
using Avalonia.Animation;
namespace RenderTest.ViewModels
namespace RenderDemo.ViewModels
{
public class AnimationsPageViewModel : ReactiveObject
{

2
samples/RenderTest/ViewModels/MainWindowViewModel.cs → samples/RenderDemo/ViewModels/MainWindowViewModel.cs

@ -1,7 +1,7 @@
using System;
using ReactiveUI;
namespace RenderTest.ViewModels
namespace RenderDemo.ViewModels
{
public class MainWindowViewModel : ReactiveObject
{

0
samples/VirtualizationTest/App.config → samples/VirtualizationDemo/App.config

0
samples/VirtualizationTest/App.xaml → samples/VirtualizationDemo/App.xaml

2
samples/VirtualizationTest/App.xaml.cs → samples/VirtualizationDemo/App.xaml.cs

@ -4,7 +4,7 @@
using Avalonia;
using Avalonia.Markup.Xaml;
namespace VirtualizationTest
namespace VirtualizationDemo
{
public class App : Application
{

0
samples/VirtualizationTest/MainWindow.xaml → samples/VirtualizationDemo/MainWindow.xaml

4
samples/VirtualizationTest/MainWindow.xaml.cs → 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
{

2
samples/VirtualizationTest/Program.cs → samples/VirtualizationDemo/Program.cs

@ -7,7 +7,7 @@ using Avalonia.Controls;
using Avalonia.Logging.Serilog;
using Serilog;
namespace VirtualizationTest
namespace VirtualizationDemo
{
class Program
{

2
samples/VirtualizationTest/ViewModels/ItemViewModel.cs → samples/VirtualizationDemo/ViewModels/ItemViewModel.cs

@ -4,7 +4,7 @@
using System;
using ReactiveUI;
namespace VirtualizationTest.ViewModels
namespace VirtualizationDemo.ViewModels
{
internal class ItemViewModel : ReactiveObject
{

2
samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs → 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
{

0
samples/VirtualizationTest/VirtualizationTest.csproj → samples/VirtualizationDemo/VirtualizationDemo.csproj

2
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<TopLevel>().FirstOrDefault();
tl.PlatformImpl?.Input(rawEvent);

24
src/Avalonia.Input/DragDropDevice.cs

@ -19,11 +19,11 @@ namespace Avalonia.Input
return null;
}
private DragDropEffects RaiseDragEvent(Interactive target, IInputElement inputRoot, Point point, RoutedEvent<DragEventArgs> routedEvent, DragDropEffects operation, IDataObject data)
private DragDropEffects RaiseDragEvent(Interactive target, IInputElement inputRoot, Point point, RoutedEvent<DragEventArgs> 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;
}
}

5
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<DragEventArgs> routedEvent, IDataObject data, Interactive target, Point targetLocation)
public DragEventArgs(RoutedEvent<DragEventArgs> routedEvent, IDataObject data, Interactive target, Point targetLocation, InputModifiers modifiers)
: base(routedEvent)
{
this.Data = data;
this._target = target;
this._targetLocation = targetLocation;
this.Modifiers = modifiers;
}
}

4
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;
}
}
}

21
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<PathGeometry>((s, e) =>
FiguresProperty.Changed.AddClassHandler<PathGeometry>((s, e) =>
s.OnFiguresChanged(e.NewValue as PathFigures));
}
@ -40,6 +41,24 @@ namespace Avalonia.Media
Figures = new PathFigures();
}
/// <summary>
/// Parses the specified path data to a <see cref="PathGeometry"/>.
/// </summary>
/// <param name="pathData">The s.</param>
/// <returns></returns>
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;
}
/// <summary>
/// Gets or sets the figures.
/// </summary>

831
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
{
/// <summary>
/// Parses a path markup string.
/// </summary>
public class PathMarkupParser
public class PathMarkupParser : IDisposable
{
private static readonly Dictionary<char, Command> Commands = new Dictionary<char, Command>
{
{ '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<char, FillRule> FillRules = new Dictionary<char, FillRule>
private static readonly string s_separatorPattern;
private static readonly Dictionary<char, Command> s_commands =
new Dictionary<char, Command>
{
{ '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();
}
/// <summary>
/// Initializes a new instance of the <see cref="PathMarkupParser"/> class.
/// </summary>
/// <param name="context">The context for the geometry.</param>
public PathMarkupParser(StreamGeometryContext context)
/// <param name="geometryContext">The geometry context.</param>
/// <exception cref="ArgumentNullException">geometryContext</exception>
public PathMarkupParser(IGeometryContext geometryContext)
{
_context = context;
if (geometryContext == null)
{
throw new ArgumentNullException(nameof(geometryContext));
}
_geometryContext = geometryContext;
}
/// <summary>
/// Defines the command currently being processed.
/// </summary>
private enum Command
{
None,
@ -62,358 +73,610 @@ namespace Avalonia.Media
SmoothCubicBezierCurve,
SmoothQuadraticBezierCurve,
Arc,
Close,
Close
}
/// <summary>
/// Parses the specified markup string.
/// Parses the specified path data and writes the result to the geometryContext of this instance.
/// </summary>
/// <param name="s">The markup string.</param>
public void Parse(string s)
/// <param name="pathData">The path data.</param>
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<CommandToken> 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<CommandToken> 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<string> arguments)
{
char c = (char)i;
Command = command;
if (char.IsWhiteSpace(c))
IsRelative = isRelative;
Arguments = new List<string>(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<string> 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<string>();
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;
}
}
}
}

11
src/Avalonia.Visuals/Media/StreamGeometry.cs

@ -35,14 +35,15 @@ namespace Avalonia.Media
/// <returns>A <see cref="StreamGeometry"/>.</returns>
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;
}
/// <inheritdoc/>

2
src/Avalonia.Visuals/Media/StreamGeometryContext.cs

@ -15,7 +15,7 @@ namespace Avalonia.Media
/// <see cref="StreamGeometry.Open"/>.
/// </remarks>
/// TODO: This class is just a wrapper around IStreamGeometryContextImpl: is it needed?
public class StreamGeometryContext : IDisposable
public class StreamGeometryContext : IGeometryContext
{
private readonly IStreamGeometryContextImpl _impl;

66
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
{
/// <summary>
/// Describes a geometry using drawing commands.
/// </summary>
public interface IGeometryContext : IDisposable
{
/// <summary>
/// Draws an arc to the specified point.
/// </summary>
/// <param name="point">The destination point.</param>
/// <param name="size">The radii of an oval whose perimeter is used to draw the angle.</param>
/// <param name="rotationAngle">The rotation angle of the oval that specifies the curve.</param>
/// <param name="isLargeArc">true to draw the arc greater than 180 degrees; otherwise, false.</param>
/// <param name="sweepDirection">
/// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction.
/// </param>
void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection);
/// <summary>
/// Begins a new figure.
/// </summary>
/// <param name="startPoint">The starting point for the figure.</param>
/// <param name="isFilled">Whether the figure is filled.</param>
void BeginFigure(Point startPoint, bool isFilled = true);
/// <summary>
/// Draws a Bezier curve to the specified point.
/// </summary>
/// <param name="point1">The first control point used to specify the shape of the curve.</param>
/// <param name="point2">The second control point used to specify the shape of the curve.</param>
/// <param name="point3">The destination point for the end of the curve.</param>
void CubicBezierTo(Point point1, Point point2, Point point3);
/// <summary>
/// Draws a quadratic Bezier curve to the specified point
/// </summary>
/// <param name="control">Control point</param>
/// <param name="endPoint">DestinationPoint</param>
void QuadraticBezierTo(Point control, Point endPoint);
/// <summary>
/// Draws a line to the specified point.
/// </summary>
/// <param name="point">The destination point.</param>
void LineTo(Point point);
/// <summary>
/// Ends the figure started by <see cref="BeginFigure(Point, bool)"/>.
/// </summary>
/// <param name="isClosed">Whether the figure is closed.</param>
void EndFigure(bool isClosed);
/// <summary>
/// Sets the fill rule.
/// </summary>
/// <param name="fillRule">The fill rule.</param>
void SetFillRule(FillRule fillRule);
}
}

54
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
{
/// <summary>
/// Describes a geometry using drawing commands.
/// </summary>
public interface IStreamGeometryContextImpl : IDisposable
{
/// <summary>
/// Draws an arc to the specified point.
/// </summary>
/// <param name="point">The destination point.</param>
/// <param name="size">The radii of an oval whose perimeter is used to draw the angle.</param>
/// <param name="rotationAngle">The rotation angle of the oval that specifies the curve.</param>
/// <param name="isLargeArc">true to draw the arc greater than 180 degrees; otherwise, false.</param>
/// <param name="sweepDirection">
/// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction.
/// </param>
void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection);
/// <summary>
/// Begins a new figure.
/// </summary>
/// <param name="startPoint">The starting point for the figure.</param>
/// <param name="isFilled">Whether the figure is filled.</param>
void BeginFigure(Point startPoint, bool isFilled);
/// <summary>
/// Draws a Bezier curve to the specified point.
/// </summary>
/// <param name="point1">The first control point used to specify the shape of the curve.</param>
/// <param name="point2">The second control point used to specify the shape of the curve.</param>
/// <param name="point3">The destination point for the end of the curve.</param>
void CubicBezierTo(Point point1, Point point2, Point point3);
/// <summary>
/// Draws a quadratic Bezier curve to the specified point
/// </summary>
/// <param name="control">Control point</param>
/// <param name="endPoint">DestinationPoint</param>
void QuadraticBezierTo(Point control, Point endPoint);
/// <summary>
/// Draws a line to the specified point.
/// </summary>
/// <param name="point">The destination point.</param>
void LineTo(Point point);
/// <summary>
/// Ends the figure started by <see cref="BeginFigure(Point, bool)"/>.
/// </summary>
/// <param name="isClosed">Whether the figure is closed.</param>
void EndFigure(bool isClosed);
void SetFillRule(FillRule fillRule);
public interface IStreamGeometryContextImpl : IGeometryContext
{
}
}

85
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;
}
}
}

3
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();
}
/// <inheritdoc/>

1
src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs

@ -133,6 +133,7 @@ namespace Avalonia.Rendering
/// </summary>
public void Dispose()
{
_renderTarget?.Dispose();
}
/// <inheritdoc/>

16
src/Avalonia.Visuals/Rendering/RenderLayers.cs

@ -11,11 +11,7 @@ namespace Avalonia.Rendering
{
private List<RenderLayer> _inner = new List<RenderLayer>();
private Dictionary<IVisual, RenderLayer> _index = new Dictionary<IVisual, RenderLayer>();
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);

52
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);
}
/// <summary>
/// Check if two vectors are equal (bitwise).
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public bool Equals(Vector other)
{
// ReSharper disable CompareOfFloatsByEqualityOperator
return _x == other._x && _y == other._y;
// ReSharper restore CompareOfFloatsByEqualityOperator
}
/// <summary>
/// Check if two vectors are nearly equal (numerically).
/// </summary>
/// <param name="other">The other vector.</param>
/// <returns>True if vectors are nearly equal.</returns>
[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);
}
/// <summary>
/// Returns the string representation of the point.
/// </summary>

3
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);
}

142
src/Skia/Avalonia.Skia/BitmapImpl.cs

@ -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<IRuntimePlatform>();
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<IRuntimePlatform>();
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);
}
}

783
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
/// <summary>
/// Skia based drawing context.
/// </summary>
public class DrawingContextImpl : IDrawingContextImpl
{
private readonly IDisposable[] _disposables;
private readonly Vector _dpi;
private readonly Stack<PaintWrapper> _maskStack = new Stack<PaintWrapper>();
private readonly Stack<double> _opacityStack = new Stack<double>();
private readonly Matrix? _postTransform;
private readonly IDisposable[] _disposables;
private readonly IVisualBrushRenderer _visualBrushRenderer;
private Stack<PaintWrapper> maskStack = new Stack<PaintWrapper>();
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;
/// <summary>
/// Context create info.
/// </summary>
public struct CreateInfo
{
/// <summary>
/// Canvas to draw to.
/// </summary>
public SKCanvas Canvas;
/// <summary>
/// Dpi of drawings.
/// </summary>
public Vector Dpi;
/// <summary>
/// Visual brush renderer.
/// </summary>
public IVisualBrushRenderer VisualBrushRenderer;
/// <summary>
/// Render text without Lcd rendering.
/// </summary>
public bool DisableTextLcdRendering;
}
/// <summary>
/// Create new drawing context.
/// </summary>
/// <param name="createInfo">Create info.</param>
/// <param name="disposables">Array of elements to dispose after drawing has finished.</param>
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;
}
/// <summary>
/// Skia canvas.
/// </summary>
public SKCanvas Canvas { get; }
/// <inheritdoc />
public void Clear(Color color)
{
Canvas.Clear(color.ToSKColor());
}
/// <inheritdoc />
public void DrawImage(IRef<IBitmapImpl> 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);
}
}
/// <inheritdoc />
public void DrawImage(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
{
PushOpacityMask(opacityMask, opacityMaskRect);
@ -59,17 +116,19 @@ namespace Avalonia.Skia
PopOpacityMask();
}
/// <inheritdoc />
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);
}
}
/// <inheritdoc />
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
/// <inheritdoc />
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);
}
}
}
/// <inheritdoc />
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()
/// <inheritdoc />
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
/// <inheritdoc />
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);
/// <inheritdoc />
public void PushClip(Rect clip)
{
Canvas.Save();
Canvas.ClipRect(clip.ToSKRect());
}
paint.Color = Paint.Color;
paint.Shader = Paint.Shader;
/// <inheritdoc />
public void PopClip()
{
Canvas.Restore();
}
return state;
}
/// <inheritdoc />
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();
}
/// <inheritdoc />
public void PopOpacity()
{
_currentOpacity = _opacityStack.Pop();
}
public PaintWrapper(SKPaint paint)
/// <inheritdoc />
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)
/// <inheritdoc />
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;
/// <inheritdoc />
public void PopGeometryClip()
{
Canvas.Restore();
}
/// <inheritdoc />
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)
/// <inheritdoc />
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();
}
/// <inheritdoc />
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());
}
}
/// <summary>
/// Configure paint wrapper for using gradient brush.
/// </summary>
/// <param name="paintWrapper">Paint wrapper.</param>
/// <param name="targetSize">Target size.</param>
/// <param name="gradientBrush">Gradient brush.</param>
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;
}
}
}
/// <summary>
/// Configure paint wrapper for using tile brush.
/// </summary>
/// <param name="paintWrapper">Paint wrapper.</param>
/// <param name="targetSize">Target size.</param>
/// <param name="tileBrush">Tile brush to use.</param>
/// <param name="tileBrushImage">Tile brush image.</param>
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);
/// <summary>
/// Configure paint wrapper to use visual brush.
/// </summary>
/// <param name="paintWrapper">Paint wrapper.</param>
/// <param name="visualBrush">Visual brush.</param>
/// <param name="visualBrushRenderer">Visual brush renderer.</param>
/// <param name="tileBrushImage">Tile brush image.</param>
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
}
/// <summary>
/// Creates paint wrapper for given brush.
/// </summary>
/// <param name="brush">Source brush.</param>
/// <param name="targetSize">Target size.</param>
/// <returns>Paint wrapper for given brush.</returns>
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;
}
/// <summary>
/// Creates paint wrapper for given pen.
/// </summary>
/// <param name="pen">Source pen.</param>
/// <param name="targetSize">Target size.</param>
/// <returns></returns>
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)
/// <summary>
/// Create new render target compatible with this drawing context.
/// </summary>
/// <param name="width">Width.</param>
/// <param name="height">Height.</param>
/// <param name="dpi">Drawing dpi.</param>
/// <param name="format">Pixel format.</param>
/// <returns></returns>
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)
/// <summary>
/// Skia cached paint state.
/// </summary>
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))
/// <inheritdoc />
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<double> _opacityStack = new Stack<double>();
public void PushOpacity(double opacity)
/// <summary>
/// Skia paint wrapper.
/// </summary>
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
/// <summary>
/// Add new disposable to a wrapper.
/// </summary>
/// <param name="disposable">Disposable to add.</param>
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.");
}
}
/// <inheritdoc />
public void Dispose()
{
Paint?.Dispose();
_disposable1?.Dispose();
_disposable2?.Dispose();
_disposable3?.Dispose();
}
}
}

47
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -10,6 +10,9 @@ using System.Linq;
namespace Avalonia.Skia
{
/// <summary>
/// Skia formatted text implementation.
/// </summary>
public class FormattedTextImpl : IFormattedTextImpl
{
public FormattedTextImpl(
@ -21,7 +24,7 @@ namespace Avalonia.Skia
IReadOnlyList<FormattedTextStyleSpan> 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;

211
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
{
/// <summary>
/// Skia render target that renders to a framebuffer surface. No gpu acceleration available.
/// </summary>
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)
/// <summary>
/// Create new framebuffer render target using a target surface.
/// </summary>
/// <param name="platformSurface">Target surface.</param>
public FramebufferRenderTarget(IFramebufferPlatformSurface platformSurface)
{
_surface = surface;
_platformSurface = platformSurface;
}
/// <inheritdoc />
public void Dispose()
{
//Nothing to do here, since we don't own framebuffer
FreeSurface();
}
/// <inheritdoc />
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);
}
/// <summary>
/// Check if two images info are compatible.
/// </summary>
/// <param name="currentImageInfo">Current.</param>
/// <param name="desiredImageInfo">Desired.</param>
/// <returns>True, if images are compatible.</returns>
private static bool AreImageInfosCompatible(SKImageInfo currentImageInfo, SKImageInfo desiredImageInfo)
{
return currentImageInfo.Width == desiredImageInfo.Width &&
currentImageInfo.Height == desiredImageInfo.Height &&
currentImageInfo.ColorType == desiredImageInfo.ColorType;
}
/// <summary>
/// Create Skia surface backed by given framebuffer.
/// </summary>
/// <param name="desiredImageInfo">Desired image info.</param>
/// <param name="framebuffer">Backing framebuffer.</param>
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;
}
/// <summary>
/// Free Skia surface.
/// </summary>
private void FreeSurface()
{
_conversionShim?.Dispose();
_conversionShim = null;
_preFramebufferCopyHandler = null;
if (_conversionShim != null)
{
_framebufferSurface?.Dispose();
}
_framebufferSurface = null;
_currentFramebufferAddress = IntPtr.Zero;
}
class PixelFormatShim : IDisposable
/// <summary>
/// Converts non-compatible pixel formats using bitmap copies.
/// </summary>
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);
/// <summary>
/// Skia surface.
/// </summary>
public SKSurface Surface { get; }
/// <summary>
/// Handler to start conversion via surface copy.
/// </summary>
public IDisposable SurfaceCopyHandler { get; }
/// <inheritdoc />
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);
/// <summary>
/// Convert and copy surface to a framebuffer.
/// </summary>
private void CopySurface()
{
using (var snapshot = Surface.Snapshot())
{
snapshot.ReadPixels(_destinationInfo, _framebufferAddress, _destinationInfo.RowBytes, 0, 0,
SKImageCachingHint.Disallow);
}
}
}
}
}
}

170
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
/// <summary>
/// A Skia implementation of <see cref="IGeometryImpl"/>.
/// </summary>
public abstract class GeometryImpl : IGeometryImpl
{
private PathCache _pathCache;
/// <inheritdoc />
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);
/// <inheritdoc />
public bool FillContains(Point point)
{
return PathContainsCore(EffectivePath, point);
}
/// <inheritdoc />
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);
}
/// <summary>
/// Update path cache for given stroke width.
/// </summary>
/// <param name="strokeWidth">Stroke width.</param>
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());
}
}
}
/// <summary>
/// Check Skia path if it contains a point.
/// </summary>
/// <param name="path">Path to check.</param>
/// <param name="point">Point.</param>
/// <returns>True, if point is contained in a path.</returns>
private static bool PathContainsCore(SKPath path, Point point)
{
return path.Contains((float)point.X, (float)point.Y);
}
/// <inheritdoc />
public IGeometryImpl Intersect(IGeometryImpl geometry)
{
var result = EffectivePath.Op(((GeometryImpl) geometry).EffectivePath, SKPathOp.Intersect);
return result == null ? null : new StreamGeometryImpl(result);
}
/// <inheritdoc />
public Rect GetRenderBounds(Pen pen)
{
var strokeWidth = (float)(pen?.Thickness ?? 0);
if (!_pathCache.HasCacheFor(strokeWidth))
{
UpdatePathCache(strokeWidth);
}
return _pathCache.CachedGeometryRenderBounds.Inflate(strokeWidth / 2.0);
}
/// <inheritdoc />
public ITransformedGeometryImpl WithTransform(Matrix transform)
{
return new TransformedGeometryImpl(this, transform);
}
/// <summary>
/// Invalidate all caches. Call after chaning path contents.
/// </summary>
protected void InvalidateCaches()
{
_pathCache.Invalidate();
}
private struct PathCache
{
private float _cachedStrokeWidth;
/// <summary>
/// Tolerance for two stroke widths to be deemed equal
/// </summary>
public const float Tolerance = float.Epsilon;
/// <summary>
/// Cached contour path.
/// </summary>
public SKPath CachedStrokePath { get; private set; }
/// <summary>
/// Cached geometry render bounds.
/// </summary>
public Rect CachedGeometryRenderBounds { get; private set; }
/// <summary>
/// Is cached valid for given stroke width.
/// </summary>
/// <param name="strokeWidth">Stroke width to check.</param>
/// <returns>True, if CachedStrokePath can be used for given stroke width.</returns>
public bool HasCacheFor(float strokeWidth)
{
return CachedStrokePath != null && Math.Abs(_cachedStrokeWidth - strokeWidth) < Tolerance;
}
/// <summary>
/// Cache path for given stroke width. Takes ownership of a passed path.
/// </summary>
/// <param name="path">Path to cache.</param>
/// <param name="strokeWidth">Stroke width to cache.</param>
/// <param name="geometryRenderBounds">Render bounds to use.</param>
public void Cache(SKPath path, float strokeWidth, Rect geometryRenderBounds)
{
if (CachedStrokePath != path)
{
CachedStrokePath?.Dispose();
}
CachedStrokePath = path;
CachedGeometryRenderBounds = geometryRenderBounds;
_cachedStrokeWidth = strokeWidth;
}
/// <summary>
/// Invalidate cache state.
/// </summary>
public void Invalidate()
{
CachedStrokePath?.Dispose();
CachedGeometryRenderBounds = Rect.Empty;
_cachedStrokeWidth = default(float);
}
}
}
}

47
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
{
/// <summary>
/// Helps with saving images to stream/file.
/// </summary>
public static class ImageSavingHelper
{
/// <summary>
/// Save Skia image to a file.
/// </summary>
/// <param name="image">Image to save</param>
/// <param name="fileName">Target file.</param>
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);
}
}
/// <summary>
/// Save Skia image to a stream.
/// </summary>
/// <param name="image">Image to save</param>
/// <param name="stream">Target stream.</param>
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);
}
}
}
}

35
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
{
/// <summary>
/// Helps with resolving pixel formats to Skia color types.
/// </summary>
public static class PixelFormatHelper
{
/// <summary>
/// Resolve given format to Skia color type.
/// </summary>
/// <param name="format">Format to resolve.</param>
/// <returns>Resolved color type.</returns>
public static SKColorType ResolveColorType(PixelFormat? format)
{
var colorType = format?.ToSkColorType() ?? SKImageInfo.PlatformColorType;
// TODO: This looks like some leftover hack
var runtimePlatform = AvaloniaLocator.Current?.GetService<IRuntimePlatform>();
var runtime = runtimePlatform?.GetRuntimeInfo();
if (runtime?.IsDesktop == true && runtime.Value.OperatingSystem == OperatingSystemType.Linux)
{
colorType = SKColorType.Bgra8888;
}
return colorType;
}
}
}

23
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
{
/// <summary>
/// Extended bitmap implementation that allows for drawing it's contents.
/// </summary>
internal interface IDrawableBitmapImpl : IBitmapImpl
{
/// <summary>
/// Draw bitmap to a drawing context.
/// </summary>
/// <param name="context">Drawing context.</param>
/// <param name="sourceRect">Source rect.</param>
/// <param name="destRect">Destination rect.</param>
/// <param name="paint">Paint to use.</param>
void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint);
}
}

92
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
{
/// <summary>
/// Immutable Skia bitmap.
/// </summary>
public class ImmutableBitmap : IDrawableBitmapImpl
{
private readonly SKImage _image;
/// <summary>
/// Create immutable bitmap from given stream.
/// </summary>
/// <param name="stream">Stream containing encoded data.</param>
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;
}
}
/// <summary>
/// Create immutable bitmap from given pixel data copy.
/// </summary>
/// <param name="width">Width of data pixels.</param>
/// <param name="height">Height of data pixels.</param>
/// <param name="stride">Stride of data pixels.</param>
/// <param name="format">Format of data pixels.</param>
/// <param name="data">Data pixels.</param>
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;
}
/// <inheritdoc />
public int PixelWidth { get; }
/// <inheritdoc />
public int PixelHeight { get; }
/// <inheritdoc />
public void Dispose()
{
_image.Dispose();
}
/// <inheritdoc />
public void Save(string fileName)
{
ImageSavingHelper.SaveImage(_image, fileName);
}
/// <inheritdoc />
public void Save(Stream stream)
{
ImageSavingHelper.SaveImage(_image, stream);
}
/// <inheritdoc />
public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint)
{
context.Canvas.DrawImage(_image, sourceRect, destRect, paint);
}
}
}

80
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
/// <summary>
/// Skia platform render interface.
/// </summary>
public class PlatformRenderInterface : IPlatformRenderInterface
{
public IBitmapImpl CreateBitmap(int width, int height)
{
return CreateRenderTargetBitmap(width, height, 96, 96);
}
/// <inheritdoc />
public IFormattedTextImpl CreateFormattedText(
string text,
Typeface typeface,
@ -27,27 +27,19 @@ namespace Avalonia.Skia
return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans);
}
/// <inheritdoc />
public IStreamGeometryImpl CreateStreamGeometry()
{
return new StreamGeometryImpl();
}
public IBitmapImpl LoadBitmap(System.IO.Stream stream)
/// <inheritdoc />
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);
}
/// <inheritdoc />
public IBitmapImpl LoadBitmap(string fileName)
{
using (var stream = File.OpenRead(fileName))
@ -56,16 +48,13 @@ namespace Avalonia.Skia
}
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public virtual IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
{
var fb = surfaces?.OfType<IFramebufferPlatformSurface>().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");
}
/// <inheritdoc />
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);
}
}
}
}

28
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
{
/// <summary>
/// Skia appication extensions.
/// </summary>
public static class SkiaApplicationExtensions
{
/// <summary>
/// Enable Skia renderer.
/// </summary>
/// <typeparam name="T">Builder type.</typeparam>
/// <param name="builder">Builder.</param>
/// <param name="preferredBackendType">Preferred backend type.</param>
/// <returns>Configure builder.</returns>
public static T UseSkia<T>(this T builder) where T : AppBuilderBase<T>, new()
{
builder.UseRenderingSubsystem(() => SkiaPlatform.Initialize(), "Skia");
return builder;
}
}
}

46
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<T>(this T builder) where T : AppBuilderBase<T>, new()
{
builder.UseRenderingSubsystem(Skia.SkiaPlatform.Initialize, "Skia");
return builder;
}
}
}
namespace Avalonia.Skia
{
/// <summary>
/// Skia platform initializer.
/// </summary>
public static class SkiaPlatform
{
private static bool s_forceSoftwareRendering;
/// <summary>
/// Initialize Skia platform.
/// </summary>
public static void Initialize()
{
var renderInterface = new PlatformRenderInterface();
AvaloniaLocator.CurrentMutable
.Bind<IPlatformRenderInterface>().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();
}
}
/// <summary>
/// Default DPI.
/// </summary>
public static Vector DefaultDpi => new Vector(96.0f, 96.0f);
}
}

6
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
{

124
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
/// <summary>
/// A Skia implementation of a <see cref="IStreamGeometryImpl"/>.
/// </summary>
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;
/// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
/// </summary>
/// <param name="path">An existing Skia <see cref="SKPath"/>.</param>
/// <param name="bounds">Precomputed path bounds.</param>
public StreamGeometryImpl(SKPath path, Rect bounds)
{
return GetRenderBounds(pen?.Thickness ?? 0);
_effectivePath = path;
_bounds = bounds;
}
public override Rect Bounds => _bounds;
public IStreamGeometryImpl Clone()
/// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
/// </summary>
/// <param name="path">An existing Skia <see cref="SKPath"/>.</param>
public StreamGeometryImpl(SKPath path) : this(path, path.TightBounds.ToAvaloniaRect())
{
return new StreamGeometryImpl
{
_path = _path?.Clone(),
_bounds = Bounds
};
}
public IStreamGeometryContextImpl Open()
/// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
/// </summary>
public StreamGeometryImpl() : this(CreateEmptyPath(), Rect.Empty)
{
_path = new SKPath();
_path.FillType = SKPathFillType.EvenOdd;
return new StreamContext(this);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public override Rect Bounds => _bounds;
public override IGeometryImpl Intersect(IGeometryImpl geometry)
/// <inheritdoc />
public IStreamGeometryImpl Clone()
{
throw new NotImplementedException();
return new StreamGeometryImpl(_effectivePath?.Clone(), Bounds);
}
public override ITransformedGeometryImpl WithTransform(Matrix transform)
/// <inheritdoc />
public IStreamGeometryContextImpl Open()
{
return new TransformedGeometryImpl(this, transform);
return new StreamContext(this);
}
private Rect GetRenderBounds(double strokeThickness)
/// <summary>
/// Create new empty <see cref="SKPath"/>.
/// </summary>
/// <returns>Empty <see cref="SKPath"/></returns>
private static SKPath CreateEmptyPath()
{
// TODO: Calculate properly.
return Bounds.Inflate(strokeThickness);
return new SKPath
{
FillType = SKPathFillType.EvenOdd
};
}
class StreamContext : IStreamGeometryContextImpl
/// <summary>
/// A Skia implementation of a <see cref="IStreamGeometryContextImpl"/>.
/// </summary>
private class StreamContext : IStreamGeometryContextImpl
{
private readonly StreamGeometryImpl _geometryImpl;
private SKPath _path;
private readonly SKPath _path;
Point _currentPoint;
/// <summary>
/// Initializes a new instance of the <see cref="StreamContext"/> class.
/// <param name="geometryImpl">Geometry to operate on.</param>
/// </summary>
public StreamContext(StreamGeometryImpl geometryImpl)
{
_geometryImpl = geometryImpl;
_path = _geometryImpl._path;
_path = _geometryImpl._effectivePath;
}
/// <inheritdoc />
/// <remarks>Will update bounds of passed geometry.</remarks>
public void Dispose()
{
SKRect rc;
_path.GetBounds(out rc);
_geometryImpl._bounds = rc.ToAvaloniaRect();
_geometryImpl._bounds = _path.TightBounds.ToAvaloniaRect();
_geometryImpl.InvalidateCaches();
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
public void BeginFigure(Point startPoint, bool isFilled)
{
_path.MoveTo((float)startPoint.X, (float)startPoint.Y);
_currentPoint = startPoint;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
public void QuadraticBezierTo(Point point1, Point point2)
{
_path.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y);
_currentPoint = point2;
}
/// <inheritdoc />
public void LineTo(Point point)
{
_path.LineTo((float)point.X, (float)point.Y);
_currentPoint = point;
}
/// <inheritdoc />
public void EndFigure(bool isClosed)
{
if (isClosed)
@ -130,6 +143,7 @@ namespace Avalonia.Skia
}
}
/// <inheritdoc />
public void SetFillRule(FillRule fillRule)
{
_path.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding;

169
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
{
/// <summary>
/// Skia render target that writes to a surface.
/// </summary>
public class SurfaceRenderTarget : IRenderTargetBitmapImpl, IDrawableBitmapImpl
{
private readonly Vector _dpi;
private readonly SKSurface _surface;
private readonly SKCanvas _canvas;
private readonly bool _disableLcdRendering;
/// <summary>
/// Create new surface render target.
/// </summary>
/// <param name="createInfo">Create info.</param>
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");
}
}
/// <summary>
/// Create backing Skia surface.
/// </summary>
/// <param name="width">Width.</param>
/// <param name="height">Height.</param>
/// <param name="format">Format.</param>
/// <returns></returns>
private static SKSurface CreateSurface(int width, int height, PixelFormat? format)
{
var imageInfo = MakeImageInfo(width, height, format);
return SKSurface.Create(imageInfo);
}
/// <inheritdoc />
public void Dispose()
{
_canvas.Dispose();
_surface.Dispose();
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public int PixelWidth { get; }
/// <inheritdoc />
public int PixelHeight { get; }
/// <inheritdoc />
public void Save(string fileName)
{
using (var image = SnapshotImage())
{
ImageSavingHelper.SaveImage(image, fileName);
}
}
/// <inheritdoc />
public void Save(Stream stream)
{
using (var image = SnapshotImage())
{
ImageSavingHelper.SaveImage(image, stream);
}
}
/// <inheritdoc />
public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint)
{
using (var image = SnapshotImage())
{
context.Canvas.DrawImage(image, sourceRect, destRect, paint);
}
}
/// <summary>
/// Create Skia image snapshot from a surface.
/// </summary>
/// <returns>Image snapshot.</returns>
public SKImage SnapshotImage()
{
return _surface.Snapshot();
}
/// <summary>
/// Create image info for given parameters.
/// </summary>
/// <param name="width">Width.</param>
/// <param name="height">Height.</param>
/// <param name="format">Format.</param>
/// <returns></returns>
private static SKImageInfo MakeImageInfo(int width, int height, PixelFormat? format)
{
var colorType = PixelFormatHelper.ResolveColorType(format);
return new SKImageInfo(width, height, colorType, SKAlphaType.Premul);
}
/// <summary>
/// Create info of a surface render target.
/// </summary>
public struct CreateInfo
{
/// <summary>
/// Width of a render target.
/// </summary>
public int Width;
/// <summary>
/// Height of a render target.
/// </summary>
public int Height;
/// <summary>
/// Dpi used when rendering to a surface.
/// </summary>
public Vector Dpi;
/// <summary>
/// Pixel format of a render target.
/// </summary>
public PixelFormat? Format;
/// <summary>
/// Render text without Lcd rendering.
/// </summary>
public bool DisableTextLcdRendering;
}
}
}

62
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
/// <summary>
/// A Skia implementation of a <see cref="ITransformedGeometryImpl"/>.
/// </summary>
public class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl
{
/// <summary>
/// Initializes a new instance of the <see cref="TransformedGeometryImpl"/> class.
/// </summary>
/// <param name="source">Source geometry.</param>
/// <param name="transform">Transform of new geometry.</param>
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();
}
/// <inheritdoc />
public override SKPath EffectivePath { get; }
/// <inheritdoc />
public IGeometryImpl SourceGeometry { get; }
/// <inheritdoc />
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);
}
/// <inheritdoc />
public override Rect Bounds { get; }
}
}

9
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
/// <summary>
/// Cache for Skia typefaces.
/// </summary>
internal static class TypefaceCache
{
public static SKTypeface Default = SKTypeface.FromFamilyName(FontFamily.Default.Name);
static readonly Dictionary<string, Dictionary<FontKey, SKTypeface>> Cache = new Dictionary<string, Dictionary<FontKey, SKTypeface>>();

151
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
{
/// <summary>
/// Skia based writeable bitmap.
/// </summary>
public class WriteableBitmapImpl : IWriteableBitmapImpl, IDrawableBitmapImpl
{
private static readonly SKBitmapReleaseDelegate s_releaseDelegate = ReleaseProc;
private readonly SKBitmap _bitmap;
/// <summary>
/// Create new writeable bitmap.
/// </summary>
/// <param name="width">Width.</param>
/// <param name="height">Height.</param>
/// <param name="format">Format.</param>
public WriteableBitmapImpl(int width, int height, PixelFormat? format = null)
{
PixelHeight = height;
PixelWidth = width;
var colorType = PixelFormatHelper.ResolveColorType(format);
var runtimePlatform = AvaloniaLocator.Current?.GetService<IRuntimePlatform>();
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);
}
/// <inheritdoc />
public int PixelWidth { get; }
/// <inheritdoc />
public int PixelHeight { get; }
/// <inheritdoc />
public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint)
{
context.Canvas.DrawBitmap(_bitmap, sourceRect, destRect, paint);
}
/// <inheritdoc />
public void Dispose()
{
_bitmap.Dispose();
}
/// <inheritdoc />
public void Save(Stream stream)
{
using (var image = GetSnapshot())
{
ImageSavingHelper.SaveImage(image, stream);
}
}
/// <inheritdoc />
public void Save(string fileName)
{
using (var image = GetSnapshot())
{
ImageSavingHelper.SaveImage(image, fileName);
}
}
/// <inheritdoc />
public ILockedFramebuffer Lock() => new BitmapFramebuffer(_bitmap);
/// <summary>
/// Get snapshot as image.
/// </summary>
/// <returns>Image snapshot.</returns>
public SKImage GetSnapshot()
{
return SKImage.FromPixels(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes);
}
/// <summary>
/// Release given unmanaged blob.
/// </summary>
/// <param name="address">Blob address.</param>
/// <param name="ctx">Blob.</param>
private static void ReleaseProc(IntPtr address, object ctx)
{
((IUnmanagedBlob)ctx).Dispose();
}
/// <summary>
/// Framebuffer for bitmap.
/// </summary>
private class BitmapFramebuffer : ILockedFramebuffer
{
private SKBitmap _bitmap;
/// <summary>
/// Create framebuffer from given bitmap.
/// </summary>
/// <param name="bitmap">Bitmap.</param>
public BitmapFramebuffer(SKBitmap bitmap)
{
_bitmap = bitmap;
}
/// <inheritdoc />
public void Dispose()
{
_bitmap = null;
}
/// <inheritdoc />
public IntPtr Address => _bitmap.GetPixels();
/// <inheritdoc />
public int Width => _bitmap.Width;
/// <inheritdoc />
public int Height => _bitmap.Height;
/// <inheritdoc />
public int RowBytes => _bitmap.RowBytes;
/// <inheritdoc />
public Vector Dpi { get; } = SkiaPlatform.DefaultDpi;
/// <inheritdoc />
public PixelFormat Format => _bitmap.ColorType.ToPixelFormat();
}
}
}

34
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)

329
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")]

9
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)
{

43
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);

228
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<string[]> 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<UnmanagedMethods.COMDLG_FILTERSPEC>();
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<string>();
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 "";
}
}
}

2
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -724,7 +724,7 @@ namespace Avalonia.Win32
UnmanagedMethods.WNDCLASSEX wndClassEx = new UnmanagedMethods.WNDCLASSEX
{
cbSize = Marshal.SizeOf<UnmanagedMethods.WNDCLASSEX>(),
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,

5
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0</TargetFrameworks>
<TargetFramework>netcoreapp2.0</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Animation\Avalonia.Animation.csproj" />
@ -15,7 +16,7 @@
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.10.8" />
<PackageReference Include="BenchmarkDotNet" Version="0.10.14" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />

45
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);
}
}
}
}

2
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
]

12
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

12
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

6
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

6
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

22
tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0</TargetFrameworks>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\Moq.props" />
<Import Project="..\..\build\XUnit.props" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\Microsoft.Reactive.Testing.props" />
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Animation\Avalonia.Animation.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Input\Avalonia.Input.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Interactivity\Avalonia.Interactivity.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Layout\Avalonia.Layout.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Visuals\Avalonia.Visuals.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Styling\Avalonia.Styling.csproj" />
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
</ItemGroup>
</Project>

79
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);
}
}
}
}

8
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)]

152
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<IStreamGeometryContextImpl>();
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<IStreamGeometryContextImpl>();
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<LineSegment>(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<IStreamGeometryContextImpl>();
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<LineSegment>(segment);
var lineSegment = (LineSegment)segment;
Assert.Equal(new Point(10, 10), lineSegment.Point);
figure = pathGeometry.Figures[1];
segment = figure.Segments[0];
Assert.IsType<LineSegment>(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<LineSegment>(segment);
var lineSegment = (LineSegment)segment;
Assert.Equal(new Point(10, 10), lineSegment.Point);
segment = figure.Segments[1];
Assert.IsType<LineSegment>(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<IStreamGeometryContextImpl> implMock = null)
[Theory]
[InlineData("0 0")]
[InlineData("j")]
public void Throws_InvalidDataException_On_None_Defined_Command(string pathData)
{
AvaloniaLocator.CurrentMutable
.Bind<IPlatformRenderInterface>()
.ToConstant(Mock.Of<IPlatformRenderInterface>());
return new PathMarkupParser(
new StreamGeometryContext(implMock != null ? implMock.Object : Mock.Of<IStreamGeometryContextImpl>()));
var pathGeometry = new PathGeometry();
using (var context = new PathGeometryContext(pathGeometry))
using (var parser = new PathMarkupParser(context))
{
Assert.Throws<InvalidDataException>(() => parser.Parse(pathData));
}
}
}
}

BIN
tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Horizontal_Fill.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 652 B

After

Width:  |  Height:  |  Size: 642 B

BIN
tests/TestFiles/Skia/Media/LinearGradientBrush/LinearGradientBrush_RedBlue_Vertical_Fill.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

BIN
tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
tests/TestFiles/Skia/Shapes/Path/Path_With_PenLineCap.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Loading…
Cancel
Save