diff --git a/Avalonia.sln b/Avalonia.sln
index 54f6f5e7e7..39396f3ab8 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -71,7 +71,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup", "src\Mark
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.UnitTests", "tests\Avalonia.Markup.UnitTests\Avalonia.Markup.UnitTests.csproj", "{8EF392D5-1416-45AA-9956-7CBBC3229E8A}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BindingTest", "samples\BindingTest\BindingTest.csproj", "{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BindingDemo", "samples\BindingDemo\BindingDemo.csproj", "{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}"
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "RenderHelpers", "src\Shared\RenderHelpers\RenderHelpers.shproj", "{3C4C0CB4-0C0F-4450-A37B-148C84FF905F}"
EndProject
@@ -109,7 +109,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.Te
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.TestApp", "tests\Avalonia.DesignerSupport.TestApp\Avalonia.DesignerSupport.TestApp.csproj", "{F1381F98-4D24-409A-A6C5-1C5B1E08BB08}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtualizationTest", "samples\VirtualizationTest\VirtualizationTest.csproj", "{FBCAF3D0-2808-4934-8E96-3F607594517B}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtualizationDemo", "samples\VirtualizationDemo\VirtualizationDemo.csproj", "{FBCAF3D0-2808-4934-8E96-3F607594517B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Interop", "Interop", "{A0CC0258-D18C-4AB3-854F-7101680FC3F9}"
EndProject
@@ -117,7 +117,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsInteropTest", "sampl
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DotNetFrameworkRuntime", "src\Avalonia.DotNetFrameworkRuntime\Avalonia.DotNetFrameworkRuntime.csproj", "{4A1ABB09-9047-4BD5-A4AD-A055E52C5EE0}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RenderTest", "samples\RenderTest\RenderTest.csproj", "{F1FDC5B0-4654-416F-AE69-E3E9BBD87801}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RenderDemo", "samples\RenderDemo\RenderDemo.csproj", "{F1FDC5B0-4654-416F-AE69-E3E9BBD87801}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Android", "samples\ControlCatalog.Android\ControlCatalog.Android.csproj", "{29132311-1848-4FD6-AE0C-4FF841151BD3}"
EndProject
@@ -141,6 +141,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\Microsoft.Reactive.Testing.props = build\Microsoft.Reactive.Testing.props
build\Moq.props = build\Moq.props
build\NetCore.props = build\NetCore.props
+ build\NetFX.props = build\NetFX.props
build\ReactiveUI.props = build\ReactiveUI.props
build\Rx.props = build\Rx.props
build\SampleApp.props = build\SampleApp.props
@@ -169,7 +170,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia.RenderTests",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Remote.Protocol", "src\Avalonia.Remote.Protocol\Avalonia.Remote.Protocol.csproj", "{D78A720C-C0C6-478B-8564-F167F9BDD01B}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RemoteTest", "samples\RemoteTest\RemoteTest.csproj", "{E2999E4A-9086-401F-898C-AEB0AD38E676}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RemoteDemo", "samples\RemoteDemo\RemoteDemo.csproj", "{E2999E4A-9086-401F-898C-AEB0AD38E676}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{4ED8B739-6F4E-4CD4-B993-545E6B5CE637}"
EndProject
@@ -183,6 +184,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.MonoMac", "src\OSX
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Designer.HostApp.NetFX", "src\tools\Avalonia.Designer.HostApp.NetFX\Avalonia.Designer.HostApp.NetFX.csproj", "{4ADA61C8-D191-428D-9066-EF4F0D86520F}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia.UnitTests", "tests\Avalonia.Skia.UnitTests\Avalonia.Skia.UnitTests.csproj", "{E1240B49-7B4B-4371-A00E-068778C5CF0B}"
+EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
@@ -2468,6 +2471,46 @@ Global
{4ADA61C8-D191-428D-9066-EF4F0D86520F}.Release|NetCoreOnly.ActiveCfg = Release|Any CPU
{4ADA61C8-D191-428D-9066-EF4F0D86520F}.Release|x86.ActiveCfg = Release|Any CPU
{4ADA61C8-D191-428D-9066-EF4F0D86520F}.Release|x86.Build.0 = Release|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|NetCoreOnly.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|NetCoreOnly.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Ad-Hoc|x86.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhone.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|NetCoreOnly.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|NetCoreOnly.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|x86.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.AppStore|x86.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|NetCoreOnly.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|NetCoreOnly.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|x86.Build.0 = Debug|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhone.Build.0 = Release|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|NetCoreOnly.ActiveCfg = Release|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|NetCoreOnly.Build.0 = Release|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|x86.ActiveCfg = Release|Any CPU
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2520,6 +2563,7 @@ Global
{F40FC0A2-1BC3-401C-BFC1-928EC4D4A9CE} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{CBFD5788-567D-401B-9DFA-74E4224025A0} = {A59C4C0A-64DF-4621-B450-2BA00D6F61E2}
{4ADA61C8-D191-428D-9066-EF4F0D86520F} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
+ {E1240B49-7B4B-4371-A00E-068778C5CF0B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}
diff --git a/build.cake b/build.cake
index bf3ae41b58..561a33186a 100644
--- a/build.cake
+++ b/build.cake
@@ -207,6 +207,7 @@ Task("Run-Unit-Tests")
RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Styling.UnitTests", data.Parameters, false);
RunCoreTest("./tests/Avalonia.Visuals.UnitTests", data.Parameters, false);
+ RunCoreTest("./tests/Avalonia.Skia.UnitTests", data.Parameters, false);
if (data.Parameters.IsRunningOnWindows)
{
RunCoreTest("./tests/Avalonia.Direct2D1.UnitTests", data.Parameters, true);
diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props
index 04e8a3ad4f..35c979a95e 100644
--- a/build/SkiaSharp.props
+++ b/build/SkiaSharp.props
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/samples/BindingTest/App.config b/samples/BindingDemo/App.config
similarity index 100%
rename from samples/BindingTest/App.config
rename to samples/BindingDemo/App.config
diff --git a/samples/BindingTest/App.xaml b/samples/BindingDemo/App.xaml
similarity index 100%
rename from samples/BindingTest/App.xaml
rename to samples/BindingDemo/App.xaml
diff --git a/samples/BindingTest/App.xaml.cs b/samples/BindingDemo/App.xaml.cs
similarity index 95%
rename from samples/BindingTest/App.xaml.cs
rename to samples/BindingDemo/App.xaml.cs
index ccad1d0ba9..01c52a2a49 100644
--- a/samples/BindingTest/App.xaml.cs
+++ b/samples/BindingDemo/App.xaml.cs
@@ -5,7 +5,7 @@ using Avalonia.Logging.Serilog;
using Avalonia.Markup.Xaml;
using Serilog;
-namespace BindingTest
+namespace BindingDemo
{
public class App : Application
{
diff --git a/samples/BindingTest/BindingTest.csproj b/samples/BindingDemo/BindingDemo.csproj
similarity index 100%
rename from samples/BindingTest/BindingTest.csproj
rename to samples/BindingDemo/BindingDemo.csproj
diff --git a/samples/BindingTest/MainWindow.xaml b/samples/BindingDemo/MainWindow.xaml
similarity index 98%
rename from samples/BindingTest/MainWindow.xaml
rename to samples/BindingDemo/MainWindow.xaml
index 6b80225686..a69fb75742 100644
--- a/samples/BindingTest/MainWindow.xaml
+++ b/samples/BindingDemo/MainWindow.xaml
@@ -1,7 +1,7 @@
diff --git a/samples/BindingTest/MainWindow.xaml.cs b/samples/BindingDemo/MainWindow.xaml.cs
similarity index 88%
rename from samples/BindingTest/MainWindow.xaml.cs
rename to samples/BindingDemo/MainWindow.xaml.cs
index c1c3c09406..eaa57e1f5f 100644
--- a/samples/BindingTest/MainWindow.xaml.cs
+++ b/samples/BindingDemo/MainWindow.xaml.cs
@@ -1,9 +1,9 @@
-using BindingTest.ViewModels;
+using BindingDemo.ViewModels;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
-namespace BindingTest
+namespace BindingDemo
{
public class MainWindow : Window
{
diff --git a/samples/BindingTest/TestItemView.xaml b/samples/BindingDemo/TestItemView.xaml
similarity index 100%
rename from samples/BindingTest/TestItemView.xaml
rename to samples/BindingDemo/TestItemView.xaml
diff --git a/samples/BindingTest/TestItemView.xaml.cs b/samples/BindingDemo/TestItemView.xaml.cs
similarity index 93%
rename from samples/BindingTest/TestItemView.xaml.cs
rename to samples/BindingDemo/TestItemView.xaml.cs
index 32f367ef68..8c0b592f00 100644
--- a/samples/BindingTest/TestItemView.xaml.cs
+++ b/samples/BindingDemo/TestItemView.xaml.cs
@@ -1,7 +1,7 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
-namespace BindingTest
+namespace BindingDemo
{
public class TestItemView : UserControl
{
diff --git a/samples/BindingTest/ViewModels/DataAnnotationsErrorViewModel.cs b/samples/BindingDemo/ViewModels/DataAnnotationsErrorViewModel.cs
similarity index 92%
rename from samples/BindingTest/ViewModels/DataAnnotationsErrorViewModel.cs
rename to samples/BindingDemo/ViewModels/DataAnnotationsErrorViewModel.cs
index 634498c165..e274f9180e 100644
--- a/samples/BindingTest/ViewModels/DataAnnotationsErrorViewModel.cs
+++ b/samples/BindingDemo/ViewModels/DataAnnotationsErrorViewModel.cs
@@ -3,7 +3,7 @@
using System.ComponentModel.DataAnnotations;
-namespace BindingTest.ViewModels
+namespace BindingDemo.ViewModels
{
public class DataAnnotationsErrorViewModel
{
diff --git a/samples/BindingTest/ViewModels/ExceptionErrorViewModel.cs b/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs
similarity index 95%
rename from samples/BindingTest/ViewModels/ExceptionErrorViewModel.cs
rename to samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs
index e6071e0678..2ab6c26e68 100644
--- a/samples/BindingTest/ViewModels/ExceptionErrorViewModel.cs
+++ b/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs
@@ -4,7 +4,7 @@
using ReactiveUI;
using System;
-namespace BindingTest.ViewModels
+namespace BindingDemo.ViewModels
{
public class ExceptionErrorViewModel : ReactiveObject
{
diff --git a/samples/BindingTest/ViewModels/IndeiErrorViewModel.cs b/samples/BindingDemo/ViewModels/IndeiErrorViewModel.cs
similarity index 98%
rename from samples/BindingTest/ViewModels/IndeiErrorViewModel.cs
rename to samples/BindingDemo/ViewModels/IndeiErrorViewModel.cs
index b4bb528abb..bb3b4d64e9 100644
--- a/samples/BindingTest/ViewModels/IndeiErrorViewModel.cs
+++ b/samples/BindingDemo/ViewModels/IndeiErrorViewModel.cs
@@ -6,7 +6,7 @@ using System;
using System.ComponentModel;
using System.Collections;
-namespace BindingTest.ViewModels
+namespace BindingDemo.ViewModels
{
public class IndeiErrorViewModel : ReactiveObject, INotifyDataErrorInfo
{
diff --git a/samples/BindingTest/ViewModels/MainWindowViewModel.cs b/samples/BindingDemo/ViewModels/MainWindowViewModel.cs
similarity index 99%
rename from samples/BindingTest/ViewModels/MainWindowViewModel.cs
rename to samples/BindingDemo/ViewModels/MainWindowViewModel.cs
index 1116810ccb..858fb5159a 100644
--- a/samples/BindingTest/ViewModels/MainWindowViewModel.cs
+++ b/samples/BindingDemo/ViewModels/MainWindowViewModel.cs
@@ -6,7 +6,7 @@ using System.Reactive.Linq;
using System.Threading.Tasks;
using System.Threading;
-namespace BindingTest.ViewModels
+namespace BindingDemo.ViewModels
{
public class MainWindowViewModel : ReactiveObject
{
diff --git a/samples/BindingTest/ViewModels/NestedCommandViewModel.cs b/samples/BindingDemo/ViewModels/NestedCommandViewModel.cs
similarity index 92%
rename from samples/BindingTest/ViewModels/NestedCommandViewModel.cs
rename to samples/BindingDemo/ViewModels/NestedCommandViewModel.cs
index 886ecbed8e..0e9139ab98 100644
--- a/samples/BindingTest/ViewModels/NestedCommandViewModel.cs
+++ b/samples/BindingDemo/ViewModels/NestedCommandViewModel.cs
@@ -6,7 +6,7 @@ using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
-namespace BindingTest.ViewModels
+namespace BindingDemo.ViewModels
{
public class NestedCommandViewModel : ReactiveObject
{
diff --git a/samples/BindingTest/ViewModels/TestItem.cs b/samples/BindingDemo/ViewModels/TestItem.cs
similarity index 93%
rename from samples/BindingTest/ViewModels/TestItem.cs
rename to samples/BindingDemo/ViewModels/TestItem.cs
index 2326a92b7d..5a9f192f58 100644
--- a/samples/BindingTest/ViewModels/TestItem.cs
+++ b/samples/BindingDemo/ViewModels/TestItem.cs
@@ -1,6 +1,6 @@
using ReactiveUI;
-namespace BindingTest.ViewModels
+namespace BindingDemo.ViewModels
{
public class TestItem : ReactiveObject
{
diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs
index b45a93455e..1f53dedc14 100644
--- a/samples/ControlCatalog.NetCore/Program.cs
+++ b/samples/ControlCatalog.NetCore/Program.cs
@@ -1,9 +1,9 @@
using System;
using System.Diagnostics;
using System.Linq;
-using System.Runtime.InteropServices;
using System.Threading;
using Avalonia;
+using Avalonia.Skia;
namespace ControlCatalog.NetCore
{
@@ -37,7 +37,7 @@ namespace ControlCatalog.NetCore
/// This method is needed for IDE previewer infrastructure
///
public static AppBuilder BuildAvaloniaApp()
- => AppBuilder.Configure().UsePlatformDetect().UseReactiveUI();
+ => AppBuilder.Configure().UsePlatformDetect().UseSkia().UseReactiveUI();
static void ConsoleSilencer()
{
diff --git a/samples/RemoteTest/Program.cs b/samples/RemoteDemo/Program.cs
similarity index 98%
rename from samples/RemoteTest/Program.cs
rename to samples/RemoteDemo/Program.cs
index f518e77143..0565b676fb 100644
--- a/samples/RemoteTest/Program.cs
+++ b/samples/RemoteDemo/Program.cs
@@ -9,7 +9,7 @@ using Avalonia.Remote.Protocol;
using Avalonia.Threading;
using ControlCatalog;
-namespace RemoteTest
+namespace RemoteDemo
{
class Program
{
diff --git a/samples/RemoteTest/RemoteTest.csproj b/samples/RemoteDemo/RemoteDemo.csproj
similarity index 100%
rename from samples/RemoteTest/RemoteTest.csproj
rename to samples/RemoteDemo/RemoteDemo.csproj
diff --git a/samples/RenderTest/App.config b/samples/RenderDemo/App.config
similarity index 100%
rename from samples/RenderTest/App.config
rename to samples/RenderDemo/App.config
diff --git a/samples/RenderTest/App.xaml b/samples/RenderDemo/App.xaml
similarity index 81%
rename from samples/RenderTest/App.xaml
rename to samples/RenderDemo/App.xaml
index c119f54915..aee75cb139 100644
--- a/samples/RenderTest/App.xaml
+++ b/samples/RenderDemo/App.xaml
@@ -2,6 +2,6 @@
-
+
\ No newline at end of file
diff --git a/samples/RenderTest/App.xaml.cs b/samples/RenderDemo/App.xaml.cs
similarity index 97%
rename from samples/RenderTest/App.xaml.cs
rename to samples/RenderDemo/App.xaml.cs
index fd2b940f6a..0f627961e6 100644
--- a/samples/RenderTest/App.xaml.cs
+++ b/samples/RenderDemo/App.xaml.cs
@@ -5,7 +5,7 @@ using Avalonia;
using Avalonia.Logging.Serilog;
using Avalonia.Markup.Xaml;
-namespace RenderTest
+namespace RenderDemo
{
public class App : Application
{
diff --git a/samples/RenderTest/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml
similarity index 95%
rename from samples/RenderTest/MainWindow.xaml
rename to samples/RenderDemo/MainWindow.xaml
index da49054b77..df2b221423 100644
--- a/samples/RenderTest/MainWindow.xaml
+++ b/samples/RenderDemo/MainWindow.xaml
@@ -1,6 +1,6 @@
diff --git a/samples/RenderTest/MainWindow.xaml.cs b/samples/RenderDemo/MainWindow.xaml.cs
similarity index 94%
rename from samples/RenderTest/MainWindow.xaml.cs
rename to samples/RenderDemo/MainWindow.xaml.cs
index 76a8e81aca..f1f974f7a1 100644
--- a/samples/RenderTest/MainWindow.xaml.cs
+++ b/samples/RenderDemo/MainWindow.xaml.cs
@@ -5,10 +5,10 @@ using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
-using RenderTest.ViewModels;
+using RenderDemo.ViewModels;
using ReactiveUI;
-namespace RenderTest
+namespace RenderDemo
{
public class MainWindow : Window
{
diff --git a/samples/RenderTest/Pages/AnimationsPage.xaml b/samples/RenderDemo/Pages/AnimationsPage.xaml
similarity index 100%
rename from samples/RenderTest/Pages/AnimationsPage.xaml
rename to samples/RenderDemo/Pages/AnimationsPage.xaml
diff --git a/samples/RenderTest/Pages/AnimationsPage.xaml.cs b/samples/RenderDemo/Pages/AnimationsPage.xaml.cs
similarity index 90%
rename from samples/RenderTest/Pages/AnimationsPage.xaml.cs
rename to samples/RenderDemo/Pages/AnimationsPage.xaml.cs
index 2623721393..5b02fd9297 100644
--- a/samples/RenderTest/Pages/AnimationsPage.xaml.cs
+++ b/samples/RenderDemo/Pages/AnimationsPage.xaml.cs
@@ -7,9 +7,9 @@ using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
-using RenderTest.ViewModels;
+using RenderDemo.ViewModels;
-namespace RenderTest.Pages
+namespace RenderDemo.Pages
{
public class AnimationsPage : UserControl
{
diff --git a/samples/RenderTest/Pages/ClippingPage.xaml b/samples/RenderDemo/Pages/ClippingPage.xaml
similarity index 100%
rename from samples/RenderTest/Pages/ClippingPage.xaml
rename to samples/RenderDemo/Pages/ClippingPage.xaml
diff --git a/samples/RenderTest/Pages/ClippingPage.xaml.cs b/samples/RenderDemo/Pages/ClippingPage.xaml.cs
similarity index 96%
rename from samples/RenderTest/Pages/ClippingPage.xaml.cs
rename to samples/RenderDemo/Pages/ClippingPage.xaml.cs
index 2a79076d4c..5357181838 100644
--- a/samples/RenderTest/Pages/ClippingPage.xaml.cs
+++ b/samples/RenderDemo/Pages/ClippingPage.xaml.cs
@@ -7,7 +7,7 @@ using Avalonia.Data;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
-namespace RenderTest.Pages
+namespace RenderDemo.Pages
{
public class ClippingPage : UserControl
{
diff --git a/samples/RenderTest/Pages/DrawingPage.xaml b/samples/RenderDemo/Pages/DrawingPage.xaml
similarity index 100%
rename from samples/RenderTest/Pages/DrawingPage.xaml
rename to samples/RenderDemo/Pages/DrawingPage.xaml
diff --git a/samples/RenderTest/Pages/DrawingPage.xaml.cs b/samples/RenderDemo/Pages/DrawingPage.xaml.cs
similarity index 91%
rename from samples/RenderTest/Pages/DrawingPage.xaml.cs
rename to samples/RenderDemo/Pages/DrawingPage.xaml.cs
index 3bf9bd545d..3475e1aa07 100644
--- a/samples/RenderTest/Pages/DrawingPage.xaml.cs
+++ b/samples/RenderDemo/Pages/DrawingPage.xaml.cs
@@ -1,7 +1,7 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
-namespace RenderTest.Pages
+namespace RenderDemo.Pages
{
public class DrawingPage : UserControl
{
diff --git a/samples/RenderTest/RenderTest.csproj b/samples/RenderDemo/RenderDemo.csproj
similarity index 100%
rename from samples/RenderTest/RenderTest.csproj
rename to samples/RenderDemo/RenderDemo.csproj
diff --git a/samples/RenderTest/SideBar.xaml b/samples/RenderDemo/SideBar.xaml
similarity index 100%
rename from samples/RenderTest/SideBar.xaml
rename to samples/RenderDemo/SideBar.xaml
diff --git a/samples/RenderTest/ViewModels/AnimationsPageViewModel.cs b/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs
similarity index 97%
rename from samples/RenderTest/ViewModels/AnimationsPageViewModel.cs
rename to samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs
index 17eee547a1..626a3e7c77 100644
--- a/samples/RenderTest/ViewModels/AnimationsPageViewModel.cs
+++ b/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs
@@ -2,7 +2,7 @@
using ReactiveUI;
using Avalonia.Animation;
-namespace RenderTest.ViewModels
+namespace RenderDemo.ViewModels
{
public class AnimationsPageViewModel : ReactiveObject
{
diff --git a/samples/RenderTest/ViewModels/MainWindowViewModel.cs b/samples/RenderDemo/ViewModels/MainWindowViewModel.cs
similarity index 96%
rename from samples/RenderTest/ViewModels/MainWindowViewModel.cs
rename to samples/RenderDemo/ViewModels/MainWindowViewModel.cs
index 02a2abeb89..0cb5e1b87b 100644
--- a/samples/RenderTest/ViewModels/MainWindowViewModel.cs
+++ b/samples/RenderDemo/ViewModels/MainWindowViewModel.cs
@@ -1,7 +1,7 @@
using System;
using ReactiveUI;
-namespace RenderTest.ViewModels
+namespace RenderDemo.ViewModels
{
public class MainWindowViewModel : ReactiveObject
{
diff --git a/samples/VirtualizationTest/App.config b/samples/VirtualizationDemo/App.config
similarity index 100%
rename from samples/VirtualizationTest/App.config
rename to samples/VirtualizationDemo/App.config
diff --git a/samples/VirtualizationTest/App.xaml b/samples/VirtualizationDemo/App.xaml
similarity index 100%
rename from samples/VirtualizationTest/App.xaml
rename to samples/VirtualizationDemo/App.xaml
diff --git a/samples/VirtualizationTest/App.xaml.cs b/samples/VirtualizationDemo/App.xaml.cs
similarity index 92%
rename from samples/VirtualizationTest/App.xaml.cs
rename to samples/VirtualizationDemo/App.xaml.cs
index 14ab5b3f84..b220807443 100644
--- a/samples/VirtualizationTest/App.xaml.cs
+++ b/samples/VirtualizationDemo/App.xaml.cs
@@ -4,7 +4,7 @@
using Avalonia;
using Avalonia.Markup.Xaml;
-namespace VirtualizationTest
+namespace VirtualizationDemo
{
public class App : Application
{
diff --git a/samples/VirtualizationTest/MainWindow.xaml b/samples/VirtualizationDemo/MainWindow.xaml
similarity index 100%
rename from samples/VirtualizationTest/MainWindow.xaml
rename to samples/VirtualizationDemo/MainWindow.xaml
diff --git a/samples/VirtualizationTest/MainWindow.xaml.cs b/samples/VirtualizationDemo/MainWindow.xaml.cs
similarity index 89%
rename from samples/VirtualizationTest/MainWindow.xaml.cs
rename to samples/VirtualizationDemo/MainWindow.xaml.cs
index 952383dffb..271519b10b 100644
--- a/samples/VirtualizationTest/MainWindow.xaml.cs
+++ b/samples/VirtualizationDemo/MainWindow.xaml.cs
@@ -4,9 +4,9 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
-using VirtualizationTest.ViewModels;
+using VirtualizationDemo.ViewModels;
-namespace VirtualizationTest
+namespace VirtualizationDemo
{
public class MainWindow : Window
{
diff --git a/samples/VirtualizationTest/Program.cs b/samples/VirtualizationDemo/Program.cs
similarity index 94%
rename from samples/VirtualizationTest/Program.cs
rename to samples/VirtualizationDemo/Program.cs
index 097f0cfdc7..98f1f08d6c 100644
--- a/samples/VirtualizationTest/Program.cs
+++ b/samples/VirtualizationDemo/Program.cs
@@ -7,7 +7,7 @@ using Avalonia.Controls;
using Avalonia.Logging.Serilog;
using Serilog;
-namespace VirtualizationTest
+namespace VirtualizationDemo
{
class Program
{
diff --git a/samples/VirtualizationTest/ViewModels/ItemViewModel.cs b/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs
similarity index 92%
rename from samples/VirtualizationTest/ViewModels/ItemViewModel.cs
rename to samples/VirtualizationDemo/ViewModels/ItemViewModel.cs
index 75777012c1..e883cdfeb9 100644
--- a/samples/VirtualizationTest/ViewModels/ItemViewModel.cs
+++ b/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs
@@ -4,7 +4,7 @@
using System;
using ReactiveUI;
-namespace VirtualizationTest.ViewModels
+namespace VirtualizationDemo.ViewModels
{
internal class ItemViewModel : ReactiveObject
{
diff --git a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
similarity index 99%
rename from samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs
rename to samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
index 8eab91e06d..eb08ef9656 100644
--- a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs
+++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
@@ -9,7 +9,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using ReactiveUI;
-namespace VirtualizationTest.ViewModels
+namespace VirtualizationDemo.ViewModels
{
internal class MainWindowViewModel : ReactiveObject
{
diff --git a/samples/VirtualizationTest/VirtualizationTest.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj
similarity index 100%
rename from samples/VirtualizationTest/VirtualizationTest.csproj
rename to samples/VirtualizationDemo/VirtualizationDemo.csproj
diff --git a/src/Avalonia.Controls/Platform/InProcessDragSource.cs b/src/Avalonia.Controls/Platform/InProcessDragSource.cs
index 52e09aba45..f8ae9f4249 100644
--- a/src/Avalonia.Controls/Platform/InProcessDragSource.cs
+++ b/src/Avalonia.Controls/Platform/InProcessDragSource.cs
@@ -60,7 +60,7 @@ namespace Avalonia.Platform
{
_lastPosition = pt;
- RawDragEvent rawEvent = new RawDragEvent(_dragDrop, type, root, pt, _draggedData, _allowedEffects);
+ RawDragEvent rawEvent = new RawDragEvent(_dragDrop, type, root, pt, _draggedData, _allowedEffects, modifiers);
var tl = root.GetSelfAndVisualAncestors().OfType().FirstOrDefault();
tl.PlatformImpl?.Input(rawEvent);
diff --git a/src/Avalonia.Input/DragDropDevice.cs b/src/Avalonia.Input/DragDropDevice.cs
index 9fb100371f..0692b21c66 100644
--- a/src/Avalonia.Input/DragDropDevice.cs
+++ b/src/Avalonia.Input/DragDropDevice.cs
@@ -19,11 +19,11 @@ namespace Avalonia.Input
return null;
}
- private DragDropEffects RaiseDragEvent(Interactive target, IInputElement inputRoot, Point point, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data)
+ private DragDropEffects RaiseDragEvent(Interactive target, IInputElement inputRoot, Point point, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data, InputModifiers modifiers)
{
if (target == null)
return DragDropEffects.None;
- var args = new DragEventArgs(routedEvent, data, target, inputRoot.TranslatePoint(point, target))
+ var args = new DragEventArgs(routedEvent, data, target, inputRoot.TranslatePoint(point, target), modifiers)
{
RoutedEvent = routedEvent,
DragEffects = operation
@@ -32,24 +32,24 @@ namespace Avalonia.Input
return args.DragEffects;
}
- private DragDropEffects DragEnter(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects)
+ private DragDropEffects DragEnter(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers)
{
_lastTarget = GetTarget(inputRoot, point);
- return RaiseDragEvent(_lastTarget, inputRoot, point, DragDrop.DragEnterEvent, effects, data);
+ return RaiseDragEvent(_lastTarget, inputRoot, point, DragDrop.DragEnterEvent, effects, data, modifiers);
}
- private DragDropEffects DragOver(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects)
+ private DragDropEffects DragOver(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers)
{
var target = GetTarget(inputRoot, point);
if (target == _lastTarget)
- return RaiseDragEvent(target, inputRoot, point, DragDrop.DragOverEvent, effects, data);
+ return RaiseDragEvent(target, inputRoot, point, DragDrop.DragOverEvent, effects, data, modifiers);
try
{
if (_lastTarget != null)
_lastTarget.RaiseEvent(new RoutedEventArgs(DragDrop.DragLeaveEvent));
- return RaiseDragEvent(target, inputRoot, point, DragDrop.DragEnterEvent, effects, data);
+ return RaiseDragEvent(target, inputRoot, point, DragDrop.DragEnterEvent, effects, data, modifiers);
}
finally
{
@@ -71,11 +71,11 @@ namespace Avalonia.Input
}
}
- private DragDropEffects Drop(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects)
+ private DragDropEffects Drop(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers)
{
try
{
- return RaiseDragEvent(_lastTarget, inputRoot, point, DragDrop.DropEvent, effects, data);
+ return RaiseDragEvent(_lastTarget, inputRoot, point, DragDrop.DropEvent, effects, data, modifiers);
}
finally
{
@@ -94,16 +94,16 @@ namespace Avalonia.Input
switch (e.Type)
{
case RawDragEventType.DragEnter:
- e.Effects = DragEnter(e.InputRoot, e.Location, e.Data, e.Effects);
+ e.Effects = DragEnter(e.InputRoot, e.Location, e.Data, e.Effects, e.Modifiers);
break;
case RawDragEventType.DragOver:
- e.Effects = DragOver(e.InputRoot, e.Location, e.Data, e.Effects);
+ e.Effects = DragOver(e.InputRoot, e.Location, e.Data, e.Effects, e.Modifiers);
break;
case RawDragEventType.DragLeave:
DragLeave(e.InputRoot);
break;
case RawDragEventType.Drop:
- e.Effects = Drop(e.InputRoot, e.Location, e.Data, e.Effects);
+ e.Effects = Drop(e.InputRoot, e.Location, e.Data, e.Effects, e.Modifiers);
break;
}
}
diff --git a/src/Avalonia.Input/DragEventArgs.cs b/src/Avalonia.Input/DragEventArgs.cs
index 669fd846a1..915ee4ee5c 100644
--- a/src/Avalonia.Input/DragEventArgs.cs
+++ b/src/Avalonia.Input/DragEventArgs.cs
@@ -13,6 +13,8 @@ namespace Avalonia.Input
public IDataObject Data { get; private set; }
+ public InputModifiers Modifiers { get; private set; }
+
public Point GetPosition(IVisual relativeTo)
{
var point = new Point(0, 0);
@@ -29,12 +31,13 @@ namespace Avalonia.Input
return point;
}
- public DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation)
+ public DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, InputModifiers modifiers)
: base(routedEvent)
{
this.Data = data;
this._target = target;
this._targetLocation = targetLocation;
+ this.Modifiers = modifiers;
}
}
diff --git a/src/Avalonia.Input/Raw/RawDragEvent.cs b/src/Avalonia.Input/Raw/RawDragEvent.cs
index 49125b4c07..80653b4873 100644
--- a/src/Avalonia.Input/Raw/RawDragEvent.cs
+++ b/src/Avalonia.Input/Raw/RawDragEvent.cs
@@ -11,9 +11,10 @@ namespace Avalonia.Input.Raw
public IDataObject Data { get; }
public DragDropEffects Effects { get; set; }
public RawDragEventType Type { get; }
+ public InputModifiers Modifiers { get; }
public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type,
- IInputElement inputRoot, Point location, IDataObject data, DragDropEffects effects)
+ IInputElement inputRoot, Point location, IDataObject data, DragDropEffects effects, InputModifiers modifiers)
:base(inputDevice, 0)
{
Type = type;
@@ -21,6 +22,7 @@ namespace Avalonia.Input.Raw
Location = location;
Data = data;
Effects = effects;
+ Modifiers = modifiers;
}
}
}
\ No newline at end of file
diff --git a/src/Avalonia.Visuals/Media/PathGeometry.cs b/src/Avalonia.Visuals/Media/PathGeometry.cs
index ecda07ada1..e7a2c8577a 100644
--- a/src/Avalonia.Visuals/Media/PathGeometry.cs
+++ b/src/Avalonia.Visuals/Media/PathGeometry.cs
@@ -5,6 +5,7 @@ using System;
using Avalonia.Collections;
using Avalonia.Metadata;
using Avalonia.Platform;
+using Avalonia.Visuals.Platform;
namespace Avalonia.Media
{
@@ -28,7 +29,7 @@ namespace Avalonia.Media
static PathGeometry()
{
- FiguresProperty.Changed.AddClassHandler((s, e) =>
+ FiguresProperty.Changed.AddClassHandler((s, e) =>
s.OnFiguresChanged(e.NewValue as PathFigures));
}
@@ -40,6 +41,24 @@ namespace Avalonia.Media
Figures = new PathFigures();
}
+ ///
+ /// Parses the specified path data to a .
+ ///
+ /// The s.
+ ///
+ public static new PathGeometry Parse(string pathData)
+ {
+ var pathGeometry = new PathGeometry();
+
+ using (var context = new PathGeometryContext(pathGeometry))
+ using (var parser = new PathMarkupParser(context))
+ {
+ parser.Parse(pathData);
+ }
+
+ return pathGeometry;
+ }
+
///
/// Gets or sets the figures.
///
diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs
index 9e4a3cbeae..a322d404bf 100644
--- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs
+++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs
@@ -5,50 +5,61 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
+using System.Linq;
using System.Text;
+using System.Text.RegularExpressions;
+using Avalonia.Platform;
namespace Avalonia.Media
{
///
/// Parses a path markup string.
///
- public class PathMarkupParser
+ public class PathMarkupParser : IDisposable
{
- private static readonly Dictionary Commands = new Dictionary
- {
- { 'F', Command.FillRule },
- { 'M', Command.Move },
- { 'L', Command.Line },
- { 'H', Command.HorizontalLine },
- { 'V', Command.VerticalLine },
- { 'Q', Command.QuadraticBezierCurve },
- { 'T', Command.SmoothQuadraticBezierCurve },
- { 'C', Command.CubicBezierCurve },
- { 'S', Command.SmoothCubicBezierCurve },
- { 'A', Command.Arc },
- { 'Z', Command.Close },
- };
-
- private static readonly Dictionary FillRules = new Dictionary
+ private static readonly string s_separatorPattern;
+ private static readonly Dictionary s_commands =
+ new Dictionary
+ {
+ { 'F', Command.FillRule },
+ { 'M', Command.Move },
+ { 'L', Command.Line },
+ { 'H', Command.HorizontalLine },
+ { 'V', Command.VerticalLine },
+ { 'Q', Command.QuadraticBezierCurve },
+ { 'T', Command.SmoothQuadraticBezierCurve },
+ { 'C', Command.CubicBezierCurve },
+ { 'S', Command.SmoothCubicBezierCurve },
+ { 'A', Command.Arc },
+ { 'Z', Command.Close },
+ };
+
+ private IGeometryContext _geometryContext;
+ private Point _currentPoint;
+ private Point? _previousControlPoint;
+ private bool? _isOpen;
+ private bool _isDisposed;
+
+ static PathMarkupParser()
{
- {'0', FillRule.EvenOdd },
- {'1', FillRule.NonZero }
- };
-
- private readonly StreamGeometryContext _context;
+ s_separatorPattern = CreatesSeparatorPattern();
+ }
///
/// Initializes a new instance of the class.
///
- /// The context for the geometry.
- public PathMarkupParser(StreamGeometryContext context)
+ /// The geometry context.
+ /// geometryContext
+ public PathMarkupParser(IGeometryContext geometryContext)
{
- _context = context;
+ if (geometryContext == null)
+ {
+ throw new ArgumentNullException(nameof(geometryContext));
+ }
+
+ _geometryContext = geometryContext;
}
- ///
- /// Defines the command currently being processed.
- ///
private enum Command
{
None,
@@ -62,358 +73,610 @@ namespace Avalonia.Media
SmoothCubicBezierCurve,
SmoothQuadraticBezierCurve,
Arc,
- Close,
+ Close
}
///
- /// Parses the specified markup string.
+ /// Parses the specified path data and writes the result to the geometryContext of this instance.
///
- /// The markup string.
- public void Parse(string s)
+ /// The path data.
+ public void Parse(string pathData)
+ {
+ var normalizedPathData = NormalizeWhiteSpaces(pathData);
+ var tokens = ParseTokens(normalizedPathData);
+
+ CreateGeometry(tokens);
+ }
+
+ void IDisposable.Dispose()
+ {
+ Dispose(true);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ _geometryContext = null;
+ }
+
+ _isDisposed = true;
+ }
+
+ private static string NormalizeWhiteSpaces(string s)
+ {
+ int length = s.Length,
+ index = 0,
+ i = 0;
+ var source = s.ToCharArray();
+ var skip = false;
+
+ for (; i < length; i++)
+ {
+ var c = source[i];
+
+ if (char.IsWhiteSpace(c))
+ {
+ if (skip)
+ {
+ continue;
+ }
+
+ source[index++] = c;
+
+ skip = true;
+
+ continue;
+ }
+
+ skip = false;
+
+ source[index++] = c;
+ }
+
+ if (char.IsWhiteSpace(source[index - 1]))
+ {
+ index--;
+ }
+
+ return char.IsWhiteSpace(source[0]) ? new string(source, 1, index) : new string(source, 0, index);
+ }
+
+ private static string CreatesSeparatorPattern()
{
- bool openFigure = false;
+ var stringBuilder = new StringBuilder();
- using (StringReader reader = new StringReader(s))
+ foreach (var command in s_commands.Keys)
{
- Command command = Command.None;
- Point point = new Point();
- bool relative = false;
- Point? previousControlPoint = null;
+ stringBuilder.Append(command);
+
+ stringBuilder.Append(char.ToLower(command));
+ }
- while (ReadCommand(reader, ref command, ref relative))
+ return @"(?=[" + stringBuilder + "])";
+ }
+
+ private static IEnumerable ParseTokens(string s)
+ {
+ var expressions = Regex.Split(s, s_separatorPattern).Where(t => !string.IsNullOrEmpty(t));
+
+ return expressions.Select(CommandToken.Parse);
+ }
+
+ private static Point MirrorControlPoint(Point controlPoint, Point center)
+ {
+ var dir = controlPoint - center;
+
+ return center + -dir;
+ }
+
+ private void CreateGeometry(IEnumerable commandTokens)
+ {
+ _currentPoint = new Point();
+
+ foreach (var commandToken in commandTokens)
+ {
+ try
{
- switch (command)
+ while (true)
{
- case Command.FillRule:
- _context.SetFillRule(ReadFillRule(reader));
- previousControlPoint = null;
- break;
-
- case Command.Move:
- if (openFigure)
- {
- _context.EndFigure(false);
- }
-
- point = ReadPoint(reader, point, relative);
- _context.BeginFigure(point, true);
- openFigure = true;
- previousControlPoint = null;
- break;
-
- case Command.Line:
- point = ReadPoint(reader, point, relative);
- _context.LineTo(point);
- previousControlPoint = null;
- break;
-
- case Command.HorizontalLine:
- if (!relative)
- {
- point = point.WithX(ReadDouble(reader));
- }
- else
- {
- point = new Point(point.X + ReadDouble(reader), point.Y);
- }
-
- _context.LineTo(point);
- previousControlPoint = null;
- break;
-
- case Command.VerticalLine:
- if (!relative)
- {
- point = point.WithY(ReadDouble(reader));
- }
- else
- {
- point = new Point(point.X, point.Y + ReadDouble(reader));
- }
-
- _context.LineTo(point);
- previousControlPoint = null;
- break;
-
- case Command.QuadraticBezierCurve:
- {
- Point handle = ReadPoint(reader, point, relative);
- previousControlPoint = handle;
- ReadSeparator(reader);
- point = ReadPoint(reader, point, relative);
- _context.QuadraticBezierTo(handle, point);
+ switch (commandToken.Command)
+ {
+ case Command.None:
+ break;
+ case Command.FillRule:
+ SetFillRule(commandToken);
+ break;
+ case Command.Move:
+ AddMove(commandToken);
break;
- }
-
- case Command.SmoothQuadraticBezierCurve:
- {
- Point end = ReadPoint(reader, point, relative);
-
- if(previousControlPoint != null)
- previousControlPoint = MirrorControlPoint((Point)previousControlPoint, point);
-
- _context.QuadraticBezierTo(previousControlPoint ?? point, end);
- point = end;
+ case Command.Line:
+ AddLine(commandToken);
break;
- }
-
- case Command.CubicBezierCurve:
- {
- Point point1 = ReadPoint(reader, point, relative);
- ReadSeparator(reader);
- Point point2 = ReadPoint(reader, point, relative);
- previousControlPoint = point2;
- ReadSeparator(reader);
- point = ReadPoint(reader, point, relative);
- _context.CubicBezierTo(point1, point2, point);
+ case Command.HorizontalLine:
+ AddHorizontalLine(commandToken);
break;
- }
-
- case Command.SmoothCubicBezierCurve:
- {
- Point point2 = ReadPoint(reader, point, relative);
- ReadSeparator(reader);
- Point end = ReadPoint(reader, point, relative);
-
- if(previousControlPoint != null)
- previousControlPoint = MirrorControlPoint((Point)previousControlPoint, point);
-
- _context.CubicBezierTo(previousControlPoint ?? point, point2, end);
- previousControlPoint = point2;
- point = end;
+ case Command.VerticalLine:
+ AddVerticalLine(commandToken);
break;
- }
-
- case Command.Arc:
- {
- Size size = ReadSize(reader);
- ReadSeparator(reader);
- double rotationAngle = ReadDouble(reader);
- ReadSeparator(reader);
- bool isLargeArc = ReadBool(reader);
- ReadSeparator(reader);
- SweepDirection sweepDirection = ReadBool(reader) ? SweepDirection.Clockwise : SweepDirection.CounterClockwise;
- ReadSeparator(reader);
- point = ReadPoint(reader, point, relative);
-
- _context.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection);
- previousControlPoint = null;
+ case Command.CubicBezierCurve:
+ AddCubicBezierCurve(commandToken);
break;
- }
+ case Command.QuadraticBezierCurve:
+ AddQuadraticBezierCurve(commandToken);
+ break;
+ case Command.SmoothCubicBezierCurve:
+ AddSmoothCubicBezierCurve(commandToken);
+ break;
+ case Command.SmoothQuadraticBezierCurve:
+ AddSmoothQuadraticBezierCurve(commandToken);
+ break;
+ case Command.Arc:
+ AddArc(commandToken);
+ break;
+ case Command.Close:
+ CloseFigure();
+ break;
+ default:
+ throw new NotSupportedException("Unsupported command");
+ }
- case Command.Close:
- _context.EndFigure(true);
- openFigure = false;
- previousControlPoint = null;
- break;
+ if (commandToken.HasImplicitCommands)
+ {
+ continue;
+ }
- default:
- throw new NotSupportedException("Unsupported command");
+ break;
}
}
-
- if (openFigure)
+ catch (InvalidDataException)
+ {
+ break;
+ }
+ catch (NotSupportedException)
{
- _context.EndFigure(false);
+ break;
}
}
+
+ if (_isOpen != null)
+ {
+ _geometryContext.EndFigure(false);
+ }
}
- private Point MirrorControlPoint(Point controlPoint, Point center)
+ private void CreateFigure()
{
- Point dir = (controlPoint - center);
- return center + -dir;
+ if (_isOpen != null)
+ {
+ _geometryContext.EndFigure(false);
+ }
+
+ _geometryContext.BeginFigure(_currentPoint);
+
+ _isOpen = true;
+ }
+
+ private void SetFillRule(CommandToken commandToken)
+ {
+ var fillRule = commandToken.ReadFillRule();
+
+ _geometryContext.SetFillRule(fillRule);
+ }
+
+ private void CloseFigure()
+ {
+ if (_isOpen == true)
+ {
+ _geometryContext.EndFigure(true);
+ }
+
+ _previousControlPoint = null;
+
+ _isOpen = null;
}
- private static bool ReadCommand(
- StringReader reader,
- ref Command command,
- ref bool relative)
+ private void AddMove(CommandToken commandToken)
{
- ReadWhitespace(reader);
+ var currentPoint = commandToken.IsRelative
+ ? commandToken.ReadRelativePoint(_currentPoint)
+ : commandToken.ReadPoint();
- int i = reader.Peek();
+ _currentPoint = currentPoint;
- if (i == -1)
+ CreateFigure();
+
+ if (!commandToken.HasImplicitCommands)
{
- return false;
+ return;
}
- else
+
+ while (commandToken.HasImplicitCommands)
{
- char c = (char)i;
- Command next = Command.None;
+ AddLine(commandToken);
- if (!Commands.TryGetValue(char.ToUpperInvariant(c), out next))
+ if (commandToken.IsRelative)
{
- if ((char.IsDigit(c) || c == '.' || c == '+' || c == '-') &&
- (command != Command.None))
- {
- return true;
- }
- else
- {
- throw new InvalidDataException("Unexpected path command '" + c + "'.");
- }
+ continue;
}
- command = next;
- relative = char.IsLower(c);
- reader.Read();
- return true;
+ _currentPoint = currentPoint;
+
+ CreateFigure();
}
}
- private static FillRule ReadFillRule(StringReader reader)
+ private void AddLine(CommandToken commandToken)
{
- int i = reader.Read();
- if (i == -1)
+ _currentPoint = commandToken.IsRelative
+ ? commandToken.ReadRelativePoint(_currentPoint)
+ : commandToken.ReadPoint();
+
+ if (_isOpen == null)
{
- throw new InvalidDataException("Invalid fill rule");
+ CreateFigure();
}
- char c = (char)i;
- FillRule rule;
- if (!FillRules.TryGetValue(c, out rule))
+ _geometryContext.LineTo(_currentPoint);
+ }
+
+ private void AddHorizontalLine(CommandToken commandToken)
+ {
+ _currentPoint = commandToken.IsRelative
+ ? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y)
+ : _currentPoint.WithX(commandToken.ReadDouble());
+
+ if (_isOpen == null)
{
- throw new InvalidDataException("Invalid fill rule");
+ CreateFigure();
}
- return rule;
+ _geometryContext.LineTo(_currentPoint);
}
- private static double ReadDouble(StringReader reader)
+ private void AddVerticalLine(CommandToken commandToken)
{
- ReadWhitespace(reader);
+ _currentPoint = commandToken.IsRelative
+ ? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble())
+ : _currentPoint.WithY(commandToken.ReadDouble());
- // TODO: Handle Infinity, NaN and scientific notation.
- StringBuilder b = new StringBuilder();
- bool readSign = false;
- bool readPoint = false;
- bool readExponent = false;
- int i;
-
- while ((i = reader.Peek()) != -1)
+ if (_isOpen == null)
{
- char c = char.ToUpperInvariant((char)i);
+ CreateFigure();
+ }
- if (((c == '+' || c == '-') && !readSign) ||
- (c == '.' && !readPoint) ||
- (c == 'E' && !readExponent) ||
- char.IsDigit(c))
- {
- if (b.Length != 0 && !readExponent && c == '-')
- break;
-
- b.Append(c);
- reader.Read();
+ _geometryContext.LineTo(_currentPoint);
+ }
- if (!readSign)
- {
- readSign = c == '+' || c == '-';
- }
+ private void AddCubicBezierCurve(CommandToken commandToken)
+ {
+ var point1 = commandToken.IsRelative
+ ? commandToken.ReadRelativePoint(_currentPoint)
+ : commandToken.ReadPoint();
- if (!readPoint)
- {
- readPoint = c == '.';
- }
+ var point2 = commandToken.IsRelative
+ ? commandToken.ReadRelativePoint(_currentPoint)
+ : commandToken.ReadPoint();
- if (c == 'E')
- {
- readSign = false;
- readExponent = true;
- }
- }
- else
- {
- break;
- }
+ _previousControlPoint = point2;
+
+ var point3 = commandToken.IsRelative
+ ? commandToken.ReadRelativePoint(_currentPoint)
+ : commandToken.ReadPoint();
+
+ if (_isOpen == null)
+ {
+ CreateFigure();
}
- return double.Parse(b.ToString(), CultureInfo.InvariantCulture);
+ _geometryContext.CubicBezierTo(point1, point2, point3);
+
+ _currentPoint = point3;
}
- private static Point ReadPoint(StringReader reader, Point current, bool relative)
+ private void AddQuadraticBezierCurve(CommandToken commandToken)
{
- if (!relative)
+ var start = commandToken.IsRelative
+ ? commandToken.ReadRelativePoint(_currentPoint)
+ : commandToken.ReadPoint();
+
+ _previousControlPoint = start;
+
+ var end = commandToken.IsRelative
+ ? commandToken.ReadRelativePoint(_currentPoint)
+ : commandToken.ReadPoint();
+
+ if (_isOpen == null)
{
- current = new Point();
+ CreateFigure();
}
- ReadWhitespace(reader);
- double x = current.X + ReadDouble(reader);
- ReadSeparator(reader);
- double y = current.Y + ReadDouble(reader);
- return new Point(x, y);
+ _geometryContext.QuadraticBezierTo(start, end);
+
+ _currentPoint = end;
}
- private static Size ReadSize(StringReader reader)
+ private void AddSmoothCubicBezierCurve(CommandToken commandToken)
{
- ReadWhitespace(reader);
- double x = ReadDouble(reader);
- ReadSeparator(reader);
- double y = ReadDouble(reader);
- return new Size(x, y);
+ var point2 = commandToken.IsRelative
+ ? commandToken.ReadRelativePoint(_currentPoint)
+ : commandToken.ReadPoint();
+
+ var end = commandToken.IsRelative
+ ? commandToken.ReadRelativePoint(_currentPoint)
+ : commandToken.ReadPoint();
+
+ if (_previousControlPoint != null)
+ {
+ _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint);
+ }
+
+ if (_isOpen == null)
+ {
+ CreateFigure();
+ }
+
+ _geometryContext.CubicBezierTo(_previousControlPoint ?? _currentPoint, point2, end);
+
+ _previousControlPoint = point2;
+
+ _currentPoint = end;
}
- private static bool ReadBool(StringReader reader)
+ private void AddSmoothQuadraticBezierCurve(CommandToken commandToken)
{
- return ReadDouble(reader) != 0;
+ var end = commandToken.IsRelative
+ ? commandToken.ReadRelativePoint(_currentPoint)
+ : commandToken.ReadPoint();
+
+ if (_previousControlPoint != null)
+ {
+ _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint);
+ }
+
+ if (_isOpen == null)
+ {
+ CreateFigure();
+ }
+
+ _geometryContext.QuadraticBezierTo(_previousControlPoint ?? _currentPoint, end);
+
+ _currentPoint = end;
}
- private static Point ReadRelativePoint(StringReader reader, Point lastPoint)
+ private void AddArc(CommandToken commandToken)
{
- ReadWhitespace(reader);
- double x = ReadDouble(reader);
- ReadSeparator(reader);
- double y = ReadDouble(reader);
- return new Point(lastPoint.X + x, lastPoint.Y + y);
+ var size = commandToken.ReadSize();
+
+ var rotationAngle = commandToken.ReadDouble();
+
+ var isLargeArc = commandToken.ReadBool();
+
+ var sweepDirection = commandToken.ReadBool() ? SweepDirection.Clockwise : SweepDirection.CounterClockwise;
+
+ var end = commandToken.IsRelative
+ ? commandToken.ReadRelativePoint(_currentPoint)
+ : commandToken.ReadPoint();
+
+ if (_isOpen == null)
+ {
+ CreateFigure();
+ }
+
+ _geometryContext.ArcTo(end, size, rotationAngle, isLargeArc, sweepDirection);
+
+ _currentPoint = end;
+
+ _previousControlPoint = null;
}
- private static void ReadSeparator(StringReader reader)
+ private class CommandToken
{
- int i;
- bool readComma = false;
+ private const string ArgumentExpression = @"-?[0-9]*\.?\d+";
- while ((i = reader.Peek()) != -1)
+ private CommandToken(Command command, bool isRelative, IEnumerable arguments)
{
- char c = (char)i;
+ Command = command;
- if (char.IsWhiteSpace(c))
+ IsRelative = isRelative;
+
+ Arguments = new List(arguments);
+ }
+
+ public Command Command { get; }
+
+ public bool IsRelative { get; }
+
+ public bool HasImplicitCommands
+ {
+ get
{
- reader.Read();
+ if (CurrentPosition == 0 && Arguments.Count > 0)
+ {
+ return true;
+ }
+
+ return CurrentPosition < Arguments.Count - 1;
}
- else if (c == ',')
+ }
+
+ private int CurrentPosition { get; set; }
+
+ private List Arguments { get; }
+
+ public static CommandToken Parse(string s)
+ {
+ using (var reader = new StringReader(s))
{
- if (readComma)
+ var command = Command.None;
+
+ var isRelative = false;
+
+ if (!ReadCommand(reader, ref command, ref isRelative))
+ {
+ throw new InvalidDataException("No path command declared.");
+ }
+
+ var commandArguments = reader.ReadToEnd();
+
+ var argumentMatches = Regex.Matches(commandArguments, ArgumentExpression);
+
+ var arguments = new List();
+
+ foreach (Match match in argumentMatches)
{
- throw new InvalidDataException("Unexpected ','.");
+ arguments.Add(match.Value);
}
- readComma = true;
- reader.Read();
+ return new CommandToken(command, isRelative, arguments);
}
- else
+ }
+
+ public FillRule ReadFillRule()
+ {
+ if (CurrentPosition == Arguments.Count)
{
- break;
+ throw new InvalidDataException("Invalid fill rule");
+ }
+
+ var value = Arguments[CurrentPosition];
+
+ CurrentPosition++;
+
+ switch (value)
+ {
+ case "0":
+ {
+ return FillRule.EvenOdd;
+ }
+
+ case "1":
+ {
+ return FillRule.NonZero;
+ }
+
+ default:
+ throw new InvalidDataException("Invalid fill rule");
}
}
- }
- private static void ReadWhitespace(StringReader reader)
- {
- int i;
+ public bool ReadBool()
+ {
+ if (CurrentPosition == Arguments.Count)
+ {
+ throw new InvalidDataException("Invalid boolean value");
+ }
+
+ var value = Arguments[CurrentPosition];
+
+ CurrentPosition++;
+
+ switch (value)
+ {
+ case "1":
+ {
+ return true;
+ }
+
+ case "0":
+ {
+ return false;
+ }
+
+ default:
+ throw new InvalidDataException("Invalid boolean value");
+ }
+ }
+
+ public double ReadDouble()
+ {
+ if (CurrentPosition == Arguments.Count)
+ {
+ throw new InvalidDataException("Invalid double value");
+ }
+
+ var value = Arguments[CurrentPosition];
- while ((i = reader.Peek()) != -1)
+ CurrentPosition++;
+
+ return double.Parse(value, CultureInfo.InvariantCulture);
+ }
+
+ public Size ReadSize()
{
- char c = (char)i;
+ var width = ReadDouble();
- if (char.IsWhiteSpace(c))
+ var height = ReadDouble();
+
+ return new Size(width, height);
+ }
+
+ public Point ReadPoint()
+ {
+ var x = ReadDouble();
+
+ var y = ReadDouble();
+
+ return new Point(x, y);
+ }
+
+ public Point ReadRelativePoint(Point origin)
+ {
+ var x = ReadDouble();
+
+ var y = ReadDouble();
+
+ return new Point(origin.X + x, origin.Y + y);
+ }
+
+ private static bool ReadCommand(TextReader reader, ref Command command, ref bool relative)
+ {
+ ReadWhitespace(reader);
+
+ var i = reader.Peek();
+
+ if (i == -1)
{
- reader.Read();
+ return false;
}
- else
+
+ var c = (char)i;
+
+ if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out var next))
{
- break;
+ throw new InvalidDataException("Unexpected path command '" + c + "'.");
+ }
+
+ command = next;
+
+ relative = char.IsLower(c);
+
+ reader.Read();
+
+ return true;
+ }
+
+ private static void ReadWhitespace(TextReader reader)
+ {
+ int i;
+
+ while ((i = reader.Peek()) != -1)
+ {
+ var c = (char)i;
+
+ if (char.IsWhiteSpace(c))
+ {
+ reader.Read();
+ }
+ else
+ {
+ break;
+ }
}
}
}
diff --git a/src/Avalonia.Visuals/Media/StreamGeometry.cs b/src/Avalonia.Visuals/Media/StreamGeometry.cs
index 9848a649aa..d3cb788486 100644
--- a/src/Avalonia.Visuals/Media/StreamGeometry.cs
+++ b/src/Avalonia.Visuals/Media/StreamGeometry.cs
@@ -35,14 +35,15 @@ namespace Avalonia.Media
/// A .
public static new StreamGeometry Parse(string s)
{
- StreamGeometry result = new StreamGeometry();
+ var streamGeometry = new StreamGeometry();
- using (StreamGeometryContext ctx = result.Open())
- {
- PathMarkupParser parser = new PathMarkupParser(ctx);
+ using (var context = streamGeometry.Open())
+ using (var parser = new PathMarkupParser(context))
+ {
parser.Parse(s);
- return result;
}
+
+ return streamGeometry;
}
///
diff --git a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs
index 70b889f817..7521582067 100644
--- a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs
+++ b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs
@@ -15,7 +15,7 @@ namespace Avalonia.Media
/// .
///
/// TODO: This class is just a wrapper around IStreamGeometryContextImpl: is it needed?
- public class StreamGeometryContext : IDisposable
+ public class StreamGeometryContext : IGeometryContext
{
private readonly IStreamGeometryContextImpl _impl;
diff --git a/src/Avalonia.Visuals/Platform/IGeometryContext.cs b/src/Avalonia.Visuals/Platform/IGeometryContext.cs
new file mode 100644
index 0000000000..ac63837428
--- /dev/null
+++ b/src/Avalonia.Visuals/Platform/IGeometryContext.cs
@@ -0,0 +1,66 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using Avalonia.Media;
+
+namespace Avalonia.Platform
+{
+ ///
+ /// Describes a geometry using drawing commands.
+ ///
+ public interface IGeometryContext : IDisposable
+ {
+ ///
+ /// Draws an arc to the specified point.
+ ///
+ /// The destination point.
+ /// The radii of an oval whose perimeter is used to draw the angle.
+ /// The rotation angle of the oval that specifies the curve.
+ /// true to draw the arc greater than 180 degrees; otherwise, false.
+ ///
+ /// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction.
+ ///
+ void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection);
+
+ ///
+ /// Begins a new figure.
+ ///
+ /// The starting point for the figure.
+ /// Whether the figure is filled.
+ void BeginFigure(Point startPoint, bool isFilled = true);
+
+ ///
+ /// Draws a Bezier curve to the specified point.
+ ///
+ /// The first control point used to specify the shape of the curve.
+ /// The second control point used to specify the shape of the curve.
+ /// The destination point for the end of the curve.
+ void CubicBezierTo(Point point1, Point point2, Point point3);
+
+ ///
+ /// Draws a quadratic Bezier curve to the specified point
+ ///
+ /// Control point
+ /// DestinationPoint
+ void QuadraticBezierTo(Point control, Point endPoint);
+
+ ///
+ /// Draws a line to the specified point.
+ ///
+ /// The destination point.
+ void LineTo(Point point);
+
+ ///
+ /// Ends the figure started by .
+ ///
+ /// Whether the figure is closed.
+ void EndFigure(bool isClosed);
+
+ ///
+ /// Sets the fill rule.
+ ///
+ /// The fill rule.
+ void SetFillRule(FillRule fillRule);
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs b/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs
index 386560c6b6..da9505cd2d 100644
--- a/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs
+++ b/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs
@@ -1,62 +1,12 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
-using System;
-using Avalonia.Media;
-
namespace Avalonia.Platform
{
///
/// Describes a geometry using drawing commands.
///
- public interface IStreamGeometryContextImpl : IDisposable
- {
- ///
- /// Draws an arc to the specified point.
- ///
- /// The destination point.
- /// The radii of an oval whose perimeter is used to draw the angle.
- /// The rotation angle of the oval that specifies the curve.
- /// true to draw the arc greater than 180 degrees; otherwise, false.
- ///
- /// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction.
- ///
- void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection);
-
- ///
- /// Begins a new figure.
- ///
- /// The starting point for the figure.
- /// Whether the figure is filled.
- void BeginFigure(Point startPoint, bool isFilled);
-
- ///
- /// Draws a Bezier curve to the specified point.
- ///
- /// The first control point used to specify the shape of the curve.
- /// The second control point used to specify the shape of the curve.
- /// The destination point for the end of the curve.
- void CubicBezierTo(Point point1, Point point2, Point point3);
-
- ///
- /// Draws a quadratic Bezier curve to the specified point
- ///
- /// Control point
- /// DestinationPoint
- void QuadraticBezierTo(Point control, Point endPoint);
-
- ///
- /// Draws a line to the specified point.
- ///
- /// The destination point.
- void LineTo(Point point);
-
- ///
- /// Ends the figure started by .
- ///
- /// Whether the figure is closed.
- void EndFigure(bool isClosed);
-
- void SetFillRule(FillRule fillRule);
+ public interface IStreamGeometryContextImpl : IGeometryContext
+ {
}
}
diff --git a/src/Avalonia.Visuals/Platform/PathGeometryContext.cs b/src/Avalonia.Visuals/Platform/PathGeometryContext.cs
new file mode 100644
index 0000000000..cc881094fd
--- /dev/null
+++ b/src/Avalonia.Visuals/Platform/PathGeometryContext.cs
@@ -0,0 +1,85 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Avalonia.Media;
+using Avalonia.Platform;
+using System;
+
+namespace Avalonia.Visuals.Platform
+{
+ public class PathGeometryContext : IGeometryContext
+ {
+ private PathFigure _currentFigure;
+ private PathGeometry _pathGeometry;
+
+ public PathGeometryContext(PathGeometry pathGeometry)
+ {
+ _pathGeometry = pathGeometry ?? throw new ArgumentNullException(nameof(pathGeometry));
+ }
+
+ public void Dispose()
+ {
+ _pathGeometry = null;
+ }
+
+ public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
+ {
+ var arcSegment = new ArcSegment
+ {
+ Size = size,
+ RotationAngle = rotationAngle,
+ IsLargeArc = isLargeArc,
+ SweepDirection = sweepDirection,
+ Point = point
+ };
+
+ _currentFigure.Segments.Add(arcSegment);
+ }
+
+ public void BeginFigure(Point startPoint, bool isFilled)
+ {
+ _currentFigure = new PathFigure { StartPoint = startPoint, IsClosed = false, IsFilled = isFilled };
+
+ _pathGeometry.Figures.Add(_currentFigure);
+ }
+
+ public void CubicBezierTo(Point point1, Point point2, Point point3)
+ {
+ var bezierSegment = new BezierSegment { Point1 = point1, Point2 = point2, Point3 = point3 };
+
+ _currentFigure.Segments.Add(bezierSegment);
+ }
+
+ public void QuadraticBezierTo(Point control, Point endPoint)
+ {
+ var quadraticBezierSegment = new QuadraticBezierSegment { Point1 = control, Point2 = endPoint };
+
+ _currentFigure.Segments.Add(quadraticBezierSegment);
+ }
+
+ public void LineTo(Point point)
+ {
+ var lineSegment = new LineSegment
+ {
+ Point = point
+ };
+
+ _currentFigure.Segments.Add(lineSegment);
+ }
+
+ public void EndFigure(bool isClosed)
+ {
+ if (_currentFigure != null)
+ {
+ _currentFigure.IsClosed = isClosed;
+ }
+
+ _currentFigure = null;
+ }
+
+ public void SetFillRule(FillRule fillRule)
+ {
+ _pathGeometry.FillRule = fillRule;
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
index fd6b149837..a14923b410 100644
--- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
+++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
@@ -117,6 +117,9 @@ namespace Avalonia.Rendering
var scene = Interlocked.Exchange(ref _scene, null);
scene?.Dispose();
Stop();
+
+ Layers.Clear();
+ RenderTarget?.Dispose();
}
///
diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
index e830d5c313..2118b66de2 100644
--- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
+++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
@@ -133,6 +133,7 @@ namespace Avalonia.Rendering
///
public void Dispose()
{
+ _renderTarget?.Dispose();
}
///
diff --git a/src/Avalonia.Visuals/Rendering/RenderLayers.cs b/src/Avalonia.Visuals/Rendering/RenderLayers.cs
index bafd644603..6a45ecd912 100644
--- a/src/Avalonia.Visuals/Rendering/RenderLayers.cs
+++ b/src/Avalonia.Visuals/Rendering/RenderLayers.cs
@@ -11,11 +11,7 @@ namespace Avalonia.Rendering
{
private List _inner = new List();
private Dictionary _index = new Dictionary();
-
- public RenderLayers()
- {
- }
-
+
public int Count => _inner.Count;
public RenderLayer this[IVisual layerRoot] => _index[layerRoot];
@@ -51,6 +47,16 @@ namespace Avalonia.Rendering
}
}
+ public void Clear()
+ {
+ foreach (var layer in _index.Values)
+ {
+ layer.Bitmap.Dispose();
+ }
+
+ _index.Clear();
+ }
+
public bool TryGetValue(IVisual layerRoot, out RenderLayer value)
{
return _index.TryGetValue(layerRoot, out value);
diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs
index acde49a84a..c2db17cd86 100644
--- a/src/Avalonia.Visuals/Vector.cs
+++ b/src/Avalonia.Visuals/Vector.cs
@@ -3,7 +3,7 @@
using System;
using System.Globalization;
-using System.Xml.Linq;
+using JetBrains.Annotations;
namespace Avalonia
{
@@ -122,6 +122,56 @@ namespace Avalonia
return new Vector(a._x - b._x, a._y - b._y);
}
+ ///
+ /// Check if two vectors are equal (bitwise).
+ ///
+ ///
+ ///
+ public bool Equals(Vector other)
+ {
+ // ReSharper disable CompareOfFloatsByEqualityOperator
+ return _x == other._x && _y == other._y;
+ // ReSharper restore CompareOfFloatsByEqualityOperator
+ }
+
+ ///
+ /// Check if two vectors are nearly equal (numerically).
+ ///
+ /// The other vector.
+ /// True if vectors are nearly equal.
+ [Pure]
+ public bool NearlyEquals(Vector other)
+ {
+ const float tolerance = float.Epsilon;
+
+ return Math.Abs(_x - other._x) < tolerance && Math.Abs(_y - other._y) < tolerance;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+
+ return obj is Vector vector && Equals(vector);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (_x.GetHashCode() * 397) ^ _y.GetHashCode();
+ }
+ }
+
+ public static bool operator ==(Vector left, Vector right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(Vector left, Vector right)
+ {
+ return !left.Equals(right);
+ }
+
///
/// Returns the string representation of the point.
///
diff --git a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs
index db7f29f05b..b528d84e4c 100644
--- a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs
+++ b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs
@@ -164,8 +164,9 @@ namespace Avalonia.MonoMac
var dragOp = DraggingInfo.ConvertDragOperation(sender.DraggingSourceOperationMask);
DraggingInfo info = new DraggingInfo(sender);
+
var pt = TranslateLocalPoint(info.Location);
- var args = new RawDragEvent(dragDevice, type, root, pt, info, dragOp);
+ var args = new RawDragEvent(dragDevice, type, root, pt, info, dragOp, GetModifiers(NSEvent.CurrentModifierFlags));
input(args);
return DraggingInfo.ConvertDragOperation(args.Effects);
}
diff --git a/src/Skia/Avalonia.Skia/BitmapImpl.cs b/src/Skia/Avalonia.Skia/BitmapImpl.cs
deleted file mode 100644
index ccc5a37105..0000000000
--- a/src/Skia/Avalonia.Skia/BitmapImpl.cs
+++ /dev/null
@@ -1,142 +0,0 @@
-using System;
-using System.IO;
-using Avalonia.Platform;
-using Avalonia.Rendering;
-using SkiaSharp;
-
-namespace Avalonia.Skia
-{
- class BitmapImpl : IRenderTargetBitmapImpl, IWriteableBitmapImpl
- {
- private Vector _dpi;
-
- public SKBitmap Bitmap { get; private set; }
-
- public BitmapImpl(SKBitmap bm)
- {
- Bitmap = bm;
- PixelHeight = bm.Height;
- PixelWidth = bm.Width;
- _dpi = new Vector(96, 96);
- }
-
- static void ReleaseProc(IntPtr address, object ctx)
- {
- ((IUnmanagedBlob) ctx).Dispose();
- }
-
- private static readonly SKBitmapReleaseDelegate ReleaseDelegate = ReleaseProc;
-
- public BitmapImpl(int width, int height, Vector dpi, PixelFormat? fmt = null)
- {
- PixelHeight = height;
- PixelWidth = width;
- _dpi = dpi;
- var colorType = fmt?.ToSkColorType() ?? SKImageInfo.PlatformColorType;
- var runtimePlatform = AvaloniaLocator.Current?.GetService();
- var runtime = runtimePlatform?.GetRuntimeInfo();
- if (runtime?.IsDesktop == true && runtime?.OperatingSystem == OperatingSystemType.Linux)
- colorType = SKColorType.Bgra8888;
-
- if (runtimePlatform != null)
- {
- Bitmap = new SKBitmap();
- var nfo = new SKImageInfo(width, height, colorType, SKAlphaType.Premul);
- var plat = AvaloniaLocator.Current.GetService();
- var blob = plat.AllocBlob(nfo.BytesSize);
- Bitmap.InstallPixels(nfo, blob.Address, nfo.RowBytes, null, ReleaseDelegate, blob);
-
- }
- else
- Bitmap = new SKBitmap(width, height, colorType, SKAlphaType.Premul);
- Bitmap.Erase(SKColor.Empty);
- }
-
- public void Dispose()
- {
- Bitmap.Dispose();
- }
-
- public int PixelWidth { get; private set; }
- public int PixelHeight { get; private set; }
-
- class BitmapDrawingContext : DrawingContextImpl
- {
- private readonly SKSurface _surface;
-
- public BitmapDrawingContext(SKBitmap bitmap, Vector dpi, IVisualBrushRenderer visualBrushRenderer)
- : this(CreateSurface(bitmap), dpi, visualBrushRenderer)
- {
- CanUseLcdRendering = false;
- }
-
- private static SKSurface CreateSurface(SKBitmap bitmap)
- {
- IntPtr length;
- var rv = SKSurface.Create(bitmap.Info, bitmap.GetPixels(out length), bitmap.RowBytes);
- if (rv == null)
- throw new Exception("Unable to create Skia surface");
- return rv;
- }
-
- public BitmapDrawingContext(SKSurface surface, Vector dpi, IVisualBrushRenderer visualBrushRenderer)
- : base(surface.Canvas, dpi, visualBrushRenderer)
- {
- _surface = surface;
- }
-
- public override void Dispose()
- {
- base.Dispose();
- _surface.Dispose();
- }
- }
-
- public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
- {
- return new BitmapDrawingContext(Bitmap, _dpi, visualBrushRenderer);
- }
-
- public void Save(Stream stream)
- {
- IntPtr length;
- using (var image = SKImage.FromPixels(Bitmap.Info, Bitmap.GetPixels(out length), Bitmap.RowBytes))
- using (var data = image.Encode())
- {
- data.SaveTo(stream);
- }
- }
-
- public void Save(string fileName)
- {
- using (var stream = File.Create(fileName))
- Save(stream);
- }
-
- class BitmapFramebuffer : ILockedFramebuffer
- {
- private SKBitmap _bmp;
-
- public BitmapFramebuffer(SKBitmap bmp)
- {
- _bmp = bmp;
- _bmp.LockPixels();
- }
-
- public void Dispose()
- {
- _bmp.UnlockPixels();
- _bmp = null;
- }
-
- public IntPtr Address => _bmp.GetPixels();
- public int Width => _bmp.Width;
- public int Height => _bmp.Height;
- public int RowBytes => _bmp.RowBytes;
- public Vector Dpi { get; } = new Vector(96, 96);
- public PixelFormat Format => _bmp.ColorType.ToPixelFormat();
- }
-
- public ILockedFramebuffer Lock() => new BitmapFramebuffer(Bitmap);
- }
-}
diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
index 22e5652cfb..b7ce6eedc4 100644
--- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
+++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
@@ -1,57 +1,114 @@
-using Avalonia.Media;
-using SkiaSharp;
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
+using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Utilities;
using Avalonia.Utilities;
+using SkiaSharp;
namespace Avalonia.Skia
{
- internal class DrawingContextImpl : IDrawingContextImpl
+ ///
+ /// Skia based drawing context.
+ ///
+ public class DrawingContextImpl : IDrawingContextImpl
{
+ private readonly IDisposable[] _disposables;
private readonly Vector _dpi;
+ private readonly Stack _maskStack = new Stack();
+ private readonly Stack _opacityStack = new Stack();
private readonly Matrix? _postTransform;
- private readonly IDisposable[] _disposables;
private readonly IVisualBrushRenderer _visualBrushRenderer;
- private Stack maskStack = new Stack();
- protected bool CanUseLcdRendering = true;
- public SKCanvas Canvas { get; private set; }
-
- public DrawingContextImpl(
- SKCanvas canvas,
- Vector dpi,
- IVisualBrushRenderer visualBrushRenderer,
- params IDisposable[] disposables)
+ private double _currentOpacity = 1.0f;
+ private readonly bool _canTextUseLcdRendering;
+ private Matrix _currentTransform;
+
+ ///
+ /// Context create info.
+ ///
+ public struct CreateInfo
+ {
+ ///
+ /// Canvas to draw to.
+ ///
+ public SKCanvas Canvas;
+
+ ///
+ /// Dpi of drawings.
+ ///
+ public Vector Dpi;
+
+ ///
+ /// Visual brush renderer.
+ ///
+ public IVisualBrushRenderer VisualBrushRenderer;
+
+ ///
+ /// Render text without Lcd rendering.
+ ///
+ public bool DisableTextLcdRendering;
+ }
+
+ ///
+ /// Create new drawing context.
+ ///
+ /// Create info.
+ /// Array of elements to dispose after drawing has finished.
+ public DrawingContextImpl(CreateInfo createInfo, params IDisposable[] disposables)
{
- _dpi = dpi;
- if (dpi.X != 96 || dpi.Y != 96)
- _postTransform = Matrix.CreateScale(dpi.X / 96, dpi.Y / 96);
- _visualBrushRenderer = visualBrushRenderer;
+ _dpi = createInfo.Dpi;
+ _visualBrushRenderer = createInfo.VisualBrushRenderer;
_disposables = disposables;
- Canvas = canvas;
+ _canTextUseLcdRendering = !createInfo.DisableTextLcdRendering;
+
+ Canvas = createInfo.Canvas;
+
+ if (Canvas == null)
+ {
+ throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo));
+ }
+
+ if (!_dpi.NearlyEquals(SkiaPlatform.DefaultDpi))
+ {
+ _postTransform =
+ Matrix.CreateScale(_dpi.X / SkiaPlatform.DefaultDpi.X, _dpi.Y / SkiaPlatform.DefaultDpi.Y);
+ }
+
Transform = Matrix.Identity;
}
+
+ ///
+ /// Skia canvas.
+ ///
+ public SKCanvas Canvas { get; }
+ ///
public void Clear(Color color)
{
Canvas.Clear(color.ToSKColor());
}
+ ///
public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect)
{
- var impl = (BitmapImpl)source.Item;
+ var drawableImage = (IDrawableBitmapImpl) source.Item;
var s = sourceRect.ToSKRect();
var d = destRect.ToSKRect();
- using (var paint = new SKPaint()
- { Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity)) })
+
+ using (var paint =
+ new SKPaint {Color = new SKColor(255, 255, 255, (byte) (255 * opacity * _currentOpacity))})
{
- Canvas.DrawBitmap(impl.Bitmap, s, d, paint);
+ drawableImage.Draw(this, s, d, paint);
}
}
+ ///
public void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
{
PushOpacityMask(opacityMask, opacityMaskRect);
@@ -59,17 +116,19 @@ namespace Avalonia.Skia
PopOpacityMask();
}
+ ///
public void DrawLine(Pen pen, Point p1, Point p2)
{
using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
{
- Canvas.DrawLine((float)p1.X, (float)p1.Y, (float)p2.X, (float)p2.Y, paint.Paint);
+ Canvas.DrawLine((float) p1.X, (float) p1.Y, (float) p2.X, (float) p2.Y, paint.Paint);
}
}
+ ///
public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
{
- var impl = (GeometryImpl)geometry;
+ var impl = (GeometryImpl) geometry;
var size = geometry.Bounds.Size;
using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper))
@@ -79,6 +138,7 @@ namespace Avalonia.Skia
{
Canvas.DrawPath(impl.EffectivePath, fill.Paint);
}
+
if (stroke.Paint != null)
{
Canvas.DrawPath(impl.EffectivePath, stroke.Paint);
@@ -86,227 +146,424 @@ namespace Avalonia.Skia
}
}
- private struct PaintState : IDisposable
+ ///
+ public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0)
{
- private readonly SKColor _color;
- private readonly SKShader _shader;
- private readonly SKPaint _paint;
+ using (var paint = CreatePaint(pen, rect.Size))
+ {
+ var rc = rect.ToSKRect();
- public PaintState(SKPaint paint, SKColor color, SKShader shader)
+ if (Math.Abs(cornerRadius) < float.Epsilon)
+ {
+ Canvas.DrawRect(rc, paint.Paint);
+ }
+ else
+ {
+ Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint);
+ }
+ }
+ }
+
+ ///
+ public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0)
+ {
+ using (var paint = CreatePaint(brush, rect.Size))
{
- _paint = paint;
- _color = color;
- _shader = shader;
+ var rc = rect.ToSKRect();
+
+ if (Math.Abs(cornerRadius) < float.Epsilon)
+ {
+ Canvas.DrawRect(rc, paint.Paint);
+ }
+ else
+ {
+ Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint);
+ }
}
+ }
- public void Dispose()
+ ///
+ public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
+ {
+ using (var paint = CreatePaint(foreground, text.Size))
{
- _paint.Color = _color;
- _paint.Shader = _shader;
+ var textImpl = (FormattedTextImpl) text;
+ textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering);
}
}
- internal struct PaintWrapper : IDisposable
+ ///
+ public IRenderTargetBitmapImpl CreateLayer(Size size)
{
- //We are saving memory allocations there
- //TODO: add more disposable fields if needed
- public readonly SKPaint Paint;
+ var normalizedDpi = new Vector(_dpi.X / SkiaPlatform.DefaultDpi.X, _dpi.Y / SkiaPlatform.DefaultDpi.Y);
+ var pixelSize = size * normalizedDpi;
- private IDisposable _disposable1;
- private IDisposable _disposable2;
+ return CreateRenderTarget((int) pixelSize.Width, (int) pixelSize.Height, _dpi);
+ }
- public IDisposable ApplyTo(SKPaint paint)
- {
- var state = new PaintState(paint, paint.Color, paint.Shader);
+ ///
+ public void PushClip(Rect clip)
+ {
+ Canvas.Save();
+ Canvas.ClipRect(clip.ToSKRect());
+ }
- paint.Color = Paint.Color;
- paint.Shader = Paint.Shader;
+ ///
+ public void PopClip()
+ {
+ Canvas.Restore();
+ }
- return state;
- }
+ ///
+ public void PushOpacity(double opacity)
+ {
+ _opacityStack.Push(_currentOpacity);
+ _currentOpacity *= opacity;
+ }
- public void AddDisposable(IDisposable disposable)
- {
- if (_disposable1 == null)
- _disposable1 = disposable;
- else if (_disposable2 == null)
- _disposable2 = disposable;
- else
- throw new InvalidOperationException();
- }
+ ///
+ public void PopOpacity()
+ {
+ _currentOpacity = _opacityStack.Pop();
+ }
- public PaintWrapper(SKPaint paint)
+ ///
+ public virtual void Dispose()
+ {
+ if (_disposables == null)
{
- Paint = paint;
- _disposable1 = null;
- _disposable2 = null;
+ return;
}
- public void Dispose()
+ foreach (var disposable in _disposables)
{
- Paint?.Dispose();
- _disposable1?.Dispose();
- _disposable2?.Dispose();
+ disposable?.Dispose();
}
}
- internal PaintWrapper CreatePaint(IBrush brush, Size targetSize)
+ ///
+ public void PushGeometryClip(IGeometryImpl clip)
{
- SKPaint paint = new SKPaint();
- var rv = new PaintWrapper(paint);
- paint.IsStroke = false;
+ Canvas.Save();
+ Canvas.ClipPath(((GeometryImpl)clip).EffectivePath);
+ }
-
- double opacity = brush.Opacity * _currentOpacity;
- paint.IsAntialias = true;
+ ///
+ public void PopGeometryClip()
+ {
+ Canvas.Restore();
+ }
+
+ ///
+ public void PushOpacityMask(IBrush mask, Rect bounds)
+ {
+ // TODO: This should be disposed
+ var paint = new SKPaint();
+
+ Canvas.SaveLayer(paint);
+ _maskStack.Push(CreatePaint(mask, bounds.Size));
+ }
- var solid = brush as ISolidColorBrush;
- if (solid != null)
+ ///
+ public void PopOpacityMask()
+ {
+ using (var paint = new SKPaint { BlendMode = SKBlendMode.DstIn })
{
- paint.Color = new SKColor(solid.Color.R, solid.Color.G, solid.Color.B, (byte) (solid.Color.A * opacity));
- return rv;
+ Canvas.SaveLayer(paint);
+ using (var paintWrapper = _maskStack.Pop())
+ {
+ Canvas.DrawPaint(paintWrapper.Paint);
+ }
+ Canvas.Restore();
}
- paint.Color = (new SKColor(255, 255, 255, (byte)(255 * opacity)));
- var gradient = brush as IGradientBrush;
- if (gradient != null)
+ Canvas.Restore();
+ }
+
+ ///
+ public Matrix Transform
+ {
+ get { return _currentTransform; }
+ set
{
- var tileMode = gradient.SpreadMethod.ToSKShaderTileMode();
- var stopColors = gradient.GradientStops.Select(s => s.Color.ToSKColor()).ToArray();
- var stopOffsets = gradient.GradientStops.Select(s => (float)s.Offset).ToArray();
+ if (_currentTransform == value)
+ return;
+
+ _currentTransform = value;
+
+ var transform = value;
- var linearGradient = brush as ILinearGradientBrush;
- if (linearGradient != null)
+ if (_postTransform.HasValue)
+ {
+ transform *= _postTransform.Value;
+ }
+
+ Canvas.SetMatrix(transform.ToSKMatrix());
+ }
+ }
+
+ ///
+ /// Configure paint wrapper for using gradient brush.
+ ///
+ /// Paint wrapper.
+ /// Target size.
+ /// Gradient brush.
+ private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Size targetSize, IGradientBrush gradientBrush)
+ {
+ var tileMode = gradientBrush.SpreadMethod.ToSKShaderTileMode();
+ var stopColors = gradientBrush.GradientStops.Select(s => s.Color.ToSKColor()).ToArray();
+ var stopOffsets = gradientBrush.GradientStops.Select(s => (float)s.Offset).ToArray();
+
+ switch (gradientBrush)
+ {
+ case ILinearGradientBrush linearGradient:
{
var start = linearGradient.StartPoint.ToPixels(targetSize).ToSKPoint();
var end = linearGradient.EndPoint.ToPixels(targetSize).ToSKPoint();
// would be nice to cache these shaders possibly?
- using (var shader = SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode))
- paint.Shader = shader;
+ using (var shader =
+ SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode))
+ {
+ paintWrapper.Paint.Shader = shader;
+ }
+ break;
}
- else
+ case IRadialGradientBrush radialGradient:
{
- var radialGradient = brush as IRadialGradientBrush;
- if (radialGradient != null)
- {
- var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint();
- var radius = (float)radialGradient.Radius;
+ var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint();
+ var radius = (float)(radialGradient.Radius * targetSize.Width);
- // TODO: There is no SetAlpha in SkiaSharp
- //paint.setAlpha(128);
-
- // would be nice to cache these shaders possibly?
- using (var shader = SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode))
- paint.Shader = shader;
+ // TODO: There is no SetAlpha in SkiaSharp
+ //paint.setAlpha(128);
+ // would be nice to cache these shaders possibly?
+ using (var shader =
+ SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode))
+ {
+ paintWrapper.Paint.Shader = shader;
}
+
+ break;
}
+ }
+ }
+
+ ///
+ /// Configure paint wrapper for using tile brush.
+ ///
+ /// Paint wrapper.
+ /// Target size.
+ /// Tile brush to use.
+ /// Tile brush image.
+ private void ConfigureTileBrush(ref PaintWrapper paintWrapper, Size targetSize, ITileBrush tileBrush, IDrawableBitmapImpl tileBrushImage)
+ {
+ var calc = new TileBrushCalculator(tileBrush,
+ new Size(tileBrushImage.PixelWidth, tileBrushImage.PixelHeight), targetSize);
+
+ var intermediate = CreateRenderTarget(
+ (int)calc.IntermediateSize.Width,
+ (int)calc.IntermediateSize.Height, _dpi);
+
+ paintWrapper.AddDisposable(intermediate);
+
+ using (var context = intermediate.CreateDrawingContext(null))
+ {
+ var rect = new Rect(0, 0, tileBrushImage.PixelWidth, tileBrushImage.PixelHeight);
- return rv;
+ context.Clear(Colors.Transparent);
+ context.PushClip(calc.IntermediateClip);
+ context.Transform = calc.IntermediateTransform;
+ context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect);
+ context.PopClip();
}
- var tileBrush = brush as ITileBrush;
- var visualBrush = brush as IVisualBrush;
- var tileBrushImage = default(BitmapImpl);
+ var tileTransform =
+ tileBrush.TileMode != TileMode.None
+ ? SKMatrix.MakeTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y)
+ : SKMatrix.MakeIdentity();
- if (visualBrush != null)
+ SKShaderTileMode tileX =
+ tileBrush.TileMode == TileMode.None
+ ? SKShaderTileMode.Clamp
+ : tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY
+ ? SKShaderTileMode.Mirror
+ : SKShaderTileMode.Repeat;
+
+ SKShaderTileMode tileY =
+ tileBrush.TileMode == TileMode.None
+ ? SKShaderTileMode.Clamp
+ : tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY
+ ? SKShaderTileMode.Mirror
+ : SKShaderTileMode.Repeat;
+
+
+ var image = intermediate.SnapshotImage();
+ paintWrapper.AddDisposable(image);
+
+ using (var shader = image.ToShader(tileX, tileY, tileTransform))
{
- if (_visualBrushRenderer != null)
- {
- var intermediateSize = _visualBrushRenderer.GetRenderTargetSize(visualBrush);
+ paintWrapper.Paint.Shader = shader;
+ }
+ }
- if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1)
- {
- var intermediate = new BitmapImpl((int)intermediateSize.Width, (int)intermediateSize.Height, _dpi);
+ ///
+ /// Configure paint wrapper to use visual brush.
+ ///
+ /// Paint wrapper.
+ /// Visual brush.
+ /// Visual brush renderer.
+ /// Tile brush image.
+ private void ConfigureVisualBrush(ref PaintWrapper paintWrapper, IVisualBrush visualBrush, IVisualBrushRenderer visualBrushRenderer, ref IDrawableBitmapImpl tileBrushImage)
+ {
+ if (_visualBrushRenderer == null)
+ {
+ throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl.");
+ }
- using (var ctx = intermediate.CreateDrawingContext(_visualBrushRenderer))
- {
- ctx.Clear(Colors.Transparent);
- _visualBrushRenderer.RenderVisualBrush(ctx, visualBrush);
- }
+ var intermediateSize = visualBrushRenderer.GetRenderTargetSize(visualBrush);
- tileBrushImage = intermediate;
- rv.AddDisposable(tileBrushImage);
- }
- }
- else
+ if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1)
+ {
+ var intermediate = CreateRenderTarget((int)intermediateSize.Width, (int)intermediateSize.Height, _dpi);
+
+ using (var ctx = intermediate.CreateDrawingContext(visualBrushRenderer))
{
- throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl.");
+ ctx.Clear(Colors.Transparent);
+
+ visualBrushRenderer.RenderVisualBrush(ctx, visualBrush);
}
+
+ tileBrushImage = intermediate;
+ paintWrapper.AddDisposable(tileBrushImage);
}
- else
+ }
+
+ ///
+ /// Creates paint wrapper for given brush.
+ ///
+ /// Source brush.
+ /// Target size.
+ /// Paint wrapper for given brush.
+ internal PaintWrapper CreatePaint(IBrush brush, Size targetSize)
+ {
+ var paint = new SKPaint
+ {
+ IsStroke = false,
+ IsAntialias = true
+ };
+
+ var paintWrapper = new PaintWrapper(paint);
+
+ double opacity = brush.Opacity * _currentOpacity;
+
+ if (brush is ISolidColorBrush solid)
{
- tileBrushImage = (BitmapImpl)((tileBrush as IImageBrush)?.Source?.PlatformImpl.Item);
+ paint.Color = new SKColor(solid.Color.R, solid.Color.G, solid.Color.B, (byte) (solid.Color.A * opacity));
+
+ return paintWrapper;
}
- if (tileBrush != null && tileBrushImage != null)
+ paint.Color = new SKColor(255, 255, 255, (byte) (255 * opacity));
+
+ if (brush is IGradientBrush gradient)
{
- var calc = new TileBrushCalculator(tileBrush, new Size(tileBrushImage.PixelWidth, tileBrushImage.PixelHeight), targetSize);
- var bitmap = new BitmapImpl((int)calc.IntermediateSize.Width, (int)calc.IntermediateSize.Height, _dpi);
- rv.AddDisposable(bitmap);
- using (var context = bitmap.CreateDrawingContext(null))
- {
- var rect = new Rect(0, 0, tileBrushImage.PixelWidth, tileBrushImage.PixelHeight);
+ ConfigureGradientBrush(ref paintWrapper, targetSize, gradient);
- context.Clear(Colors.Transparent);
- context.PushClip(calc.IntermediateClip);
- context.Transform = calc.IntermediateTransform;
- context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect);
- context.PopClip();
- }
+ return paintWrapper;
+ }
+
+ var tileBrush = brush as ITileBrush;
+ var visualBrush = brush as IVisualBrush;
+ var tileBrushImage = default(IDrawableBitmapImpl);
- SKMatrix translation = SKMatrix.MakeTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y);
- SKShaderTileMode tileX =
- tileBrush.TileMode == TileMode.None
- ? SKShaderTileMode.Clamp
- : tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY
- ? SKShaderTileMode.Mirror
- : SKShaderTileMode.Repeat;
-
- SKShaderTileMode tileY =
- tileBrush.TileMode == TileMode.None
- ? SKShaderTileMode.Clamp
- : tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY
- ? SKShaderTileMode.Mirror
- : SKShaderTileMode.Repeat;
- using (var shader = SKShader.CreateBitmap(bitmap.Bitmap, tileX, tileY, translation))
- paint.Shader = shader;
+ if (visualBrush != null)
+ {
+ ConfigureVisualBrush(ref paintWrapper, visualBrush, _visualBrushRenderer, ref tileBrushImage);
+ }
+ else
+ {
+ tileBrushImage = (IDrawableBitmapImpl) (tileBrush as IImageBrush)?.Source?.PlatformImpl.Item;
}
- return rv;
+ if (tileBrush != null && tileBrushImage != null)
+ {
+ ConfigureTileBrush(ref paintWrapper, targetSize, tileBrush, tileBrushImage);
+ }
+
+ return paintWrapper;
}
+ ///
+ /// Creates paint wrapper for given pen.
+ ///
+ /// Source pen.
+ /// Target size.
+ ///
private PaintWrapper CreatePaint(Pen pen, Size targetSize)
{
var rv = CreatePaint(pen.Brush, targetSize);
var paint = rv.Paint;
paint.IsStroke = true;
- paint.StrokeWidth = (float)pen.Thickness;
+ paint.StrokeWidth = (float) pen.Thickness;
- if (pen.StartLineCap == PenLineCap.Round)
- paint.StrokeCap = SKStrokeCap.Round;
- else if (pen.StartLineCap == PenLineCap.Square)
- paint.StrokeCap = SKStrokeCap.Square;
- else
- paint.StrokeCap = SKStrokeCap.Butt;
+ // Need to modify dashes due to Skia modifying their lengths
+ // https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/paths/dots
+ // TODO: Still something is off, dashes are now present, but don't look the same as D2D ones.
+ float dashLengthModifier;
+ float gapLengthModifier;
- if (pen.LineJoin == PenLineJoin.Miter)
- paint.StrokeJoin = SKStrokeJoin.Miter;
- else if (pen.LineJoin == PenLineJoin.Round)
- paint.StrokeJoin = SKStrokeJoin.Round;
- else
- paint.StrokeJoin = SKStrokeJoin.Bevel;
+ switch (pen.StartLineCap)
+ {
+ case PenLineCap.Round:
+ paint.StrokeCap = SKStrokeCap.Round;
+ dashLengthModifier = -paint.StrokeWidth;
+ gapLengthModifier = paint.StrokeWidth;
+ break;
+ case PenLineCap.Square:
+ paint.StrokeCap = SKStrokeCap.Square;
+ dashLengthModifier = -paint.StrokeWidth;
+ gapLengthModifier = paint.StrokeWidth;
+ break;
+ default:
+ paint.StrokeCap = SKStrokeCap.Butt;
+ dashLengthModifier = 0.0f;
+ gapLengthModifier = 0.0f;
+ break;
+ }
+
+ switch (pen.LineJoin)
+ {
+ case PenLineJoin.Miter:
+ paint.StrokeJoin = SKStrokeJoin.Miter;
+ break;
+ case PenLineJoin.Round:
+ paint.StrokeJoin = SKStrokeJoin.Round;
+ break;
+ default:
+ paint.StrokeJoin = SKStrokeJoin.Bevel;
+ break;
+ }
- paint.StrokeMiter = (float)pen.MiterLimit;
+ paint.StrokeMiter = (float) pen.MiterLimit;
if (pen.DashStyle?.Dashes != null && pen.DashStyle.Dashes.Count > 0)
{
- var pe = SKPathEffect.CreateDash(
- pen.DashStyle?.Dashes.Select(x => (float)x).ToArray(),
- (float)pen.DashStyle.Offset);
+ var srcDashes = pen.DashStyle.Dashes;
+ var dashesArray = new float[srcDashes.Count];
+
+ for (var i = 0; i < srcDashes.Count; ++i)
+ {
+ var lengthModifier = i % 2 == 0 ? dashLengthModifier : gapLengthModifier;
+
+ // Avalonia dash lengths are relative, but Skia takes absolute sizes - need to scale
+ dashesArray[i] = (float) srcDashes[i] * paint.StrokeWidth + lengthModifier;
+ }
+
+ var pe = SKPathEffect.CreateDash(dashesArray, (float) pen.DashStyle.Offset);
+
paint.PathEffect = pe;
rv.AddDisposable(pe);
}
@@ -314,128 +571,118 @@ namespace Avalonia.Skia
return rv;
}
- public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0)
+ ///
+ /// Create new render target compatible with this drawing context.
+ ///
+ /// Width.
+ /// Height.
+ /// Drawing dpi.
+ /// Pixel format.
+ ///
+ private SurfaceRenderTarget CreateRenderTarget(int width, int height, Vector dpi, PixelFormat? format = null)
{
- using (var paint = CreatePaint(pen, rect.Size))
+ var createInfo = new SurfaceRenderTarget.CreateInfo
{
- var rc = rect.ToSKRect();
- if (cornerRadius == 0)
- {
- Canvas.DrawRect(rc, paint.Paint);
- }
- else
- {
- Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint);
- }
- }
+ Width = width,
+ Height = height,
+ Dpi = dpi,
+ Format = format,
+ DisableTextLcdRendering = !_canTextUseLcdRendering
+ };
+
+ return new SurfaceRenderTarget(createInfo);
}
- public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0)
+ ///
+ /// Skia cached paint state.
+ ///
+ private struct PaintState : IDisposable
{
- using (var paint = CreatePaint(brush, rect.Size))
+ private readonly SKColor _color;
+ private readonly SKShader _shader;
+ private readonly SKPaint _paint;
+
+ public PaintState(SKPaint paint, SKColor color, SKShader shader)
{
- var rc = rect.ToSKRect();
- if (cornerRadius == 0)
- {
- Canvas.DrawRect(rc, paint.Paint);
- }
- else
- {
- Canvas.DrawRoundRect(rc, cornerRadius, cornerRadius, paint.Paint);
- }
+ _paint = paint;
+ _color = color;
+ _shader = shader;
}
- }
- public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
- {
- using (var paint = CreatePaint(foreground, text.Size))
+ ///
+ public void Dispose()
{
- var textImpl = (FormattedTextImpl)text;
- textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, CanUseLcdRendering);
+ _paint.Color = _color;
+ _paint.Shader = _shader;
}
}
- public IRenderTargetBitmapImpl CreateLayer(Size size)
- {
- var pixelSize = size * (_dpi / 96);
- return new BitmapImpl((int)pixelSize.Width, (int)pixelSize.Height, _dpi);
- }
-
- public void PushClip(Rect clip)
- {
- Canvas.Save();
- Canvas.ClipRect(clip.ToSKRect());
- }
-
- public void PopClip()
- {
- Canvas.Restore();
- }
-
- private double _currentOpacity = 1.0f;
- private readonly Stack _opacityStack = new Stack();
-
- public void PushOpacity(double opacity)
+ ///
+ /// Skia paint wrapper.
+ ///
+ internal struct PaintWrapper : IDisposable
{
- _opacityStack.Push(_currentOpacity);
- _currentOpacity *= opacity;
- }
+ //We are saving memory allocations there
+ public readonly SKPaint Paint;
- public void PopOpacity()
- {
- _currentOpacity = _opacityStack.Pop();
- }
+ private IDisposable _disposable1;
+ private IDisposable _disposable2;
+ private IDisposable _disposable3;
- public virtual void Dispose()
- {
- if(_disposables!=null)
- foreach (var disposable in _disposables)
- disposable?.Dispose();
- }
+ public PaintWrapper(SKPaint paint)
+ {
+ Paint = paint;
- public void PushGeometryClip(IGeometryImpl clip)
- {
- Canvas.Save();
- Canvas.ClipPath(((StreamGeometryImpl)clip).EffectivePath);
- }
+ _disposable1 = null;
+ _disposable2 = null;
+ _disposable3 = null;
+ }
- public void PopGeometryClip()
- {
- Canvas.Restore();
- }
+ public IDisposable ApplyTo(SKPaint paint)
+ {
+ var state = new PaintState(paint, paint.Color, paint.Shader);
- public void PushOpacityMask(IBrush mask, Rect bounds)
- {
- Canvas.SaveLayer(new SKPaint());
- maskStack.Push(CreatePaint(mask, bounds.Size));
- }
+ paint.Color = Paint.Color;
+ paint.Shader = Paint.Shader;
- public void PopOpacityMask()
- {
- Canvas.SaveLayer(new SKPaint { BlendMode = SKBlendMode.DstIn });
- using (var paintWrapper = maskStack.Pop())
- {
- Canvas.DrawPaint(paintWrapper.Paint);
+ return state;
}
- Canvas.Restore();
- Canvas.Restore();
- }
- private Matrix _currentTransform;
-
- public Matrix Transform
- {
- get { return _currentTransform; }
- set
+ ///
+ /// Add new disposable to a wrapper.
+ ///
+ /// Disposable to add.
+ public void AddDisposable(IDisposable disposable)
{
- if (_currentTransform == value)
- return;
+ if (_disposable1 == null)
+ {
+ _disposable1 = disposable;
+ }
+ else if (_disposable2 == null)
+ {
+ _disposable2 = disposable;
+ }
+ else if (_disposable3 == null)
+ {
+ _disposable3 = disposable;
+ }
+ else
+ {
+ Debug.Assert(false);
- _currentTransform = value;
- var transform = value;
- if (_postTransform.HasValue)
- transform *= _postTransform.Value;
- Canvas.SetMatrix(transform.ToSKMatrix());
+ // ReSharper disable once HeuristicUnreachableCode
+ throw new InvalidOperationException(
+ "PaintWrapper disposable object limit reached. You need to add extra struct fields to support more disposables.");
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ Paint?.Dispose();
+ _disposable1?.Dispose();
+ _disposable2?.Dispose();
+ _disposable3?.Dispose();
}
}
}
diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
index 13dcd9669d..d835c83aa6 100644
--- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
+++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
@@ -10,6 +10,9 @@ using System.Linq;
namespace Avalonia.Skia
{
+ ///
+ /// Skia formatted text implementation.
+ ///
public class FormattedTextImpl : IFormattedTextImpl
{
public FormattedTextImpl(
@@ -21,7 +24,7 @@ namespace Avalonia.Skia
IReadOnlyList spans)
{
Text = text ?? string.Empty;
-
+
// Replace 0 characters with zero-width spaces (200B)
Text = Text.Replace((char)0, (char)0x200B);
@@ -250,7 +253,26 @@ namespace Avalonia.Skia
{
float currX = x;
string subStr;
+ float measure;
int len;
+ float factor;
+ switch (paint.TextAlign)
+ {
+ case SKTextAlign.Left:
+ factor = 0;
+ break;
+ case SKTextAlign.Center:
+ factor = 0.5f;
+ break;
+ case SKTextAlign.Right:
+ factor = 1;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+
+ var textLine = Text.Substring(line.Start, line.Length);
+ currX -= paint.MeasureText(textLine) * factor;
for (int i = line.Start; i < line.Start + line.Length;)
{
@@ -268,13 +290,15 @@ namespace Avalonia.Skia
}
subStr = Text.Substring(i, len);
+ measure = paint.MeasureText(subStr);
+ currX += measure * factor;
ApplyWrapperTo(ref currentPaint, currentWrapper, ref currd, paint, canUseLcdRendering);
canvas.DrawText(subStr, currX, origin.Y + line.Top + _lineOffset, paint);
i += len;
- currX += paint.MeasureText(subStr);
+ currX += measure * (1 - factor);
}
}
}
@@ -331,7 +355,7 @@ namespace Avalonia.Skia
{
float measuredWidth;
string subText = textInput.Substring(textIndex, stop - textIndex);
- lengthBreak = (int)paint.BreakText(subText, maxWidth, out measuredWidth) / 2;
+ lengthBreak = (int)paint.BreakText(subText, maxWidth, out measuredWidth);
}
//Check for white space or line breakers before the lengthBreak
@@ -430,7 +454,6 @@ namespace Avalonia.Skia
private void BuildRects()
{
// Build character rects
- var fm = _paint.FontMetrics;
SKTextAlign align = _paint.TextAlign;
for (int li = 0; li < _skiaLines.Count; li++)
@@ -538,18 +561,16 @@ namespace Avalonia.Skia
string subString;
- float widthConstraint = (_constraint.Width != double.PositiveInfinity)
- ? (float)_constraint.Width
- : -1;
-
- for (int c = 0; curOff < length; c++)
+ float widthConstraint = double.IsPositiveInfinity(_constraint.Width)
+ ? -1
+ : (float)_constraint.Width;
+
+ while(curOff < length)
{
float lineWidth = -1;
int measured;
int trailingnumber = 0;
-
- subString = Text.Substring(curOff);
-
+
float constraint = -1;
if (_wrapping == TextWrapping.Wrap)
@@ -561,8 +582,8 @@ namespace Avalonia.Skia
measured = LineBreak(Text, curOff, length, _paint, constraint, out trailingnumber);
AvaloniaFormattedTextLine line = new AvaloniaFormattedTextLine();
- line.TextLength = measured;
line.Start = curOff;
+ line.TextLength = measured;
subString = Text.Substring(line.Start, line.TextLength);
lineWidth = _paint.MeasureText(subString);
line.Length = measured - trailingnumber;
diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs
index 1956f02d1b..99dbbefd4d 100644
--- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs
+++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs
@@ -1,82 +1,199 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Reactive.Disposables;
using Avalonia.Controls.Platform.Surfaces;
-using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
+using Avalonia.Skia.Helpers;
using SkiaSharp;
namespace Avalonia.Skia
{
+ ///
+ /// Skia render target that renders to a framebuffer surface. No gpu acceleration available.
+ ///
public class FramebufferRenderTarget : IRenderTarget
{
- private readonly IFramebufferPlatformSurface _surface;
+ private readonly IFramebufferPlatformSurface _platformSurface;
+ private SKImageInfo _currentImageInfo;
+ private IntPtr _currentFramebufferAddress;
+ private SKSurface _framebufferSurface;
+ private PixelFormatConversionShim _conversionShim;
+ private IDisposable _preFramebufferCopyHandler;
- public FramebufferRenderTarget(IFramebufferPlatformSurface surface)
+ ///
+ /// Create new framebuffer render target using a target surface.
+ ///
+ /// Target surface.
+ public FramebufferRenderTarget(IFramebufferPlatformSurface platformSurface)
{
- _surface = surface;
+ _platformSurface = platformSurface;
}
+ ///
public void Dispose()
{
- //Nothing to do here, since we don't own framebuffer
+ FreeSurface();
+ }
+
+ ///
+ public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
+ {
+ var framebuffer = _platformSurface.Lock();
+ var framebufferImageInfo = new SKImageInfo(framebuffer.Width, framebuffer.Height,
+ framebuffer.Format.ToSkColorType(), SKAlphaType.Premul);
+
+ CreateSurface(framebufferImageInfo, framebuffer);
+
+ var canvas = _framebufferSurface.Canvas;
+
+ canvas.RestoreToCount(-1);
+ canvas.Save();
+ canvas.ResetMatrix();
+
+ var createInfo = new DrawingContextImpl.CreateInfo
+ {
+ Canvas = canvas,
+ Dpi = framebuffer.Dpi,
+ VisualBrushRenderer = visualBrushRenderer,
+ DisableTextLcdRendering = true
+ };
+
+ return new DrawingContextImpl(createInfo, _preFramebufferCopyHandler, framebuffer);
+ }
+
+ ///
+ /// Check if two images info are compatible.
+ ///
+ /// Current.
+ /// Desired.
+ /// True, if images are compatible.
+ private static bool AreImageInfosCompatible(SKImageInfo currentImageInfo, SKImageInfo desiredImageInfo)
+ {
+ return currentImageInfo.Width == desiredImageInfo.Width &&
+ currentImageInfo.Height == desiredImageInfo.Height &&
+ currentImageInfo.ColorType == desiredImageInfo.ColorType;
+ }
+
+ ///
+ /// Create Skia surface backed by given framebuffer.
+ ///
+ /// Desired image info.
+ /// Backing framebuffer.
+ private void CreateSurface(SKImageInfo desiredImageInfo, ILockedFramebuffer framebuffer)
+ {
+ if (_framebufferSurface != null && AreImageInfosCompatible(_currentImageInfo, desiredImageInfo) && _currentFramebufferAddress == framebuffer.Address)
+ {
+ return;
+ }
+
+ FreeSurface();
+
+ _currentFramebufferAddress = framebuffer.Address;
+
+ var surface = SKSurface.Create(desiredImageInfo, _currentFramebufferAddress, framebuffer.RowBytes);
+
+ // If surface cannot be created - try to create a compatibilty shim first
+ if (surface == null)
+ {
+ _conversionShim = new PixelFormatConversionShim(desiredImageInfo, framebuffer.Address);
+ _preFramebufferCopyHandler = _conversionShim.SurfaceCopyHandler;
+
+ surface = _conversionShim.Surface;
+ }
+
+ _framebufferSurface = surface ?? throw new Exception("Unable to create a surface for pixel format " +
+ framebuffer.Format +
+ " or pixel format translator");
+ _currentImageInfo = desiredImageInfo;
+ }
+
+ ///
+ /// Free Skia surface.
+ ///
+ private void FreeSurface()
+ {
+ _conversionShim?.Dispose();
+ _conversionShim = null;
+ _preFramebufferCopyHandler = null;
+
+ if (_conversionShim != null)
+ {
+ _framebufferSurface?.Dispose();
+ }
+
+ _framebufferSurface = null;
+ _currentFramebufferAddress = IntPtr.Zero;
}
- class PixelFormatShim : IDisposable
+ ///
+ /// Converts non-compatible pixel formats using bitmap copies.
+ ///
+ private class PixelFormatConversionShim : IDisposable
{
- private readonly SKImageInfo _nfo;
- private readonly IntPtr _fb;
- private readonly int _rowBytes;
- private SKBitmap _bitmap;
+ private readonly SKBitmap _bitmap;
+ private readonly SKImageInfo _destinationInfo;
+ private readonly IntPtr _framebufferAddress;
- public PixelFormatShim(SKImageInfo nfo, IntPtr fb, int rowBytes)
+ public PixelFormatConversionShim(SKImageInfo destinationInfo, IntPtr framebufferAddress)
{
- _nfo = nfo;
- _fb = fb;
- _rowBytes = rowBytes;
+ _destinationInfo = destinationInfo;
+ _framebufferAddress = framebufferAddress;
+
+ // Create bitmap using default platform settings
+ _bitmap = new SKBitmap(destinationInfo.Width, destinationInfo.Height);
+
+ if (!_bitmap.CanCopyTo(destinationInfo.ColorType))
+ {
+ _bitmap.Dispose();
+
+ throw new Exception(
+ $"Unable to create pixel format shim for conversion from {_bitmap.ColorType} to {destinationInfo.ColorType}");
+ }
+
+ Surface = SKSurface.Create(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes);
-
- _bitmap = new SKBitmap(nfo.Width, nfo.Height);
- if (!_bitmap.CanCopyTo(nfo.ColorType))
+ if (Surface == null)
{
_bitmap.Dispose();
+
throw new Exception(
- $"Unable to create pixel format shim for conversion from {_bitmap.ColorType} to {nfo.ColorType}");
+ $"Unable to create pixel format shim surface for conversion from {_bitmap.ColorType} to {destinationInfo.ColorType}");
}
+
+ SurfaceCopyHandler = Disposable.Create(CopySurface);
}
- public SKSurface CreateSurface() => SKSurface.Create(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes);
+ ///
+ /// Skia surface.
+ ///
+ public SKSurface Surface { get; }
+ ///
+ /// Handler to start conversion via surface copy.
+ ///
+ public IDisposable SurfaceCopyHandler { get; }
+
+ ///
public void Dispose()
{
- using (var tmp = _bitmap.Copy(_nfo.ColorType))
- tmp.CopyPixelsTo(_fb, _nfo.BytesPerPixel * _nfo.Height * _rowBytes, _rowBytes);
+ Surface.Dispose();
_bitmap.Dispose();
}
-
- }
-
- public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
- {
- var fb = _surface.Lock();
- PixelFormatShim shim = null;
- SKImageInfo framebuffer = new SKImageInfo(fb.Width, fb.Height, fb.Format.ToSkColorType(),
- SKAlphaType.Premul);
- var surface = SKSurface.Create(framebuffer, fb.Address, fb.RowBytes) ??
- (shim = new PixelFormatShim(framebuffer, fb.Address, fb.RowBytes))
- .CreateSurface();
- if (surface == null)
- throw new Exception("Unable to create a surface for pixel format " + fb.Format +
- " or pixel format translator");
- var canvas = surface.Canvas;
-
-
- canvas.RestoreToCount(0);
- canvas.Save();
- canvas.ResetMatrix();
- return new DrawingContextImpl(canvas, fb.Dpi, visualBrushRenderer, canvas, surface, shim, fb);
+ ///
+ /// Convert and copy surface to a framebuffer.
+ ///
+ private void CopySurface()
+ {
+ using (var snapshot = Surface.Snapshot())
+ {
+ snapshot.ReadPixels(_destinationInfo, _framebufferAddress, _destinationInfo.RowBytes, 0, 0,
+ SKImageCachingHint.Disallow);
+ }
+ }
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs
index fb134b728c..af4cdb8056 100644
--- a/src/Skia/Avalonia.Skia/GeometryImpl.cs
+++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs
@@ -1,3 +1,6 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
using System;
using Avalonia.Media;
using Avalonia.Platform;
@@ -5,14 +8,169 @@ using SkiaSharp;
namespace Avalonia.Skia
{
- abstract class GeometryImpl : IGeometryImpl
+ ///
+ /// A Skia implementation of .
+ ///
+ public abstract class GeometryImpl : IGeometryImpl
{
+ private PathCache _pathCache;
+
+ ///
public abstract Rect Bounds { get; }
public abstract SKPath EffectivePath { get; }
- public abstract bool FillContains(Point point);
- public abstract Rect GetRenderBounds(Pen pen);
- public abstract IGeometryImpl Intersect(IGeometryImpl geometry);
- public abstract bool StrokeContains(Pen pen, Point point);
- public abstract ITransformedGeometryImpl WithTransform(Matrix transform);
+
+ ///
+ public bool FillContains(Point point)
+ {
+ return PathContainsCore(EffectivePath, point);
+ }
+
+ ///
+ public bool StrokeContains(Pen pen, Point point)
+ {
+ // Skia requires to compute stroke path to check for point containment.
+ // Due to that we are caching using stroke width.
+ // Usually this function is being called with same stroke width per path, so this saves a lot of Skia traffic.
+
+ var strokeWidth = (float)(pen?.Thickness ?? 0);
+
+ if (!_pathCache.HasCacheFor(strokeWidth))
+ {
+ UpdatePathCache(strokeWidth);
+ }
+
+ return PathContainsCore(_pathCache.CachedStrokePath, point);
+ }
+
+ ///
+ /// Update path cache for given stroke width.
+ ///
+ /// Stroke width.
+ private void UpdatePathCache(float strokeWidth)
+ {
+ var strokePath = new SKPath();
+
+ // For stroke widths close to 0 simply use empty path. Render bounds are cached from fill path.
+ if (Math.Abs(strokeWidth) < float.Epsilon)
+ {
+ _pathCache.Cache(strokePath, strokeWidth, Bounds);
+ }
+ else
+ {
+ using (var paint = new SKPaint())
+ {
+ paint.IsStroke = true;
+ paint.StrokeWidth = strokeWidth;
+
+ paint.GetFillPath(EffectivePath, strokePath);
+
+ _pathCache.Cache(strokePath, strokeWidth, strokePath.TightBounds.ToAvaloniaRect());
+ }
+ }
+ }
+
+ ///
+ /// Check Skia path if it contains a point.
+ ///
+ /// Path to check.
+ /// Point.
+ /// True, if point is contained in a path.
+ private static bool PathContainsCore(SKPath path, Point point)
+ {
+ return path.Contains((float)point.X, (float)point.Y);
+ }
+
+ ///
+ public IGeometryImpl Intersect(IGeometryImpl geometry)
+ {
+ var result = EffectivePath.Op(((GeometryImpl) geometry).EffectivePath, SKPathOp.Intersect);
+
+ return result == null ? null : new StreamGeometryImpl(result);
+ }
+
+ ///
+ public Rect GetRenderBounds(Pen pen)
+ {
+ var strokeWidth = (float)(pen?.Thickness ?? 0);
+
+ if (!_pathCache.HasCacheFor(strokeWidth))
+ {
+ UpdatePathCache(strokeWidth);
+ }
+
+ return _pathCache.CachedGeometryRenderBounds.Inflate(strokeWidth / 2.0);
+ }
+
+ ///
+ public ITransformedGeometryImpl WithTransform(Matrix transform)
+ {
+ return new TransformedGeometryImpl(this, transform);
+ }
+
+ ///
+ /// Invalidate all caches. Call after chaning path contents.
+ ///
+ protected void InvalidateCaches()
+ {
+ _pathCache.Invalidate();
+ }
+
+ private struct PathCache
+ {
+ private float _cachedStrokeWidth;
+
+ ///
+ /// Tolerance for two stroke widths to be deemed equal
+ ///
+ public const float Tolerance = float.Epsilon;
+
+ ///
+ /// Cached contour path.
+ ///
+ public SKPath CachedStrokePath { get; private set; }
+
+ ///
+ /// Cached geometry render bounds.
+ ///
+ public Rect CachedGeometryRenderBounds { get; private set; }
+
+ ///
+ /// Is cached valid for given stroke width.
+ ///
+ /// Stroke width to check.
+ /// True, if CachedStrokePath can be used for given stroke width.
+ public bool HasCacheFor(float strokeWidth)
+ {
+ return CachedStrokePath != null && Math.Abs(_cachedStrokeWidth - strokeWidth) < Tolerance;
+ }
+
+ ///
+ /// Cache path for given stroke width. Takes ownership of a passed path.
+ ///
+ /// Path to cache.
+ /// Stroke width to cache.
+ /// Render bounds to use.
+ public void Cache(SKPath path, float strokeWidth, Rect geometryRenderBounds)
+ {
+ if (CachedStrokePath != path)
+ {
+ CachedStrokePath?.Dispose();
+ }
+
+ CachedStrokePath = path;
+ CachedGeometryRenderBounds = geometryRenderBounds;
+ _cachedStrokeWidth = strokeWidth;
+ }
+
+ ///
+ /// Invalidate cache state.
+ ///
+ public void Invalidate()
+ {
+ CachedStrokePath?.Dispose();
+ CachedGeometryRenderBounds = Rect.Empty;
+ _cachedStrokeWidth = default(float);
+ }
+ }
}
}
diff --git a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs
new file mode 100644
index 0000000000..d587a989cc
--- /dev/null
+++ b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs
@@ -0,0 +1,47 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.IO;
+using SkiaSharp;
+
+namespace Avalonia.Skia.Helpers
+{
+ ///
+ /// Helps with saving images to stream/file.
+ ///
+ public static class ImageSavingHelper
+ {
+ ///
+ /// Save Skia image to a file.
+ ///
+ /// Image to save
+ /// Target file.
+ public static void SaveImage(SKImage image, string fileName)
+ {
+ if (image == null) throw new ArgumentNullException(nameof(image));
+ if (fileName == null) throw new ArgumentNullException(nameof(fileName));
+
+ using (var stream = File.Create(fileName))
+ {
+ SaveImage(image, stream);
+ }
+ }
+
+ ///
+ /// Save Skia image to a stream.
+ ///
+ /// Image to save
+ /// Target stream.
+ public static void SaveImage(SKImage image, Stream stream)
+ {
+ if (image == null) throw new ArgumentNullException(nameof(image));
+ if (stream == null) throw new ArgumentNullException(nameof(stream));
+
+ using (var data = image.Encode())
+ {
+ data.SaveTo(stream);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs b/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs
new file mode 100644
index 0000000000..307af708af
--- /dev/null
+++ b/src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs
@@ -0,0 +1,35 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Avalonia.Platform;
+using SkiaSharp;
+
+namespace Avalonia.Skia.Helpers
+{
+ ///
+ /// Helps with resolving pixel formats to Skia color types.
+ ///
+ public static class PixelFormatHelper
+ {
+ ///
+ /// Resolve given format to Skia color type.
+ ///
+ /// Format to resolve.
+ /// Resolved color type.
+ public static SKColorType ResolveColorType(PixelFormat? format)
+ {
+ var colorType = format?.ToSkColorType() ?? SKImageInfo.PlatformColorType;
+
+ // TODO: This looks like some leftover hack
+ var runtimePlatform = AvaloniaLocator.Current?.GetService();
+ var runtime = runtimePlatform?.GetRuntimeInfo();
+
+ if (runtime?.IsDesktop == true && runtime.Value.OperatingSystem == OperatingSystemType.Linux)
+ {
+ colorType = SKColorType.Bgra8888;
+ }
+
+ return colorType;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs b/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs
new file mode 100644
index 0000000000..5aa5de2abc
--- /dev/null
+++ b/src/Skia/Avalonia.Skia/IDrawableBitmapImpl.cs
@@ -0,0 +1,23 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Avalonia.Platform;
+using SkiaSharp;
+
+namespace Avalonia.Skia
+{
+ ///
+ /// Extended bitmap implementation that allows for drawing it's contents.
+ ///
+ internal interface IDrawableBitmapImpl : IBitmapImpl
+ {
+ ///
+ /// Draw bitmap to a drawing context.
+ ///
+ /// Drawing context.
+ /// Source rect.
+ /// Destination rect.
+ /// Paint to use.
+ void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint);
+ }
+}
\ No newline at end of file
diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs
new file mode 100644
index 0000000000..332b8547bf
--- /dev/null
+++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs
@@ -0,0 +1,92 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.IO;
+using Avalonia.Platform;
+using Avalonia.Skia.Helpers;
+using SkiaSharp;
+
+namespace Avalonia.Skia
+{
+ ///
+ /// Immutable Skia bitmap.
+ ///
+ public class ImmutableBitmap : IDrawableBitmapImpl
+ {
+ private readonly SKImage _image;
+
+ ///
+ /// Create immutable bitmap from given stream.
+ ///
+ /// Stream containing encoded data.
+ public ImmutableBitmap(Stream stream)
+ {
+ using (var skiaStream = new SKManagedStream(stream))
+ {
+ _image = SKImage.FromEncodedData(SKData.Create(skiaStream));
+
+ if (_image == null)
+ {
+ throw new ArgumentException("Unable to load bitmap from provided data");
+ }
+
+ PixelWidth = _image.Width;
+ PixelHeight = _image.Height;
+ }
+ }
+
+ ///
+ /// Create immutable bitmap from given pixel data copy.
+ ///
+ /// Width of data pixels.
+ /// Height of data pixels.
+ /// Stride of data pixels.
+ /// Format of data pixels.
+ /// Data pixels.
+ public ImmutableBitmap(int width, int height, int stride, PixelFormat format, IntPtr data)
+ {
+ var imageInfo = new SKImageInfo(width, height, format.ToSkColorType(), SKAlphaType.Premul);
+
+ _image = SKImage.FromPixelCopy(imageInfo, data, stride);
+
+ if (_image == null)
+ {
+ throw new ArgumentException("Unable to create bitmap from provided data");
+ }
+
+ PixelWidth = width;
+ PixelHeight = height;
+ }
+
+ ///
+ public int PixelWidth { get; }
+
+ ///
+ public int PixelHeight { get; }
+
+ ///
+ public void Dispose()
+ {
+ _image.Dispose();
+ }
+
+ ///
+ public void Save(string fileName)
+ {
+ ImageSavingHelper.SaveImage(_image, fileName);
+ }
+
+ ///
+ public void Save(Stream stream)
+ {
+ ImageSavingHelper.SaveImage(_image, stream);
+ }
+
+ ///
+ public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint)
+ {
+ context.Canvas.DrawImage(_image, sourceRect, destRect, paint);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
index 50e65f45dc..d4e6403dc9 100644
--- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
+++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
@@ -1,21 +1,21 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
using System;
using System.Collections.Generic;
using System.IO;
-using System.Linq;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Media;
using Avalonia.Platform;
-using SkiaSharp;
namespace Avalonia.Skia
{
- public partial class PlatformRenderInterface : IPlatformRenderInterface
+ ///
+ /// Skia platform render interface.
+ ///
+ public class PlatformRenderInterface : IPlatformRenderInterface
{
- public IBitmapImpl CreateBitmap(int width, int height)
- {
- return CreateRenderTargetBitmap(width, height, 96, 96);
- }
-
+ ///
public IFormattedTextImpl CreateFormattedText(
string text,
Typeface typeface,
@@ -27,27 +27,19 @@ namespace Avalonia.Skia
return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans);
}
+ ///
public IStreamGeometryImpl CreateStreamGeometry()
{
return new StreamGeometryImpl();
}
- public IBitmapImpl LoadBitmap(System.IO.Stream stream)
+ ///
+ public IBitmapImpl LoadBitmap(Stream stream)
{
- using (var s = new SKManagedStream(stream))
- {
- var bitmap = SKBitmap.Decode(s);
- if (bitmap != null)
- {
- return new BitmapImpl(bitmap);
- }
- else
- {
- throw new ArgumentException("Unable to load bitmap from provided data");
- }
- }
+ return new ImmutableBitmap(stream);
}
+ ///
public IBitmapImpl LoadBitmap(string fileName)
{
using (var stream = File.OpenRead(fileName))
@@ -56,16 +48,13 @@ namespace Avalonia.Skia
}
}
+ ///
public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride)
{
- using (var tmp = new SKBitmap())
- {
- tmp.InstallPixels(new SKImageInfo(width, height, format.ToSkColorType(), SKAlphaType.Premul)
- , data, stride);
- return new BitmapImpl(tmp.Copy());
- }
+ return new ImmutableBitmap(width, height, stride, format, data);
}
+ ///
public IRenderTargetBitmapImpl CreateRenderTargetBitmap(
int width,
int height,
@@ -73,24 +62,47 @@ namespace Avalonia.Skia
double dpiY)
{
if (width < 1)
+ {
throw new ArgumentException("Width can't be less than 1", nameof(width));
+ }
+
if (height < 1)
+ {
throw new ArgumentException("Height can't be less than 1", nameof(height));
+ }
+
+ var dpi = new Vector(dpiX, dpiY);
- return new BitmapImpl(width, height, new Vector(dpiX, dpiY));
+ var createInfo = new SurfaceRenderTarget.CreateInfo
+ {
+ Width = width,
+ Height = height,
+ Dpi = dpi,
+ DisableTextLcdRendering = false
+ };
+
+ return new SurfaceRenderTarget(createInfo);
}
+ ///
public virtual IRenderTarget CreateRenderTarget(IEnumerable