Browse Source

Merge pull request #7029 from AvaloniaUI/features/web

Avalonia.Web (wasm)
pull/7080/head
Dan Walmsley 4 years ago
committed by GitHub
parent
commit
3c56b3751f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .gitignore
  2. 90
      Avalonia.sln
  3. 1
      NuGet.Config
  4. 10
      build/ApiDiff.props
  5. 5
      build/HarfBuzzSharp.props
  6. 5
      build/SkiaSharp.props
  7. 10
      samples/ControlCatalog.Web/App.razor
  8. 14
      samples/ControlCatalog.Web/App.razor.cs
  9. 38
      samples/ControlCatalog.Web/ControlCatalog.Web.csproj
  10. 28
      samples/ControlCatalog.Web/LinkerConfig.xml
  11. 5
      samples/ControlCatalog.Web/Pages/Index.razor
  12. 29
      samples/ControlCatalog.Web/Program.cs
  13. 30
      samples/ControlCatalog.Web/Properties/launchSettings.json
  14. 7
      samples/ControlCatalog.Web/Shared/MainLayout.razor
  15. 70
      samples/ControlCatalog.Web/Shared/MainLayout.razor.css
  16. 11
      samples/ControlCatalog.Web/_Imports.razor
  17. 90
      samples/ControlCatalog.Web/wwwroot/css/app.css
  18. BIN
      samples/ControlCatalog.Web/wwwroot/favicon.ico
  19. 23
      samples/ControlCatalog.Web/wwwroot/index.html
  20. 1
      samples/ControlCatalog.Web/wwwroot/js/app.js
  21. 1
      src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
  22. 2
      src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
  23. 2
      src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
  24. BIN
      src/Web/Avalonia.Web.Blazor/Assets/NotoMono-Regular.ttf
  25. BIN
      src/Web/Avalonia.Web.Blazor/Assets/NotoSans-Italic.ttf
  26. 57
      src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj
  27. 6
      src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.targets
  28. 18
      src/Web/Avalonia.Web.Blazor/AvaloniaBlazorAppBuilder.cs
  29. 19
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor
  30. 375
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
  31. 61
      src/Web/Avalonia.Web.Blazor/BlazorRuntimePlatform.cs
  32. 33
      src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs
  33. 25
      src/Web/Avalonia.Web.Blazor/BlazorSkiaGpu.cs
  34. 37
      src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderSession.cs
  35. 47
      src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs
  36. 30
      src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs
  37. 78
      src/Web/Avalonia.Web.Blazor/CustomFontManagerImpl.cs
  38. 20
      src/Web/Avalonia.Web.Blazor/Interop/ActionHelper.cs
  39. 87
      src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs
  40. 20
      src/Web/Avalonia.Web.Blazor/Interop/FloatFloatActionHelper.cs
  41. 41
      src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs
  42. 42
      src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs
  43. 79
      src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs
  44. 61
      src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs
  45. 41
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/DpiWatcher.ts
  46. 23
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/InputHelper.ts
  47. 225
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts
  48. 68
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/SizeWatcher.ts
  49. 7
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/extras.d.ts
  50. 56
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/index.d.ts
  51. 326
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/emscripten/index.d.ts
  52. 127
      src/Web/Avalonia.Web.Blazor/Keycodes.cs
  53. 16
      src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs
  54. 165
      src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs
  55. 80
      src/Web/Avalonia.Web.Blazor/WinStubs.cs
  56. 100
      src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs
  57. 1
      src/Web/Avalonia.Web.Blazor/_Imports.razor
  58. 14
      src/Web/Avalonia.Web.Blazor/tsconfig.json

3
.gitignore

@ -210,3 +210,6 @@ obj-Skia/
coc-settings.json
.ccls-cache
.ccls
*.map
src/Web/Avalonia.Web.Blazor/wwwroot/*.js
src/Web/Avalonia.Web.Blazor/Interop/Typescript/*.js

90
Avalonia.sln

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29102.190
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}"
EndProject
@ -222,11 +222,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sandbox", "samples\Sandbox\Sandbox.csproj", "{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroComGenerator", "src\tools\MicroComGenerator\MicroComGenerator.csproj", "{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroComGenerator", "src\tools\MicroComGenerator\MicroComGenerator.csproj", "{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.MicroCom", "src\Avalonia.MicroCom\Avalonia.MicroCom.csproj", "{FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.MicroCom", "src\Avalonia.MicroCom\Avalonia.MicroCom.csproj", "{FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniMvvm", "samples\MiniMvvm\MiniMvvm.csproj", "{BC594FD5-4AF2-409E-A1E6-04123F54D7C5}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniMvvm", "samples\MiniMvvm\MiniMvvm.csproj", "{BC594FD5-4AF2-409E-A1E6-04123F54D7C5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Web.Blazor", "src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.csproj", "{25831348-EB2A-483E-9576-E8F6528674A5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Web", "samples\ControlCatalog.Web\ControlCatalog.Web.csproj", "{C08E9894-AA92-426E-BF56-033E262CAD3E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsInteropTest", "samples\interop\WindowsInteropTest\WindowsInteropTest.csproj", "{26A98DA1-D89D-4A95-8152-349F404DA2E2}"
EndProject
@ -1996,30 +2002,6 @@ Global
{909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhone.Build.0 = Release|Any CPU
{909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhone.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhone.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|Any CPU.Build.0 = Release|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.ActiveCfg = Release|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.Build.0 = Release|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
@ -2116,6 +2098,54 @@ Global
{BC594FD5-4AF2-409E-A1E6-04123F54D7C5}.Release|iPhone.Build.0 = Release|Any CPU
{BC594FD5-4AF2-409E-A1E6-04123F54D7C5}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{BC594FD5-4AF2-409E-A1E6-04123F54D7C5}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.AppStore|iPhone.Build.0 = Debug|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Debug|iPhone.Build.0 = Debug|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Release|Any CPU.Build.0 = Release|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Release|iPhone.ActiveCfg = Release|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Release|iPhone.Build.0 = Release|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.AppStore|iPhone.Build.0 = Debug|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.Debug|iPhone.Build.0 = Debug|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.Release|Any CPU.Build.0 = Release|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.Release|iPhone.ActiveCfg = Release|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.Release|iPhone.Build.0 = Release|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{26A98DA1-D89D-4A95-8152-349F404DA2E2}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{26A98DA1-D89D-4A95-8152-349F404DA2E2}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{26A98DA1-D89D-4A95-8152-349F404DA2E2}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
@ -2200,6 +2230,8 @@ Global
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{BC594FD5-4AF2-409E-A1E6-04123F54D7C5} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{25831348-EB2A-483E-9576-E8F6528674A5} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}
{C08E9894-AA92-426E-BF56-033E262CAD3E} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{26A98DA1-D89D-4A95-8152-349F404DA2E2} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution

1
NuGet.Config

@ -5,5 +5,6 @@
<clear />
<add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="dotnet-eng" value="https://nuget.avaloniaui.net/repository/avalonia-devdeps/index.json" protocolVersion="3" />
<add key="skiasharp" value="https://aka.ms/skiasharp-eap/index.json" protocolVersion="3" />
</packageSources>
</configuration>

10
build/ApiDiff.props

@ -4,9 +4,9 @@
<NugetPackageName Condition="'$(PackageId)' != ''">$(PackageId)</NugetPackageName>
<NugetPackageName Condition="'$(PackageId)' == ''">Avalonia</NugetPackageName>
</PropertyGroup>
<ItemGroup>
<PackageDownload Include="$(NugetPackageName)" Version="[$(ApiContractPackageVersion)]" />
<PackageReference Include="Microsoft.DotNet.ApiCompat" Version="5.0.0-beta.20372.2" PrivateAssets="All" />
<ResolvedMatchingContract Include="$(NuGetPackageRoot)\$(NugetPackageName.ToLowerInvariant())\$(ApiContractPackageVersion)\lib\$(TargetFramework)\$(AssemblyName).dll" />
</ItemGroup>
<ItemGroup>
<PackageDownload Include="$(NugetPackageName)" Version="[$(ApiContractPackageVersion)]" />
<PackageReference Include="Microsoft.DotNet.ApiCompat" Version="5.0.0-beta.20372.2" PrivateAssets="All" />
<ResolvedMatchingContract Include="$(NuGetPackageRoot)\$(NugetPackageName.ToLowerInvariant())\$(ApiContractPackageVersion)\lib\$(TargetFramework)\$(AssemblyName).dll" />
</ItemGroup>
</Project>

5
build/HarfBuzzSharp.props

@ -1,6 +1,7 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="HarfBuzzSharp" Version="2.8.2-preview.155" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2-preview.155" />
<PackageReference Include="HarfBuzzSharp" Version="2.8.2-preview.171" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2-preview.171" />
<PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="2.8.2-preview.171"/>
</ItemGroup>
</Project>

5
build/SkiaSharp.props

@ -1,6 +1,7 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="2.88.0-preview.155" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.88.0-preview.155" />
<PackageReference Include="SkiaSharp" Version="2.88.0-preview.171" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.88.0-preview.171" />
<PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="SkiaSharp.NativeAssets.WebAssembly" Version="2.88.0-preview.171"/>
</ItemGroup>
</Project>

10
samples/ControlCatalog.Web/App.razor

@ -0,0 +1,10 @@
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

14
samples/ControlCatalog.Web/App.razor.cs

@ -0,0 +1,14 @@
using Avalonia.Web.Blazor;
namespace ControlCatalog.Web;
public partial class App
{
protected override void OnParametersSet()
{
WebAppBuilder.Configure<ControlCatalog.App>()
.SetupWithSingleViewLifetime();
base.OnParametersSet();
}
}

38
samples/ControlCatalog.Web/ControlCatalog.Web.csproj

@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<BlazorLinkerDescriptor Include="LinkerConfig.xml" />
</ItemGroup>
<!-- In debug, make builds faster by reducing optimizations -->
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<WasmNativeStrip>false</WasmNativeStrip>
<EmccCompileOptimizationFlag>-O1</EmccCompileOptimizationFlag>
<RunAOTCompilation>false</RunAOTCompilation>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<Optimize>true</Optimize>
<WasmNativeStrip>true</WasmNativeStrip>
<EmccCompileOptimizationFlag>-O3</EmccCompileOptimizationFlag>
<EmccLinkOptimizationFlag>-O3</EmccLinkOptimizationFlag>
<RunAOTCompilation>false</RunAOTCompilation>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.0"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.0" PrivateAssets="all"/>
</ItemGroup>
<Import Project="..\..\src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.targets" />
<ItemGroup>
<ProjectReference Include="..\..\src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.csproj"/>
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj"/>
</ItemGroup>
</Project>

28
samples/ControlCatalog.Web/LinkerConfig.xml

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
This file specifies which parts of the BCL or Blazor packages must not be
stripped by the IL Linker even if they aren't referenced by user code.
-->
<linker>
<assembly fullname="mscorlib">
<!--
Preserve the methods in WasmRuntime because its methods are called by
JavaScript client-side code to implement timers.
Fixes: https://github.com/dotnet/blazor/issues/239
-->
<type fullname="System.Threading.WasmRuntime"/>
</assembly>
<assembly fullname="System.Core">
<!--
System.Linq.Expressions* is required by Json.NET and any
expression.Compile caller. The assembly isn't stripped.
-->
<type fullname="System.Linq.Expressions*"/>
</assembly>
<!--
In this example, the app's entry point assembly is listed. The assembly
isn't stripped by the IL Linker.
-->
<assembly fullname="ControlCatalog" preserve="All" />
</linker>

5
samples/ControlCatalog.Web/Pages/Index.razor

@ -0,0 +1,5 @@
@page "/"
@using Avalonia.Web.Blazor
<AvaloniaView />

29
samples/ControlCatalog.Web/Program.cs

@ -0,0 +1,29 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using ControlCatalog.Web;
public class Program
{
public static async Task Main(string[] args)
{
await CreateHostBuilder(args).Build().RunAsync();
}
public static WebAssemblyHostBuilder CreateHostBuilder(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
return builder;
}
}

30
samples/ControlCatalog.Web/Properties/launchSettings.json

@ -0,0 +1,30 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:13961",
"sslPort": 44319
}
},
"profiles": {
"ControlCatalog.Web - IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"ControlCatalog.Web": {
"commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

7
samples/ControlCatalog.Web/Shared/MainLayout.razor

@ -0,0 +1,7 @@
@inherits LayoutComponentBase
<div class="page">
<div class="main">
@Body
</div>
</div>

70
samples/ControlCatalog.Web/Shared/MainLayout.razor.css

@ -0,0 +1,70 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
.main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
}
.top-row a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row:not(.auth) {
display: none;
}
.top-row.auth {
justify-content: space-between;
}
.top-row a, .top-row .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.main > div {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}

11
samples/ControlCatalog.Web/_Imports.razor

@ -0,0 +1,11 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using ControlCatalog.Web
@using ControlCatalog.Web.Shared
@using SkiaSharp

90
samples/ControlCatalog.Web/wwwroot/css/app.css

@ -0,0 +1,90 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
margin: 0;
height: 100vh;
overflow: hidden;
touch-action: none;
}
a, .btn-link {
color: #0366d6;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.content {
padding-top: 1.1rem;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid red;
}
.validation-message {
color: red;
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
.canvas-container {
opacity:1;
background-color:#ccc;
position:fixed;
width:100%;
height:100%;
top:0px;
left:0px;
z-index:500;
}
canvas
{
opacity:1;
background-color:#ccc;
position:fixed;
width:100%;
height:100%;
top:0px;
left:0px;
z-index:500;
}
#app, .page {
height: 100%;
}
.overlay{
opacity:0.0;
background-color:#ccc;
position:fixed;
width:100vw;
height:100vh;
top:0px;
left:0px;
z-index:1000;
}

BIN
samples/ControlCatalog.Web/wwwroot/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

23
samples/ControlCatalog.Web/wwwroot/index.html

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>Avalonia Sample</title>
<base href="/" />
<link href="css/app.css" rel="stylesheet" />
</head>
<body>
<div id="app">Powered by Avalonia</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="js/app.js"></script>
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>

1
samples/ControlCatalog.Web/wwwroot/js/app.js

@ -0,0 +1 @@


1
src/Skia/Avalonia.Skia/Avalonia.Skia.csproj

@ -5,6 +5,7 @@
<AssemblyName>Avalonia.Skia</AssemblyName>
<PackageId>Avalonia.Skia</PackageId>
<IncludeLinuxSkia>true</IncludeLinuxSkia>
<IncludeWasmSkia>true</IncludeWasmSkia>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>

2
src/Skia/Avalonia.Skia/SKTypefaceCollection.cs

@ -6,7 +6,7 @@ using SkiaSharp;
namespace Avalonia.Skia
{
internal class SKTypefaceCollection
public class SKTypefaceCollection
{
private readonly ConcurrentDictionary<Typeface, SKTypeface> _typefaces =
new ConcurrentDictionary<Typeface, SKTypeface>();

2
src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs

@ -7,7 +7,7 @@ using SkiaSharp;
namespace Avalonia.Skia
{
internal static class SKTypefaceCollectionCache
public static class SKTypefaceCollectionCache
{
private static readonly ConcurrentDictionary<FontFamily, SKTypefaceCollection> s_cachedCollections;

BIN
src/Web/Avalonia.Web.Blazor/Assets/NotoMono-Regular.ttf

Binary file not shown.

BIN
src/Web/Avalonia.Web.Blazor/Assets/NotoSans-Italic.ttf

Binary file not shown.

57
src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj

@ -0,0 +1,57 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PackageId>Avalonia.Web.Blazor</PackageId>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<SupportedPlatform Include="browser" />
<Compile Include="..\..\Shared\PlatformSupport\AssetLoader.cs" />
</ItemGroup>
<PropertyGroup>
<TypescriptOutDir>wwwroot</TypescriptOutDir>
<TypeScriptNoEmitOnError>true</TypeScriptNoEmitOnError>
<TypeScriptNoImplicitReturns>true</TypeScriptNoImplicitReturns>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<TypeScriptRemoveComments>false</TypeScriptRemoveComments>
<TypeScriptSourceMap>true</TypeScriptSourceMap>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<TypeScriptRemoveComments>true</TypeScriptRemoveComments>
<TypeScriptSourceMap>false</TypeScriptSourceMap>
</PropertyGroup>
<Import Project="..\..\..\build\BuildTargets.targets" />
<Import Project="..\..\..\build\SkiaSharp.props" />
<Import Project="..\..\..\build\HarfBuzzSharp.props" />
<ItemGroup>
<AvaloniaResource Include="Assets\*" />
<Content Include="*.props">
<Pack>true</Pack>
<PackagePath>build\;buildTransitive\</PackagePath>
</Content>
<Content Include="*.targets">
<Pack>true</Pack>
<PackagePath>build\;buildTransitive\</PackagePath>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.0" />
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="4.5.2" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\..\Avalonia.Controls\Avalonia.Controls.csproj" />
<ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
</ItemGroup>
</Project>

6
src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.targets

@ -0,0 +1,6 @@
<Project>
<ItemGroup>
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\2.0.23\libHarfBuzzSharp.a" />
<NativeFileReference Include="$(SkiaSharpStaticLibraryPath)\2.0.23\libSkiaSharp.a" />
</ItemGroup>
</Project>

18
src/Web/Avalonia.Web.Blazor/AvaloniaBlazorAppBuilder.cs

@ -0,0 +1,18 @@
using Avalonia.Controls;
using Avalonia.Platform;
namespace Avalonia.Web.Blazor
{
public class AvaloniaBlazorAppBuilder : AppBuilderBase<AvaloniaBlazorAppBuilder>
{
public AvaloniaBlazorAppBuilder(IRuntimePlatform platform, Action<AvaloniaBlazorAppBuilder> platformServices)
: base(platform, platformServices)
{
}
public AvaloniaBlazorAppBuilder() : base(BlazorRuntimePlatform.Instance, BlazorRuntimePlatform.RegisterServices)
{
UseWindowingSubsystem(BlazorWindowingPlatform.Register);
}
}
}

19
src/Web/Avalonia.Web.Blazor/AvaloniaView.razor

@ -0,0 +1,19 @@
<div id="container" class="avalonia-container" tabindex="0" oncontextmenu="return false;"
ontouchstart="@OnTouchStart"
ontouchend="@OnTouchEnd"
ontouchcancel="@OnTouchCancel"
ontouchmove="@OnTouchMove"
onmousemove="@OnMouseMove"
onmousedown="@OnMouseDown"
onmouseup="@OnMouseUp"
onwheel="@OnWheel"
onkeydown="@OnKeyDown"
onkeyup="@OnKeyUp">
<canvas @ref="_htmlCanvas" @attributes="AdditionalAttributes"/>
<input @ref="_inputElement"
class="overlay"
type="text"
oninput="@OnInput"/>
</div>

375
src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs

@ -0,0 +1,375 @@
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Embedding;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Web.Blazor.Interop;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
using SkiaSharp;
namespace Avalonia.Web.Blazor
{
public partial class AvaloniaView : ITextInputMethodImpl
{
private readonly RazorViewTopLevelImpl _topLevelImpl;
private EmbeddableControlRoot _topLevel;
// Interop
private SKHtmlCanvasInterop _interop = null!;
private SizeWatcherInterop _sizeWatcher = null!;
private DpiWatcherInterop _dpiWatcher = null!;
private SKHtmlCanvasInterop.GLInfo? _jsGlInfo = null!;
private InputHelperInterop _inputHelper = null!;
private ElementReference _htmlCanvas;
private ElementReference _inputElement;
private double _dpi;
private SKSize _canvasSize;
private GRContext? _context;
private GRGlInterface? _glInterface;
private const SKColorType ColorType = SKColorType.Rgba8888;
private bool _initialised;
[Inject] private IJSRuntime Js { get; set; } = null!;
public AvaloniaView()
{
_topLevelImpl = new RazorViewTopLevelImpl(this);
_topLevel = new EmbeddableControlRoot(_topLevelImpl);
if (Application.Current.ApplicationLifetime is ISingleViewApplicationLifetime lifetime)
{
_topLevel.Content = lifetime.MainView;
}
}
private void OnTouchStart(TouchEventArgs e)
{
foreach (var touch in e.ChangedTouches)
{
_topLevelImpl.RawTouchEvent(RawPointerEventType.TouchBegin, new Point(touch.ClientX, touch.ClientY),
GetModifiers(e), touch.Identifier);
}
}
private void OnTouchEnd(TouchEventArgs e)
{
foreach (var touch in e.ChangedTouches)
{
_topLevelImpl.RawTouchEvent(RawPointerEventType.TouchEnd, new Point(touch.ClientX, touch.ClientY),
GetModifiers(e), touch.Identifier);
}
}
private void OnTouchCancel(TouchEventArgs e)
{
foreach (var touch in e.ChangedTouches)
{
_topLevelImpl.RawTouchEvent(RawPointerEventType.TouchCancel, new Point(touch.ClientX, touch.ClientY),
GetModifiers(e), touch.Identifier);
}
}
private void OnTouchMove(TouchEventArgs e)
{
foreach (var touch in e.ChangedTouches)
{
_topLevelImpl.RawTouchEvent(RawPointerEventType.TouchUpdate, new Point(touch.ClientX, touch.ClientY),
GetModifiers(e), touch.Identifier);
}
}
private void OnMouseMove(MouseEventArgs e)
{
_topLevelImpl.RawMouseEvent(RawPointerEventType.Move, new Point(e.ClientX, e.ClientY), GetModifiers(e));
}
private void OnMouseUp(MouseEventArgs e)
{
RawPointerEventType type = default;
switch (e.Button)
{
case 0:
type = RawPointerEventType.LeftButtonUp;
break;
case 1:
type = RawPointerEventType.MiddleButtonUp;
break;
case 2:
type = RawPointerEventType.RightButtonUp;
break;
}
_topLevelImpl.RawMouseEvent(type, new Point(e.ClientX, e.ClientY), GetModifiers(e));
}
private void OnMouseDown(MouseEventArgs e)
{
RawPointerEventType type = default;
switch (e.Button)
{
case 0:
type = RawPointerEventType.LeftButtonDown;
break;
case 1:
type = RawPointerEventType.MiddleButtonDown;
break;
case 2:
type = RawPointerEventType.RightButtonDown;
break;
}
_topLevelImpl.RawMouseEvent(type, new Point(e.ClientX, e.ClientY), GetModifiers(e));
}
private void OnWheel(WheelEventArgs e)
{
_topLevelImpl.RawMouseWheelEvent(new Point(e.ClientX, e.ClientY),
new Vector(-(e.DeltaX / 50), -(e.DeltaY / 50)), GetModifiers(e));
}
private static RawInputModifiers GetModifiers(WheelEventArgs e)
{
var modifiers = RawInputModifiers.None;
if (e.CtrlKey)
modifiers |= RawInputModifiers.Control;
if (e.AltKey)
modifiers |= RawInputModifiers.Alt;
if (e.ShiftKey)
modifiers |= RawInputModifiers.Shift;
if (e.MetaKey)
modifiers |= RawInputModifiers.Meta;
if ((e.Buttons & 1L) == 1)
modifiers |= RawInputModifiers.LeftMouseButton;
if ((e.Buttons & 2L) == 2)
modifiers |= RawInputModifiers.RightMouseButton;
if ((e.Buttons & 4L) == 4)
modifiers |= RawInputModifiers.MiddleMouseButton;
return modifiers;
}
private static RawInputModifiers GetModifiers(TouchEventArgs e)
{
var modifiers = RawInputModifiers.None;
if (e.CtrlKey)
modifiers |= RawInputModifiers.Control;
if (e.AltKey)
modifiers |= RawInputModifiers.Alt;
if (e.ShiftKey)
modifiers |= RawInputModifiers.Shift;
if (e.MetaKey)
modifiers |= RawInputModifiers.Meta;
return modifiers;
}
private static RawInputModifiers GetModifiers(MouseEventArgs e)
{
var modifiers = RawInputModifiers.None;
if (e.CtrlKey)
modifiers |= RawInputModifiers.Control;
if (e.AltKey)
modifiers |= RawInputModifiers.Alt;
if (e.ShiftKey)
modifiers |= RawInputModifiers.Shift;
if (e.MetaKey)
modifiers |= RawInputModifiers.Meta;
if ((e.Buttons & 1L) == 1)
modifiers |= RawInputModifiers.LeftMouseButton;
if ((e.Buttons & 2L) == 2)
modifiers |= RawInputModifiers.RightMouseButton;
if ((e.Buttons & 4L) == 4)
modifiers |= RawInputModifiers.MiddleMouseButton;
return modifiers;
}
private static RawInputModifiers GetModifiers(KeyboardEventArgs e)
{
var modifiers = RawInputModifiers.None;
if (e.CtrlKey)
modifiers |= RawInputModifiers.Control;
if (e.AltKey)
modifiers |= RawInputModifiers.Alt;
if (e.ShiftKey)
modifiers |= RawInputModifiers.Shift;
if (e.MetaKey)
modifiers |= RawInputModifiers.Meta;
return modifiers;
}
private void OnKeyDown(KeyboardEventArgs e)
{
_topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, e.Key, GetModifiers(e));
}
private void OnKeyUp(KeyboardEventArgs e)
{
_topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, e.Code, GetModifiers(e));
}
private void OnInput(ChangeEventArgs e)
{
if (e.Value != null)
{
var inputData = e.Value.ToString();
if (inputData != null)
{
_topLevelImpl.RawTextEvent(inputData);
}
}
_inputHelper.Clear();
}
[Parameter(CaptureUnmatchedValues = true)]
public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
Threading.Dispatcher.UIThread.Post(async () =>
{
_inputHelper = await InputHelperInterop.ImportAsync(Js, _inputElement);
_inputHelper.Hide();
_inputHelper.SetCursor("default");
Console.WriteLine("starting html canvas setup");
_interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame);
Console.WriteLine("Interop created");
_jsGlInfo = _interop.InitGL();
Console.WriteLine("jsglinfo created - init gl");
_sizeWatcher = await SizeWatcherInterop.ImportAsync(Js, _htmlCanvas, OnSizeChanged);
_dpiWatcher = await DpiWatcherInterop.ImportAsync(Js, OnDpiChanged);
Console.WriteLine("watchers created.");
// create the SkiaSharp context
if (_context == null)
{
Console.WriteLine("create glcontext");
_glInterface = GRGlInterface.Create();
_context = GRContext.CreateGl(_glInterface);
// bump the default resource cache limit
_context.SetResourceCacheLimit(256 * 1024 * 1024);
Console.WriteLine("glcontext created and resource limit set");
}
_topLevelImpl.SetSurface(_context, _jsGlInfo, ColorType,
new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi);
_initialised = true;
_topLevel.Prepare();
_topLevel.Renderer.Start();
Invalidate();
});
}
}
private void OnRenderFrame()
{
if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0 || _jsGlInfo == null)
{
Console.WriteLine("nothing to render");
return;
}
ManualTriggerRenderTimer.Instance.RaiseTick();
}
public void Dispose()
{
_dpiWatcher.Unsubscribe(OnDpiChanged);
_sizeWatcher.Dispose();
_interop.Dispose();
}
private void OnDpiChanged(double newDpi)
{
_dpi = newDpi;
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
Invalidate();
}
private void OnSizeChanged(SKSize newSize)
{
_canvasSize = newSize;
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
Invalidate();
}
public void Invalidate()
{
if (!_initialised || _canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0 || _jsGlInfo == null)
{
Console.WriteLine("invalidate ignored");
return;
}
_interop.RequestAnimationFrame(true, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
}
public void SetActive(bool active)
{
_inputHelper.Clear();
if (active)
{
_inputHelper.Show();
_inputHelper.Focus();
}
else
{
_inputHelper.Hide();
}
}
public void SetCursorRect(Rect rect)
{
}
public void SetOptions(TextInputOptionsQueryEventArgs options)
{
}
public void Reset()
{
_inputHelper.Clear();
}
}
}

61
src/Web/Avalonia.Web.Blazor/BlazorRuntimePlatform.cs

@ -0,0 +1,61 @@
using System.Runtime.InteropServices;
using Avalonia.Platform;
using Avalonia.Shared.PlatformSupport;
namespace Avalonia.Web.Blazor
{
internal class BlazorRuntimePlatform : IRuntimePlatform
{
public static readonly IRuntimePlatform Instance = new BlazorRuntimePlatform();
public IDisposable StartSystemTimer(TimeSpan interval, Action tick)
{
return new Timer(_ => tick(), null, interval, interval);
}
public RuntimePlatformInfo GetRuntimeInfo()
{
return new RuntimePlatformInfo
{
IsDesktop = false,
IsMobile = false,
IsMono = true,
IsUnix = false,
IsCoreClr = false,
IsDotNetFramework = false
};
}
private class BasicBlob : IUnmanagedBlob
{
public BasicBlob(int size)
{
Address = Marshal.AllocHGlobal(size);
Size = size;
}
public void Dispose()
{
if (Address != IntPtr.Zero)
Marshal.FreeHGlobal(Address);
Address = IntPtr.Zero;
}
public IntPtr Address { get; private set; }
public int Size { get; }
public bool IsDisposed => Address == IntPtr.Zero;
}
public IUnmanagedBlob AllocBlob(int size)
{
return new BasicBlob(size);
}
public static void RegisterServices(AvaloniaBlazorAppBuilder builder)
{
AssetLoader.RegisterResUriParsers();
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>().ToConstant(Instance);
AvaloniaLocator.CurrentMutable.Bind<IAssetLoader>().ToConstant(new AssetLoader());
}
}
}

33
src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs

@ -0,0 +1,33 @@
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Media;
namespace Avalonia.Web.Blazor
{
public class BlazorSingleViewLifetime : ISingleViewApplicationLifetime
{
public Control? MainView { get; set; }
}
public static class WebAppBuilder
{
public static T SetupWithSingleViewLifetime<T>(
this T builder)
where T : AppBuilderBase<T>, new()
{
return builder.SetupWithLifetime(new BlazorSingleViewLifetime());
}
public static AvaloniaBlazorAppBuilder Configure<TApp>()
where TApp : Application, new()
{
var builder = AvaloniaBlazorAppBuilder.Configure<TApp>()
.UseSkia()
.With(new SkiaOptions { CustomGpuFactory = () => new BlazorSkiaGpu() });
AvaloniaLocator.CurrentMutable.Bind<FontManager>().ToConstant(new FontManager(new CustomFontManagerImpl()));
return builder;
}
}
}

25
src/Web/Avalonia.Web.Blazor/BlazorSkiaGpu.cs

@ -0,0 +1,25 @@
using Avalonia.Skia;
namespace Avalonia.Web.Blazor
{
public class BlazorSkiaGpu : ISkiaGpu
{
public ISkiaGpuRenderTarget? TryCreateRenderTarget(IEnumerable<object> surfaces)
{
foreach (var surface in surfaces)
{
if (surface is BlazorSkiaSurface blazorSkiaSurface)
{
return new BlazorSkiaGpuRenderTarget(blazorSkiaSurface);
}
}
return null;
}
public ISkiaSurface? TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session)
{
return null;
}
}
}

37
src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderSession.cs

@ -0,0 +1,37 @@
using Avalonia.Skia;
using SkiaSharp;
namespace Avalonia.Web.Blazor
{
internal class BlazorSkiaGpuRenderSession : ISkiaGpuRenderSession
{
private readonly SKSurface _surface;
public BlazorSkiaGpuRenderSession(BlazorSkiaSurface blazorSkiaSurface, GRBackendRenderTarget renderTarget)
{
_surface = SKSurface.Create(blazorSkiaSurface.Context, renderTarget, blazorSkiaSurface.Origin, blazorSkiaSurface.ColorType);
GrContext = blazorSkiaSurface.Context;
ScaleFactor = blazorSkiaSurface.Scaling;
SurfaceOrigin = blazorSkiaSurface.Origin;
}
public void Dispose()
{
_surface.Flush();
_surface.Dispose();
}
public GRContext GrContext { get; }
public SKSurface SkSurface => _surface;
public double ScaleFactor { get; }
public GRSurfaceOrigin SurfaceOrigin { get; }
}
}

47
src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs

@ -0,0 +1,47 @@
using Avalonia.Skia;
using SkiaSharp;
namespace Avalonia.Web.Blazor
{
internal class BlazorSkiaGpuRenderTarget : ISkiaGpuRenderTarget
{
private readonly GRBackendRenderTarget _renderTarget;
private readonly BlazorSkiaSurface _blazorSkiaSurface;
private readonly PixelSize _size;
public BlazorSkiaGpuRenderTarget(BlazorSkiaSurface blazorSkiaSurface)
{
_size = blazorSkiaSurface.Size;
var glFbInfo = new GRGlFramebufferInfo(blazorSkiaSurface.GlInfo.FboId, blazorSkiaSurface.ColorType.ToGlSizedFormat());
{
_blazorSkiaSurface = blazorSkiaSurface;
_renderTarget = new GRBackendRenderTarget(
(int)(blazorSkiaSurface.Size.Width * blazorSkiaSurface.Scaling),
(int)(blazorSkiaSurface.Size.Height * blazorSkiaSurface.Scaling),
blazorSkiaSurface.GlInfo.Samples,
blazorSkiaSurface.GlInfo.Stencils, glFbInfo);
}
}
public void Dispose()
{
_renderTarget.Dispose();
}
public ISkiaGpuRenderSession BeginRenderingSession()
{
return new BlazorSkiaGpuRenderSession(_blazorSkiaSurface, _renderTarget);
}
public bool IsCorrupted
{
get
{
var result = _size.Width != _renderTarget.Width || _size.Height != _renderTarget.Height;
return result;
}
}
}
}

30
src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs

@ -0,0 +1,30 @@
using Avalonia.Web.Blazor.Interop;
using SkiaSharp;
namespace Avalonia.Web.Blazor
{
internal class BlazorSkiaSurface
{
public BlazorSkiaSurface(GRContext context, SKHtmlCanvasInterop.GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling, GRSurfaceOrigin origin)
{
Context = context;
GlInfo = glInfo;
ColorType = colorType;
Size = size;
Scaling = scaling;
Origin = origin;
}
public SKColorType ColorType { get; set; }
public PixelSize Size { get; set; }
public GRContext Context { get; set; }
public GRSurfaceOrigin Origin { get; set; }
public double Scaling { get; set; }
public SKHtmlCanvasInterop.GLInfo GlInfo { get; set; }
}
}

78
src/Web/Avalonia.Web.Blazor/CustomFontManagerImpl.cs

@ -0,0 +1,78 @@
using System.Globalization;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Skia;
using SkiaSharp;
namespace Avalonia.Web.Blazor
{
public class CustomFontManagerImpl : IFontManagerImpl
{
private readonly Typeface[] _customTypefaces;
private readonly string _defaultFamilyName;
private readonly Typeface _defaultTypeface =
new Typeface("avares://Avalonia.Web.Blazor/Assets#Noto Mono");
private readonly Typeface _italicTypeface =
new Typeface("avares://Avalonia.Web.Blazor/Assets#Noto Sans");
public CustomFontManagerImpl()
{
_customTypefaces = new[] { _italicTypeface, _defaultTypeface };
_defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName;
}
public string GetDefaultFontFamilyName()
{
return _defaultFamilyName;
}
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
return _customTypefaces.Select(x => x.FontFamily.Name);
}
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily,
CultureInfo culture, out Typeface typeface)
{
foreach (var customTypeface in _customTypefaces)
{
if (customTypeface.GlyphTypeface.GetGlyph((uint)codepoint) == 0)
{
continue;
}
typeface = new Typeface(customTypeface.FontFamily, fontStyle, fontWeight);
return true;
}
typeface = _defaultTypeface;
return true;
}
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{
SKTypeface skTypeface;
switch (typeface.FontFamily.Name)
{
case "Noto Sans":
{
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily);
skTypeface = typefaceCollection.Get(typeface);
break;
}
default:
{
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily);
skTypeface = typefaceCollection.Get(_defaultTypeface);
break;
}
}
return new GlyphTypefaceImpl(skTypeface);
}
}
}

20
src/Web/Avalonia.Web.Blazor/Interop/ActionHelper.cs

@ -0,0 +1,20 @@
using System;
using System.ComponentModel;
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor.Interop
{
[EditorBrowsable(EditorBrowsableState.Never)]
public class ActionHelper
{
private readonly Action action;
public ActionHelper(Action action)
{
this.action = action;
}
[JSInvokable]
public void Invoke() => action?.Invoke();
}
}

87
src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs

@ -0,0 +1,87 @@
using System;
using System.Threading.Tasks;
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor.Interop
{
internal class DpiWatcherInterop : JSModuleInterop
{
private const string JsFilename = "./_content/Avalonia.Web.Blazor/DpiWatcher.js";
private const string StartSymbol = "DpiWatcher.start";
private const string StopSymbol = "DpiWatcher.stop";
private const string GetDpiSymbol = "DpiWatcher.getDpi";
private static DpiWatcherInterop? instance;
private event Action<double>? callbacksEvent;
private readonly FloatFloatActionHelper callbackHelper;
private DotNetObjectReference<FloatFloatActionHelper>? callbackReference;
public static async Task<DpiWatcherInterop> ImportAsync(IJSRuntime js, Action<double>? callback = null)
{
var interop = Get(js);
await interop.ImportAsync();
if (callback != null)
interop.Subscribe(callback);
return interop;
}
public static DpiWatcherInterop Get(IJSRuntime js) =>
instance ??= new DpiWatcherInterop(js);
private DpiWatcherInterop(IJSRuntime js)
: base(js, JsFilename)
{
callbackHelper = new FloatFloatActionHelper((o, n) => callbacksEvent?.Invoke(n));
}
protected override void OnDisposingModule() =>
Stop();
public void Subscribe(Action<double> callback)
{
var shouldStart = callbacksEvent == null;
callbacksEvent += callback;
var dpi = shouldStart
? Start()
: GetDpi();
callback(dpi);
}
public void Unsubscribe(Action<double> callback)
{
callbacksEvent -= callback;
if (callbacksEvent == null)
Stop();
}
private double Start()
{
if (callbackReference != null)
return GetDpi();
callbackReference = DotNetObjectReference.Create(callbackHelper);
return Invoke<double>(StartSymbol, callbackReference);
}
private void Stop()
{
if (callbackReference == null)
return;
Invoke(StopSymbol);
callbackReference?.Dispose();
callbackReference = null;
}
public double GetDpi() =>
Invoke<double>(GetDpiSymbol);
}
}

20
src/Web/Avalonia.Web.Blazor/Interop/FloatFloatActionHelper.cs

@ -0,0 +1,20 @@
using System;
using System.ComponentModel;
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor.Interop
{
[EditorBrowsable(EditorBrowsableState.Never)]
public class FloatFloatActionHelper
{
private readonly Action<float, float> action;
public FloatFloatActionHelper(Action<float, float> action)
{
this.action = action;
}
[JSInvokable]
public void Invoke(float width, float height) => action?.Invoke(width, height);
}
}

41
src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs

@ -0,0 +1,41 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using SkiaSharp;
namespace Avalonia.Web.Blazor.Interop
{
internal class InputHelperInterop : JSModuleInterop
{
private const string JsFilename = "./_content/Avalonia.Web.Blazor/InputHelper.js";
private const string ClearSymbol = "InputHelper.clear";
private const string FocusSymbol = "InputHelper.focus";
private const string SetCursorSymbol = "InputHelper.setCursor";
private const string HideSymbol = "InputHelper.hide";
private const string ShowSymbol = "InputHelper.show";
private readonly ElementReference inputElement;
public static async Task<InputHelperInterop> ImportAsync(IJSRuntime js, ElementReference element)
{
var interop = new InputHelperInterop(js, element);
await interop.ImportAsync();
return interop;
}
public InputHelperInterop(IJSRuntime js, ElementReference element)
: base(js, JsFilename)
{
inputElement = element;
}
public void Clear() => Invoke(ClearSymbol, inputElement);
public void Focus() => Invoke(FocusSymbol, inputElement);
public void SetCursor(string kind) => Invoke(SetCursorSymbol, inputElement, kind);
public void Hide() => Invoke(HideSymbol, inputElement);
public void Show() => Invoke(ShowSymbol, inputElement);
}
}

42
src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs

@ -0,0 +1,42 @@
using System;
using System.Threading.Tasks;
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor.Interop
{
internal class JSModuleInterop : IDisposable
{
private readonly Task<IJSUnmarshalledObjectReference> moduleTask;
private IJSUnmarshalledObjectReference? module;
public JSModuleInterop(IJSRuntime js, string filename)
{
if (js is not IJSInProcessRuntime)
throw new NotSupportedException("SkiaSharp currently only works on Web Assembly.");
moduleTask = js.InvokeAsync<IJSUnmarshalledObjectReference>("import", filename).AsTask();
}
public async Task ImportAsync()
{
module = await moduleTask;
}
public void Dispose()
{
OnDisposingModule();
Module.Dispose();
}
protected IJSUnmarshalledObjectReference Module =>
module ?? throw new InvalidOperationException("Make sure to run ImportAsync() first.");
protected void Invoke(string identifier, params object?[]? args) =>
Module.InvokeVoid(identifier, args);
protected TValue Invoke<TValue>(string identifier, params object?[]? args) =>
Module.Invoke<TValue>(identifier, args);
protected virtual void OnDisposingModule() { }
}
}

79
src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs

@ -0,0 +1,79 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using SkiaSharp;
namespace Avalonia.Web.Blazor.Interop
{
internal class SKHtmlCanvasInterop : JSModuleInterop
{
private const string JsFilename = "./_content/Avalonia.Web.Blazor/SKHtmlCanvas.js";
private const string InitGLSymbol = "SKHtmlCanvas.initGL";
private const string InitRasterSymbol = "SKHtmlCanvas.initRaster";
private const string DeinitSymbol = "SKHtmlCanvas.deinit";
private const string RequestAnimationFrameSymbol = "SKHtmlCanvas.requestAnimationFrame";
private const string PutImageDataSymbol = "SKHtmlCanvas.putImageData";
private readonly ElementReference htmlCanvas;
private readonly string htmlElementId;
private readonly ActionHelper callbackHelper;
private DotNetObjectReference<ActionHelper>? callbackReference;
public static async Task<SKHtmlCanvasInterop> ImportAsync(IJSRuntime js, ElementReference element, Action callback)
{
var interop = new SKHtmlCanvasInterop(js, element, callback);
await interop.ImportAsync();
return interop;
}
public SKHtmlCanvasInterop(IJSRuntime js, ElementReference element, Action renderFrameCallback)
: base(js, JsFilename)
{
htmlCanvas = element;
htmlElementId = element.Id;
callbackHelper = new ActionHelper(renderFrameCallback);
}
protected override void OnDisposingModule() =>
Deinit();
public GLInfo InitGL()
{
if (callbackReference != null)
throw new InvalidOperationException("Unable to initialize the same canvas more than once.");
callbackReference = DotNetObjectReference.Create(callbackHelper);
return Invoke<GLInfo>(InitGLSymbol, htmlCanvas, htmlElementId, callbackReference);
}
public bool InitRaster()
{
if (callbackReference != null)
throw new InvalidOperationException("Unable to initialize the same canvas more than once.");
callbackReference = DotNetObjectReference.Create(callbackHelper);
return Invoke<bool>(InitRasterSymbol, htmlCanvas, htmlElementId, callbackReference);
}
public void Deinit()
{
if (callbackReference == null)
return;
Invoke(DeinitSymbol, htmlElementId);
callbackReference?.Dispose();
}
public void RequestAnimationFrame(bool enableRenderLoop, int rawWidth, int rawHeight) =>
Invoke(RequestAnimationFrameSymbol, htmlCanvas, enableRenderLoop, rawWidth, rawHeight);
public void PutImageData(IntPtr intPtr, SKSizeI rawSize) =>
Invoke(PutImageDataSymbol, htmlCanvas, intPtr.ToInt64(), rawSize.Width, rawSize.Height);
public record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth);
}
}

61
src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs

@ -0,0 +1,61 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using SkiaSharp;
namespace Avalonia.Web.Blazor.Interop
{
internal class SizeWatcherInterop : JSModuleInterop
{
private const string JsFilename = "./_content/Avalonia.Web.Blazor/SizeWatcher.js";
private const string ObserveSymbol = "SizeWatcher.observe";
private const string UnobserveSymbol = "SizeWatcher.unobserve";
private readonly ElementReference htmlElement;
private readonly string htmlElementId;
private readonly FloatFloatActionHelper callbackHelper;
private DotNetObjectReference<FloatFloatActionHelper>? callbackReference;
public static async Task<SizeWatcherInterop> ImportAsync(IJSRuntime js, ElementReference element, Action<SKSize> callback)
{
var interop = new SizeWatcherInterop(js, element, callback);
await interop.ImportAsync();
interop.Start();
return interop;
}
public SizeWatcherInterop(IJSRuntime js, ElementReference element, Action<SKSize> callback)
: base(js, JsFilename)
{
htmlElement = element;
htmlElementId = element.Id;
callbackHelper = new FloatFloatActionHelper((x, y) => callback(new SKSize(x, y)));
}
protected override void OnDisposingModule() =>
Stop();
public void Start()
{
if (callbackReference != null)
return;
callbackReference = DotNetObjectReference.Create(callbackHelper);
Invoke(ObserveSymbol, htmlElement, htmlElementId, callbackReference);
}
public void Stop()
{
if (callbackReference == null)
return;
Invoke(UnobserveSymbol, htmlElementId);
callbackReference?.Dispose();
callbackReference = null;
}
}
}

41
src/Web/Avalonia.Web.Blazor/Interop/Typescript/DpiWatcher.ts

@ -0,0 +1,41 @@

export class DpiWatcher {
static lastDpi: number;
static timerId: number;
static callback: DotNet.DotNetObjectReference;
public static getDpi() {
return window.devicePixelRatio;
}
public static start(callback: DotNet.DotNetObjectReference): number {
//console.info(`Starting DPI watcher with callback ${callback._id}...`);
DpiWatcher.lastDpi = window.devicePixelRatio;
DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000);
DpiWatcher.callback = callback;
return DpiWatcher.lastDpi;
}
public static stop() {
//console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`);
window.clearInterval(DpiWatcher.timerId);
DpiWatcher.callback = undefined;
}
static update() {
if (!DpiWatcher.callback)
return;
const currentDpi = window.devicePixelRatio;
const lastDpi = DpiWatcher.lastDpi;
DpiWatcher.lastDpi = currentDpi;
if (Math.abs(lastDpi - currentDpi) > 0.001) {
DpiWatcher.callback.invokeMethod('Invoke', lastDpi, currentDpi);
}
}
}

23
src/Web/Avalonia.Web.Blazor/Interop/Typescript/InputHelper.ts

@ -0,0 +1,23 @@

export class InputHelper {
public static clear (inputElement: HTMLInputElement){
inputElement.value = "";
}
public static focus (inputElement: HTMLInputElement){
inputElement.focus();
inputElement.setSelectionRange(0, 0);
}
public static setCursor (inputElement: HTMLInputElement, kind: string) {
inputElement.style.cursor = kind;
}
public static hide (inputElement: HTMLInputElement){
inputElement.style.display = 'none';
}
public static show (inputElement: HTMLInputElement){
inputElement.style.display = 'block';
}
}

225
src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts

@ -0,0 +1,225 @@
// aliases for emscripten
declare let GL: any;
declare let GLctx: WebGLRenderingContext;
declare let Module: EmscriptenModule;
// container for gl info
type SKGLViewInfo = {
context: WebGLRenderingContext | WebGL2RenderingContext | undefined;
fboId: number;
stencil: number;
sample: number;
depth: number;
}
// alias for a potential skia html canvas
type SKHtmlCanvasElement = {
SKHtmlCanvas: SKHtmlCanvas
} & HTMLCanvasElement
export class SKHtmlCanvas {
static elements: Map<string, HTMLCanvasElement>;
htmlCanvas: HTMLCanvasElement;
glInfo: SKGLViewInfo;
renderFrameCallback: DotNet.DotNetObjectReference;
renderLoopEnabled: boolean = false;
renderLoopRequest: number = 0;
public static initGL(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): SKGLViewInfo {
var view = SKHtmlCanvas.init(true, element, elementId, callback);
if (!view || !view.glInfo)
return null;
return view.glInfo;
}
public static initRaster(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): boolean {
var view = SKHtmlCanvas.init(false, element, elementId, callback);
if (!view)
return false;
return true;
}
static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): SKHtmlCanvas {
var htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas) {
console.error(`No canvas element was provided.`);
return null;
}
if (!SKHtmlCanvas.elements)
SKHtmlCanvas.elements = new Map<string, HTMLCanvasElement>();
SKHtmlCanvas.elements[elementId] = element;
const view = new SKHtmlCanvas(useGL, element, callback);
htmlCanvas.SKHtmlCanvas = view;
return view;
}
public static deinit(elementId: string) {
if (!elementId)
return;
const element = SKHtmlCanvas.elements[elementId];
SKHtmlCanvas.elements.delete(elementId);
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.deinit();
htmlCanvas.SKHtmlCanvas = undefined;
}
public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean, width?: number, height?: number) {
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop, width, height);
}
public static setEnableRenderLoop(element: HTMLCanvasElement, enable: boolean) {
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.setEnableRenderLoop(enable);
}
public static putImageData(element: HTMLCanvasElement, pData: number, width: number, height: number) {
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.putImageData(pData, width, height);
}
public constructor(useGL: boolean, element: HTMLCanvasElement, callback: DotNet.DotNetObjectReference) {
this.htmlCanvas = element;
this.renderFrameCallback = callback;
if (useGL) {
const ctx = SKHtmlCanvas.createWebGLContext(this.htmlCanvas);
if (!ctx) {
console.error(`Failed to create WebGL context: err ${ctx}`);
return null;
}
// make current
GL.makeContextCurrent(ctx);
// read values
const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING);
this.glInfo = {
context: ctx,
fboId: fbo ? fbo.id : 0,
stencil: GLctx.getParameter(GLctx.STENCIL_BITS),
sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES)
depth: GLctx.getParameter(GLctx.DEPTH_BITS),
};
}
}
public deinit() {
this.setEnableRenderLoop(false);
}
public requestAnimationFrame(renderLoop?: boolean, width?: number, height?: number) {
// optionally update the render loop
if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop)
this.setEnableRenderLoop(renderLoop);
// make sure the canvas is scaled correctly for the drawing
if (width && height) {
this.htmlCanvas.width = width;
this.htmlCanvas.height = height;
}
// skip because we have a render loop
if (this.renderLoopRequest !== 0)
return;
// add the draw to the next frame
this.renderLoopRequest = window.requestAnimationFrame(() => {
if (this.glInfo) {
// make current
GL.makeContextCurrent(this.glInfo.context);
}
this.renderFrameCallback.invokeMethod('Invoke');
this.renderLoopRequest = 0;
// we may want to draw the next frame
if (this.renderLoopEnabled)
this.requestAnimationFrame();
});
}
public setEnableRenderLoop(enable: boolean) {
this.renderLoopEnabled = enable;
// either start the new frame or cancel the existing one
if (enable) {
//console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`);
this.requestAnimationFrame();
} else if (this.renderLoopRequest !== 0) {
window.cancelAnimationFrame(this.renderLoopRequest);
this.renderLoopRequest = 0;
}
}
public putImageData(pData: number, width: number, height: number): boolean {
if (this.glInfo || !pData || width <= 0 || width <= 0)
return false;
var ctx = this.htmlCanvas.getContext('2d');
if (!ctx) {
console.error(`Failed to obtain 2D canvas context.`);
return false;
}
// make sure the canvas is scaled correctly for the drawing
this.htmlCanvas.width = width;
this.htmlCanvas.height = height;
// set the canvas to be the bytes
var buffer = new Uint8ClampedArray(Module.HEAPU8.buffer, pData, width * height * 4);
var imageData = new ImageData(buffer, width, height);
ctx.putImageData(imageData, 0, 0);
return true;
}
static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext {
const contextAttributes = {
alpha: 1,
depth: 1,
stencil: 8,
antialias: 0,
premultipliedAlpha: 1,
preserveDrawingBuffer: 0,
preferLowPowerToHighPerformance: 0,
failIfMajorPerformanceCaveat: 0,
majorVersion: 2,
minorVersion: 0,
enableExtensionsByDefault: 1,
explicitSwapControl: 0,
renderViaOffscreenBackBuffer: 1,
};
let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes);
if (!ctx && contextAttributes.majorVersion > 1) {
console.warn('Falling back to WebGL 1.0');
contextAttributes.majorVersion = 1;
contextAttributes.minorVersion = 0;
ctx = GL.createContext(htmlCanvas, contextAttributes);
}
return ctx;
}
}

68
src/Web/Avalonia.Web.Blazor/Interop/Typescript/SizeWatcher.ts

@ -0,0 +1,68 @@

type SizeWatcherElement = {
SizeWatcher: SizeWatcherInstance;
} & HTMLElement
type SizeWatcherInstance = {
callback: DotNet.DotNetObjectReference;
}
export class SizeWatcher {
static observer: ResizeObserver;
static elements: Map<string, HTMLElement>;
public static observe(element: HTMLElement, elementId: string, callback: DotNet.DotNetObjectReference) {
if (!element || !callback)
return;
//console.info(`Adding size watcher observation with callback ${callback._id}...`);
SizeWatcher.init();
const watcherElement = element as SizeWatcherElement;
watcherElement.SizeWatcher = {
callback: callback
};
SizeWatcher.elements[elementId] = element;
SizeWatcher.observer.observe(element);
SizeWatcher.invoke(element);
}
public static unobserve(elementId: string) {
if (!elementId || !SizeWatcher.observer)
return;
//console.info('Removing size watcher observation...');
const element = SizeWatcher.elements[elementId];
SizeWatcher.elements.delete(elementId);
SizeWatcher.observer.unobserve(element);
}
static init() {
if (SizeWatcher.observer)
return;
//console.info('Starting size watcher...');
SizeWatcher.elements = new Map<string, HTMLElement>();
SizeWatcher.observer = new ResizeObserver((entries) => {
for (let entry of entries) {
SizeWatcher.invoke(entry.target);
}
});
}
static invoke(element: Element) {
const watcherElement = element as SizeWatcherElement;
const instance = watcherElement.SizeWatcher;
if (!instance || !instance.callback)
return;
return instance.callback.invokeMethod('Invoke', element.clientWidth, element.clientHeight);
}
}

7
src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/extras.d.ts

@ -0,0 +1,7 @@
declare namespace DotNet {
interface DotNetObjectReference extends DotNet.DotNetObject {
_id: number;
dispose();
}
}

56
src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/index.d.ts

@ -0,0 +1,56 @@
// Type definitions for non-npm package @blazor/javascript-interop 3.1
// Project: https://docs.microsoft.com/en-us/aspnet/core/blazor/javascript-interop?view=aspnetcore-3.1
// Definitions by: Piotr Błażejewicz (Peter Blazejewicz) <https://github.com/peterblazejewicz>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// Minimum TypeScript Version: 3.0
// Here be dragons!
// This is community-maintained definition file intended to ease the process of developing
// high quality JavaScript interop code to be used in Blazor application from your C# .Net code.
// Could be removed without a notice in case official definition types ships with Blazor itself.
// tslint:disable:no-unnecessary-generics
declare namespace DotNet {
/**
* Invokes the specified .NET public method synchronously. Not all hosting scenarios support
* synchronous invocation, so if possible use invokeMethodAsync instead.
*
* @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly containing the method.
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier.
* @param args Arguments to pass to the method, each of which must be JSON-serializable.
* @returns The result of the operation.
*/
function invokeMethod<T>(assemblyName: string, methodIdentifier: string, ...args: any[]): T;
/**
* Invokes the specified .NET public method asynchronously.
*
* @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly containing the method.
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier.
* @param args Arguments to pass to the method, each of which must be JSON-serializable.
* @returns A promise representing the result of the operation.
*/
function invokeMethodAsync<T>(assemblyName: string, methodIdentifier: string, ...args: any[]): Promise<T>;
/**
* Represents the .NET instance passed by reference to JavaScript.
*/
interface DotNetObject {
/**
* Invokes the specified .NET instance public method synchronously. Not all hosting scenarios support
* synchronous invocation, so if possible use invokeMethodAsync instead.
*
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier.
* @param args Arguments to pass to the method, each of which must be JSON-serializable.
* @returns The result of the operation.
*/
invokeMethod<T>(methodIdentifier: string, ...args: any[]): T;
/**
* Invokes the specified .NET instance public method asynchronously.
*
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier.
* @param args Arguments to pass to the method, each of which must be JSON-serializable.
* @returns A promise representing the result of the operation.
*/
invokeMethodAsync<T>(methodIdentifier: string, ...args: any[]): Promise<T>;
}
}

326
src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/emscripten/index.d.ts

@ -0,0 +1,326 @@
// Type definitions for Emscripten 1.39.16
// Project: https://emscripten.org
// Definitions by: Kensuke Matsuzaki <https://github.com/zakki>
// Periklis Tsirakidis <https://github.com/periklis>
// Bumsik Kim <https://github.com/kbumsik>
// Louis DeScioli <https://github.com/lourd>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.2
/** Other WebAssembly declarations, for compatibility with older versions of Typescript */
declare namespace WebAssembly {
interface Module {}
}
declare namespace Emscripten {
interface FileSystemType {}
type EnvironmentType = 'WEB' | 'NODE' | 'SHELL' | 'WORKER';
type JSType = 'number' | 'string' | 'array' | 'boolean';
type TypeCompatibleWithC = number | string | any[] | boolean;
type CIntType = 'i8' | 'i16' | 'i32' | 'i64';
type CFloatType = 'float' | 'double';
type CPointerType = 'i8*' | 'i16*' | 'i32*' | 'i64*' | 'float*' | 'double*' | '*';
type CType = CIntType | CFloatType | CPointerType;
type WebAssemblyImports = Array<{
name: string;
kind: string;
}>;
type WebAssemblyExports = Array<{
module: string;
name: string;
kind: string;
}>;
interface CCallOpts {
async?: boolean | undefined;
}
}
interface EmscriptenModule {
print(str: string): void;
printErr(str: string): void;
arguments: string[];
environment: Emscripten.EnvironmentType;
preInit: Array<{ (): void }>;
preRun: Array<{ (): void }>;
postRun: Array<{ (): void }>;
onAbort: { (what: any): void };
onRuntimeInitialized: { (): void };
preinitializedWebGLContext: WebGLRenderingContext;
noInitialRun: boolean;
noExitRuntime: boolean;
logReadFiles: boolean;
filePackagePrefixURL: string;
wasmBinary: ArrayBuffer;
destroy(object: object): void;
getPreloadedPackage(remotePackageName: string, remotePackageSize: number): ArrayBuffer;
instantiateWasm(
imports: Emscripten.WebAssemblyImports,
successCallback: (module: WebAssembly.Module) => void,
): Emscripten.WebAssemblyExports;
locateFile(url: string, scriptDirectory: string): string;
onCustomMessage(event: MessageEvent): void;
// USE_TYPED_ARRAYS == 1
HEAP: Int32Array;
IHEAP: Int32Array;
FHEAP: Float64Array;
// USE_TYPED_ARRAYS == 2
HEAP8: Int8Array;
HEAP16: Int16Array;
HEAP32: Int32Array;
HEAPU8: Uint8Array;
HEAPU16: Uint16Array;
HEAPU32: Uint32Array;
HEAPF32: Float32Array;
HEAPF64: Float64Array;
TOTAL_STACK: number;
TOTAL_MEMORY: number;
FAST_MEMORY: number;
addOnPreRun(cb: () => any): void;
addOnInit(cb: () => any): void;
addOnPreMain(cb: () => any): void;
addOnExit(cb: () => any): void;
addOnPostRun(cb: () => any): void;
preloadedImages: any;
preloadedAudios: any;
_malloc(size: number): number;
_free(ptr: number): void;
}
/**
* A factory function is generated when setting the `MODULARIZE` build option
* to `1` in your Emscripten build. It return a Promise that resolves to an
* initialized, ready-to-call `EmscriptenModule` instance.
*
* By default, the factory function will be named `Module`. It's recommended to
* use the `EXPORT_ES6` option, in which the factory function will be the
* default export. If used without `EXPORT_ES6`, the factory function will be a
* global variable. You can rename the variable using the `EXPORT_NAME` build
* option. It's left to you to declare any global variables as needed in your
* application's types.
* @param moduleOverrides Default properties for the initialized module.
*/
type EmscriptenModuleFactory<T extends EmscriptenModule = EmscriptenModule> = (
moduleOverrides?: Partial<T>,
) => Promise<T>;
declare namespace FS {
interface Lookup {
path: string;
node: FSNode;
}
interface FSStream {}
interface FSNode {}
interface ErrnoError {}
let ignorePermissions: boolean;
let trackingDelegate: any;
let tracking: any;
let genericErrors: any;
//
// paths
//
function lookupPath(path: string, opts: any): Lookup;
function getPath(node: FSNode): string;
//
// nodes
//
function isFile(mode: number): boolean;
function isDir(mode: number): boolean;
function isLink(mode: number): boolean;
function isChrdev(mode: number): boolean;
function isBlkdev(mode: number): boolean;
function isFIFO(mode: number): boolean;
function isSocket(mode: number): boolean;
//
// devices
//
function major(dev: number): number;
function minor(dev: number): number;
function makedev(ma: number, mi: number): number;
function registerDevice(dev: number, ops: any): void;
//
// core
//
function syncfs(populate: boolean, callback: (e: any) => any): void;
function syncfs(callback: (e: any) => any, populate?: boolean): void;
function mount(type: Emscripten.FileSystemType, opts: any, mountpoint: string): any;
function unmount(mountpoint: string): void;
function mkdir(path: string, mode?: number): any;
function mkdev(path: string, mode?: number, dev?: number): any;
function symlink(oldpath: string, newpath: string): any;
function rename(old_path: string, new_path: string): void;
function rmdir(path: string): void;
function readdir(path: string): any;
function unlink(path: string): void;
function readlink(path: string): string;
function stat(path: string, dontFollow?: boolean): any;
function lstat(path: string): any;
function chmod(path: string, mode: number, dontFollow?: boolean): void;
function lchmod(path: string, mode: number): void;
function fchmod(fd: number, mode: number): void;
function chown(path: string, uid: number, gid: number, dontFollow?: boolean): void;
function lchown(path: string, uid: number, gid: number): void;
function fchown(fd: number, uid: number, gid: number): void;
function truncate(path: string, len: number): void;
function ftruncate(fd: number, len: number): void;
function utime(path: string, atime: number, mtime: number): void;
function open(path: string, flags: string, mode?: number, fd_start?: number, fd_end?: number): FSStream;
function close(stream: FSStream): void;
function llseek(stream: FSStream, offset: number, whence: number): any;
function read(stream: FSStream, buffer: ArrayBufferView, offset: number, length: number, position?: number): number;
function write(
stream: FSStream,
buffer: ArrayBufferView,
offset: number,
length: number,
position?: number,
canOwn?: boolean,
): number;
function allocate(stream: FSStream, offset: number, length: number): void;
function mmap(
stream: FSStream,
buffer: ArrayBufferView,
offset: number,
length: number,
position: number,
prot: number,
flags: number,
): any;
function ioctl(stream: FSStream, cmd: any, arg: any): any;
function readFile(path: string, opts: { encoding: 'binary'; flags?: string | undefined }): Uint8Array;
function readFile(path: string, opts: { encoding: 'utf8'; flags?: string | undefined }): string;
function readFile(path: string, opts?: { flags?: string | undefined }): Uint8Array;
function writeFile(path: string, data: string | ArrayBufferView, opts?: { flags?: string | undefined }): void;
//
// module-level FS code
//
function cwd(): string;
function chdir(path: string): void;
function init(
input: null | (() => number | null),
output: null | ((c: number) => any),
error: null | ((c: number) => any),
): void;
function createLazyFile(
parent: string | FSNode,
name: string,
url: string,
canRead: boolean,
canWrite: boolean,
): FSNode;
function createPreloadedFile(
parent: string | FSNode,
name: string,
url: string,
canRead: boolean,
canWrite: boolean,
onload?: () => void,
onerror?: () => void,
dontCreateFile?: boolean,
canOwn?: boolean,
): void;
function createDataFile(
parent: string | FSNode,
name: string,
data: ArrayBufferView,
canRead: boolean,
canWrite: boolean,
canOwn: boolean,
): FSNode;
}
declare var MEMFS: Emscripten.FileSystemType;
declare var NODEFS: Emscripten.FileSystemType;
declare var IDBFS: Emscripten.FileSystemType;
// Below runtime function/variable declarations are exportable by
// -s EXTRA_EXPORTED_RUNTIME_METHODS. You can extend or merge
// EmscriptenModule interface to add runtime functions.
//
// For example, by using -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']"
// You can access ccall() via Module["ccall"]. In this case, you should
// extend EmscriptenModule to pass the compiler check like the following:
//
// interface YourOwnEmscriptenModule extends EmscriptenModule {
// ccall: typeof ccall;
// }
//
// See: https://emscripten.org/docs/getting_started/FAQ.html#why-do-i-get-typeerror-module-something-is-not-a-function
declare function ccall(
ident: string,
returnType: Emscripten.JSType | null,
argTypes: Emscripten.JSType[],
args: Emscripten.TypeCompatibleWithC[],
opts?: Emscripten.CCallOpts,
): any;
declare function cwrap(
ident: string,
returnType: Emscripten.JSType | null,
argTypes: Emscripten.JSType[],
opts?: Emscripten.CCallOpts,
): (...args: any[]) => any;
declare function setValue(ptr: number, value: any, type: Emscripten.CType, noSafe?: boolean): void;
declare function getValue(ptr: number, type: Emscripten.CType, noSafe?: boolean): number;
declare function allocate(
slab: number[] | ArrayBufferView | number,
types: Emscripten.CType | Emscripten.CType[],
allocator: number,
ptr?: number,
): number;
declare function stackAlloc(size: number): number;
declare function stackSave(): number;
declare function stackRestore(ptr: number): void;
declare function UTF8ToString(ptr: number, maxBytesToRead?: number): string;
declare function stringToUTF8(str: string, outPtr: number, maxBytesToRead?: number): void;
declare function lengthBytesUTF8(str: string): number;
declare function allocateUTF8(str: string): number;
declare function allocateUTF8OnStack(str: string): number;
declare function UTF16ToString(ptr: number): string;
declare function stringToUTF16(str: string, outPtr: number, maxBytesToRead?: number): void;
declare function lengthBytesUTF16(str: string): number;
declare function UTF32ToString(ptr: number): string;
declare function stringToUTF32(str: string, outPtr: number, maxBytesToRead?: number): void;
declare function lengthBytesUTF32(str: string): number;
declare function intArrayFromString(stringy: string, dontAddNull?: boolean, length?: number): number[];
declare function intArrayToString(array: number[]): string;
declare function writeStringToMemory(str: string, buffer: number, dontAddNull: boolean): void;
declare function writeArrayToMemory(array: number[], buffer: number): void;
declare function writeAsciiToMemory(str: string, buffer: number, dontAddNull: boolean): void;
declare function addRunDependency(id: any): void;
declare function removeRunDependency(id: any): void;
declare function addFunction(func: (...args: any[]) => any, signature?: string): number;
declare function removeFunction(funcPtr: number): void;
declare var ALLOC_NORMAL: number;
declare var ALLOC_STACK: number;
declare var ALLOC_STATIC: number;
declare var ALLOC_DYNAMIC: number;
declare var ALLOC_NONE: number;

127
src/Web/Avalonia.Web.Blazor/Keycodes.cs

@ -0,0 +1,127 @@
using Avalonia.Input;
namespace Avalonia.Web.Blazor
{
internal static class Keycodes
{
public static Dictionary<string, Key> KeyCodes = new()
{
{ "Escape", Key.Escape },
{ "Digit1", Key.D1 },
{ "Digit2", Key.D2 },
{ "Digit3", Key.D3 },
{ "Digit4", Key.D4 },
{ "Digit5", Key.D5 },
{ "Digit6", Key.D6 },
{ "Digit7", Key.D7 },
{ "Digit8", Key.D8 },
{ "Digit9", Key.D9 },
{ "Digit0", Key.D0 },
{ "Minus", Key.OemMinus },
//{ "Equal" , Key. },
{ "Backspace", Key.Back },
{ "Tab", Key.Tab },
{ "KeyQ", Key.Q },
{ "KeyW", Key.W },
{ "KeyE", Key.E },
{ "KeyR", Key.R },
{ "KeyT", Key.T },
{ "KeyY", Key.Y },
{ "KeyU", Key.U },
{ "KeyI", Key.I },
{ "KeyO", Key.O },
{ "KeyP", Key.P },
{ "BracketLeft", Key.OemOpenBrackets },
{ "BracketRight", Key.OemCloseBrackets },
{ "Enter", Key.Enter },
{ "ControlLeft", Key.LeftCtrl },
{ "KeyA", Key.A },
{ "KeyS", Key.S },
{ "KeyD", Key.D },
{ "KeyF", Key.F },
{ "KeyG", Key.G },
{ "KeyH", Key.H },
{ "KeyJ", Key.J },
{ "KeyK", Key.K },
{ "KeyL", Key.L },
{ "Semicolon", Key.OemSemicolon },
{ "Quote", Key.OemQuotes },
//{ "Backquote" , Key. },
{ "ShiftLeft", Key.LeftShift },
{ "Backslash", Key.OemBackslash },
{ "KeyZ", Key.Z },
{ "KeyX", Key.X },
{ "KeyC", Key.C },
{ "KeyV", Key.V },
{ "KeyB", Key.B },
{ "KeyN", Key.N },
{ "KeyM", Key.M },
{ "Comma", Key.OemComma },
{ "Period", Key.OemPeriod },
//{ "Slash" , Key. },
{ "ShiftRight", Key.RightShift },
{ "NumpadMultiply", Key.Multiply },
{ "AltLeft", Key.LeftAlt },
{ "Space", Key.Space },
{ "CapsLock", Key.CapsLock },
{ "F1", Key.F1 },
{ "F2", Key.F2 },
{ "F3", Key.F3 },
{ "F4", Key.F4 },
{ "F5", Key.F5 },
{ "F6", Key.F6 },
{ "F7", Key.F7 },
{ "F8", Key.F8 },
{ "F9", Key.F9 },
{ "F10", Key.F10 },
{ "NumLock", Key.NumLock },
{ "ScrollLock", Key.Scroll },
{ "Numpad7", Key.NumPad7 },
{ "Numpad8", Key.NumPad8 },
{ "Numpad9", Key.NumPad9 },
{ "NumpadSubtract", Key.Subtract },
{ "Numpad4", Key.NumPad4 },
{ "Numpad5", Key.NumPad5 },
{ "Numpad6", Key.NumPad6 },
{ "NumpadAdd", Key.Add },
{ "Numpad1", Key.NumPad1 },
{ "Numpad2", Key.NumPad2 },
{ "Numpad3", Key.NumPad3 },
{ "Numpad0", Key.NumPad0 },
{ "NumpadDecimal", Key.Decimal },
{ "Unidentified", Key.NoName },
//{ "IntlBackslash" , Key.bac },
{ "F11", Key.F11 },
{ "F12", Key.F12 },
//{ "IntlRo" , Key.Ro },
//{ "Unidentified" , Key. },
{ "Convert", Key.ImeConvert },
{ "KanaMode", Key.KanaMode },
{ "NonConvert", Key.ImeNonConvert },
//{ "Unidentified" , Key. },
{ "NumpadEnter", Key.Enter },
{ "ControlRight", Key.RightCtrl },
{ "NumpadDivide", Key.Divide },
{ "PrintScreen", Key.PrintScreen },
{ "AltRight", Key.RightAlt },
//{ "Unidentified" , Key. },
{ "Home", Key.Home },
{ "ArrowUp", Key.Up },
{ "PageUp", Key.PageUp },
{ "ArrowLeft", Key.Left },
{ "ArrowRight", Key.Right },
{ "End", Key.End },
{ "ArrowDown", Key.Down },
{ "PageDown", Key.PageDown },
{ "Insert", Key.Insert },
{ "Delete", Key.Delete },
//{ "Unidentified" , Key. },
{ "AudioVolumeMute", Key.VolumeMute },
{ "AudioVolumeDown", Key.VolumeDown },
{ "AudioVolumeUp", Key.VolumeUp },
//{ "NumpadEqual" , Key. },
{ "Pause", Key.Pause },
{ "NumpadComma", Key.OemComma }
};
}
}

16
src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs

@ -0,0 +1,16 @@
using System.Diagnostics;
using Avalonia.Rendering;
namespace Avalonia.Web.Blazor
{
public class ManualTriggerRenderTimer : IRenderTimer
{
private static readonly Stopwatch s_sw = Stopwatch.StartNew();
public static ManualTriggerRenderTimer Instance { get; } = new();
public void RaiseTick() => Tick?.Invoke(s_sw.Elapsed);
public event Action<TimeSpan>? Tick;
}
}

165
src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs

@ -0,0 +1,165 @@
using System.Diagnostics;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Web.Blazor.Interop;
using SkiaSharp;
#nullable enable
namespace Avalonia.Web.Blazor
{
internal class RazorViewTopLevelImpl : ITopLevelImplWithTextInputMethod
{
private Size _clientSize;
private BlazorSkiaSurface? _currentSurface;
private IInputRoot? _inputRoot;
private readonly Stopwatch _sw = Stopwatch.StartNew();
private readonly ITextInputMethodImpl _textInputMethod;
private readonly TouchDevice _touchDevice;
public RazorViewTopLevelImpl(ITextInputMethodImpl textInputMethod)
{
_textInputMethod = textInputMethod;
TransparencyLevel = WindowTransparencyLevel.None;
AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1);
_touchDevice = new TouchDevice();
}
public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds;
internal void SetSurface(GRContext context, SKHtmlCanvasInterop.GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling)
{
_currentSurface =
new BlazorSkiaSurface(context, glInfo, colorType, size, scaling, GRSurfaceOrigin.BottomLeft);
}
public void SetClientSize(SKSize size, double dpi)
{
var newSize = new Size(size.Width, size.Height);
if (newSize != _clientSize)
{
_clientSize = newSize;
if (_currentSurface is { })
{
_currentSurface.Size = new PixelSize((int)size.Width, (int)size.Height);
}
Resized?.Invoke(newSize, PlatformResizeReason.User);
}
}
public void RawTouchEvent(RawPointerEventType type, Point p, RawInputModifiers modifiers, long touchPointId)
{
if (_inputRoot is { })
{
Input?.Invoke(new RawTouchEventArgs(_touchDevice, Timestamp, _inputRoot, type, p, modifiers, touchPointId));
}
}
public void RawMouseEvent(RawPointerEventType type, Point p, RawInputModifiers modifiers)
{
if (_inputRoot is { })
{
Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, _inputRoot, type, p, modifiers));
}
}
public void RawMouseWheelEvent(Point p, Vector v, RawInputModifiers modifiers)
{
if (_inputRoot is { })
{
Input?.Invoke(new RawMouseWheelEventArgs(MouseDevice, Timestamp, _inputRoot, p, v, modifiers));
}
}
public void RawKeyboardEvent(RawKeyEventType type, string key, RawInputModifiers modifiers)
{
if (Keycodes.KeyCodes.TryGetValue(key, out var avkey))
{
if (_inputRoot is { })
{
Input?.Invoke(new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers));
}
}
}
public void RawTextEvent(string text)
{
if (_inputRoot is { })
{
Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice, Timestamp, _inputRoot, text));
}
}
public void Dispose()
{
}
public IRenderer CreateRenderer(IRenderRoot root)
{
var loop = AvaloniaLocator.Current.GetService<IRenderLoop>();
return new DeferredRenderer(root, loop);
}
public void Invalidate(Rect rect)
{
//Console.WriteLine("invalidate rect called");
}
public void SetInputRoot(IInputRoot inputRoot)
{
_inputRoot = inputRoot;
}
public Point PointToClient(PixelPoint point) => new Point(point.X, point.Y);
public PixelPoint PointToScreen(Point point) => new PixelPoint((int)point.X, (int)point.Y);
public void SetCursor(ICursorImpl cursor)
{
// nop
}
public IPopupImpl? CreatePopup()
{
return null;
}
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)
{
}
public Size ClientSize => _clientSize;
public Size? FrameSize => null;
public double RenderScaling => 1;
public IEnumerable<object> Surfaces => new object[] { _currentSurface! };
public Action<RawInputEventArgs>? Input { get; set; }
public Action<Rect>? Paint { get; set; }
public Action<Size, PlatformResizeReason>? Resized { get; set; }
public Action<double>? ScalingChanged { get; set; }
public Action<WindowTransparencyLevel>? TransparencyLevelChanged { get; set; }
public Action? Closed { get; set; }
public Action? LostFocus { get; set; }
public IMouseDevice MouseDevice { get; } = new MouseDevice();
public IKeyboardDevice KeyboardDevice { get; } = BlazorWindowingPlatform.Keyboard;
public WindowTransparencyLevel TransparencyLevel { get; }
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; }
public ITextInputMethodImpl TextInputMethod => _textInputMethod;
}
}

80
src/Web/Avalonia.Web.Blazor/WinStubs.cs

@ -0,0 +1,80 @@
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Platform;
#nullable enable
namespace Avalonia.Web.Blazor
{
internal class ClipboardStub : IClipboard
{
public Task<string> GetTextAsync() => Task.FromResult("");
public Task SetTextAsync(string text) => Task.CompletedTask;
public Task ClearAsync() => Task.CompletedTask;
public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask;
public Task<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>());
public Task<object> GetDataAsync(string format) => Task.FromResult<object>(new ());
}
internal class CursorStub : ICursorImpl
{
public void Dispose()
{
}
}
internal class CursorFactoryStub : ICursorFactory
{
public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot)
{
return new CursorStub();
}
ICursorImpl ICursorFactory.GetCursor(StandardCursorType cursorType)
{
return new CursorStub();
}
}
internal class IconLoaderStub : IPlatformIconLoader
{
private class IconStub : IWindowIconImpl
{
public void Save(Stream outputStream)
{
}
}
public IWindowIconImpl LoadIcon(string fileName) => new IconStub();
public IWindowIconImpl LoadIcon(Stream stream) => new IconStub();
public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) => new IconStub();
}
internal class SystemDialogsStub : ISystemDialogImpl
{
public Task<string[]?> ShowFileDialogAsync(FileDialog dialog, Window parent) =>
Task.FromResult((string[]?)null);
public Task<string?> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) =>
Task.FromResult((string?)null);
}
internal class ScreenStub : IScreenImpl
{
public int ScreenCount => 1;
public IReadOnlyList<Screen> AllScreens { get; } =
new[] { new Screen(96, new PixelRect(0, 0, 4000, 4000), new PixelRect(0, 0, 4000, 4000), true) };
}
}

100
src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs

@ -0,0 +1,100 @@
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Threading;
#nullable enable
namespace Avalonia.Web.Blazor
{
public class BlazorWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface
{
private bool _signaled;
private static int s_uiThreadId = -1;
public IWindowImpl CreateWindow() => throw new NotSupportedException();
IWindowImpl IWindowingPlatform.CreateEmbeddableWindow()
{
throw new NotImplementedException();
}
public ITrayIconImpl? CreateTrayIcon()
{
return null;
}
public static KeyboardDevice Keyboard { get; private set; }
public static void Register()
{
var instance = new BlazorWindowingPlatform();
Keyboard = new KeyboardDevice();
AvaloniaLocator.CurrentMutable
.Bind<IClipboard>().ToSingleton<ClipboardStub>()
.Bind<ICursorFactory>().ToSingleton<CursorFactoryStub>()
.Bind<IKeyboardDevice>().ToConstant(Keyboard)
.Bind<IPlatformSettings>().ToConstant(instance)
.Bind<IPlatformThreadingInterface>().ToConstant(instance)
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<IRenderTimer>().ToConstant(ManualTriggerRenderTimer.Instance)
.Bind<ISystemDialogImpl>().ToSingleton<SystemDialogsStub>()
.Bind<IWindowingPlatform>().ToConstant(instance)
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();
}
public Size DoubleClickSize { get; } = new Size(2, 2);
public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500);
public void RunLoop(CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
{
return AvaloniaLocator.Current.GetService<IRuntimePlatform>()
.StartSystemTimer(interval, () =>
{
Dispatcher.UIThread.RunJobs(priority);
tick();
});
}
public void Signal(DispatcherPriority priority)
{
if (_signaled)
return;
_signaled = true;
IDisposable? disp = null;
disp = AvaloniaLocator.Current.GetService<IRuntimePlatform>()
.StartSystemTimer(TimeSpan.FromMilliseconds(1),
() =>
{
_signaled = false;
disp?.Dispose();
Signaled?.Invoke(null);
});
}
public bool CurrentThreadIsLoopThread
{
get
{
return true; // Blazor is single threaded.
}
}
public event Action<DispatcherPriority?>? Signaled;
}
}

1
src/Web/Avalonia.Web.Blazor/_Imports.razor

@ -0,0 +1 @@
@using Microsoft.AspNetCore.Components.Web

14
src/Web/Avalonia.Web.Blazor/tsconfig.json

@ -0,0 +1,14 @@
{
"compilerOptions": {
"noImplicitAny": false,
"noEmitOnError": true,
"removeComments": false,
"sourceMap": true,
"target": "ES2020",
"module": "ES2020",
"outDir": "wwwroot"
},
"exclude": [
"node_modules"
]
}
Loading…
Cancel
Save