Browse Source

Merge pull request #9142 from AvaloniaUI/wasm-updates

Remove old blazor backend. Keep blazor components with new WASM implementation.
pull/9161/head
Dan Walmsley 3 years ago
committed by GitHub
parent
commit
0dd39b8421
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 36
      Avalonia.sln
  2. 0
      samples/ControlCatalog.Blazor.Web/App.razor
  3. 17
      samples/ControlCatalog.Blazor.Web/App.razor.cs
  4. 29
      samples/ControlCatalog.Blazor.Web/ControlCatalog.Blazor.Web.csproj
  5. 0
      samples/ControlCatalog.Blazor.Web/Pages/Index.razor
  6. 29
      samples/ControlCatalog.Blazor.Web/Program.cs
  7. 0
      samples/ControlCatalog.Blazor.Web/Properties/launchSettings.json
  8. 0
      samples/ControlCatalog.Blazor.Web/Shared/MainLayout.razor
  9. 3
      samples/ControlCatalog.Blazor.Web/_Imports.razor
  10. 0
      samples/ControlCatalog.Blazor.Web/wwwroot/css/app.css
  11. 0
      samples/ControlCatalog.Blazor.Web/wwwroot/favicon.ico
  12. 0
      samples/ControlCatalog.Blazor.Web/wwwroot/index.html
  13. 20
      samples/ControlCatalog.Web/App.razor.cs
  14. 68
      samples/ControlCatalog.Web/ControlCatalog.Web.csproj
  15. 32
      samples/ControlCatalog.Web/EmbedSample.Browser.cs
  16. 0
      samples/ControlCatalog.Web/Logo.svg
  17. 34
      samples/ControlCatalog.Web/Program.cs
  18. 6
      samples/ControlCatalog.Web/Roots.xml
  19. 23
      samples/ControlCatalog.Web/app.css
  20. 0
      samples/ControlCatalog.Web/embed.js
  21. 0
      samples/ControlCatalog.Web/favicon.ico
  22. 2
      samples/ControlCatalog.Web/index.html
  23. 4
      samples/ControlCatalog.Web/main.js
  24. 0
      samples/ControlCatalog.Web/runtimeconfig.template.json
  25. 7
      src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.CompilationTuning.props
  26. 53
      src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj
  27. 4
      src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.props
  28. 6
      src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.targets
  29. 20
      src/Web/Avalonia.Web.Blazor/AvaloniaBlazorAppBuilder.cs
  30. 44
      src/Web/Avalonia.Web.Blazor/AvaloniaView.cs
  31. 67
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor
  32. 500
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
  33. 48
      src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs
  34. 25
      src/Web/Avalonia.Web.Blazor/BlazorSkiaGpu.cs
  35. 37
      src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderSession.cs
  36. 39
      src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs
  37. 87
      src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs
  38. 30
      src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs
  39. 34
      src/Web/Avalonia.Web.Blazor/ClipboardImpl.cs
  40. 93
      src/Web/Avalonia.Web.Blazor/Cursor.cs
  41. 9
      src/Web/Avalonia.Web.Blazor/IBlazorSkiaSurface.cs
  42. 81
      src/Web/Avalonia.Web.Blazor/Interop/ActionHelper.cs
  43. 18
      src/Web/Avalonia.Web.Blazor/Interop/AvaloniaModule.cs
  44. 72
      src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs
  45. 22
      src/Web/Avalonia.Web.Blazor/Interop/FocusHelperInterop.cs
  46. 130
      src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs
  47. 46
      src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs
  48. 144
      src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs
  49. 76
      src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs
  50. 50
      src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs
  51. 225
      src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs
  52. 124
      src/Web/Avalonia.Web.Blazor/Interop/Storage/WriteableStream.cs
  53. 35
      src/Web/Avalonia.Web.Blazor/JSObjectControlHandle.cs
  54. 127
      src/Web/Avalonia.Web.Blazor/Keycodes.cs
  55. 17
      src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs
  56. 222
      src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs
  57. 50
      src/Web/Avalonia.Web.Blazor/WinStubs.cs
  58. 106
      src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs
  59. 1
      src/Web/Avalonia.Web.Blazor/_Imports.razor
  60. 16
      src/Web/Avalonia.Web.Blazor/webapp/build.js
  61. 7
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia.ts
  62. 149
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.ts
  63. 40
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/DpiWatcher.ts
  64. 9
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/FocusHelper.ts
  65. 86
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts
  66. 61
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/NativeControlHost.ts
  67. 255
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SKHtmlCanvas.ts
  68. 67
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SizeWatcher.ts
  69. 1
      src/Web/Avalonia.Web.Blazor/webapp/modules/Storage.ts
  70. 79
      src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/IndexedDbWrapper.ts
  71. 204
      src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts
  72. 13
      src/Web/Avalonia.Web.Blazor/webapp/package.json
  73. 18
      src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json
  74. 56
      src/Web/Avalonia.Web.Blazor/webapp/types/dotnet/index.d.ts
  75. 41
      src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj
  76. 44
      src/Web/Avalonia.Web.Sample/EmbedSample.Browser.cs
  77. 19
      src/Web/Avalonia.Web.Sample/Program.cs
  78. 8
      src/Web/Avalonia.Web/Avalonia.Web.csproj
  79. 22
      src/Web/Avalonia.Web/AvaloniaView.cs
  80. 71
      src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs
  81. 2
      src/Web/Avalonia.Web/Cursor.cs
  82. 22
      src/Web/Avalonia.Web/Interop/AvaloniaModule.cs
  83. 6
      src/Web/Avalonia.Web/Interop/CanvasHelper.cs
  84. 10
      src/Web/Avalonia.Web/Interop/DomHelper.cs
  85. 24
      src/Web/Avalonia.Web/Interop/InputHelper.cs
  86. 14
      src/Web/Avalonia.Web/Interop/NativeControlHostHelper.cs
  87. 30
      src/Web/Avalonia.Web/Interop/StorageHelper.cs
  88. 16
      src/Web/Avalonia.Web/Interop/StreamHelper.cs
  89. 2
      src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs
  90. 2
      src/Web/Avalonia.Web/Storage/BlobReadableStream.cs
  91. 12
      src/Web/Avalonia.Web/Storage/BrowserStorageProvider.cs
  92. 2
      src/Web/Avalonia.Web/Storage/WriteableStream.cs
  93. 2
      src/Web/Avalonia.Web/WindowingPlatform.cs
  94. 16
      src/Web/Avalonia.Web/webapp/modules/avalonia.ts
  95. 4
      src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts
  96. 7
      src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts

36
Avalonia.sln

@ -198,8 +198,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{86A3F706-DC3
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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsInteropTest", "samples\interop\WindowsInteropTest\WindowsInteropTest.csproj", "{26A98DA1-D89D-4A95-8152-349F404DA2E2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlSamples", "samples\SampleControls\ControlSamples.csproj", "{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}"
@ -216,15 +214,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevGenerators", "src\tools\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Web", "src\Web\Avalonia.Web\Avalonia.Web.csproj", "{76D39FF6-6B4F-46C4-93CD-E6FC4665739E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Web.Sample", "src\Web\Avalonia.Web.Sample\Avalonia.Web.Sample.csproj", "{1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MobileSandbox", "samples\MobileSandbox\MobileSandbox.csproj", "{3B8519C1-2F51-4F12-A348-120AB91D4532}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MobileSandbox.Android", "samples\MobileSandbox.Android\MobileSandbox.Android.csproj", "{C90FE60B-B01E-4F35-91D6-379D6966030F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MobileSandbox", "samples\MobileSandbox\MobileSandbox.csproj", "{3B8519C1-2F51-4F12-A348-120AB91D4532}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MobileSandbox.iOS", "samples\MobileSandbox.iOS\MobileSandbox.iOS.csproj", "{FED9A71D-00D7-4F40-A9E4-1229EEA28EEB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MobileSandbox.Android", "samples\MobileSandbox.Android\MobileSandbox.Android.csproj", "{C90FE60B-B01E-4F35-91D6-379D6966030F}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MobileSandbox.Desktop", "samples\MobileSandbox.Desktop\MobileSandbox.Desktop.csproj", "{62D392C9-81CF-487F-92E8-598B2AF3FDCE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MobileSandbox.iOS", "samples\MobileSandbox.iOS\MobileSandbox.iOS.csproj", "{FED9A71D-00D7-4F40-A9E4-1229EEA28EEB}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Blazor.Web", "samples\ControlCatalog.Blazor.Web\ControlCatalog.Blazor.Web.csproj", "{6A710364-AE6D-40BD-968B-024311527AC2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MobileSandbox.Desktop", "samples\MobileSandbox.Desktop\MobileSandbox.Desktop.csproj", "{62D392C9-81CF-487F-92E8-598B2AF3FDCE}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Web", "samples\ControlCatalog.Web\ControlCatalog.Web.csproj", "{8B3E8405-DE18-4048-A459-9CA4AC3319A2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -480,10 +480,6 @@ Global
{25831348-EB2A-483E-9576-E8F6528674A5}.Debug|Any CPU.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
{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}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C08E9894-AA92-426E-BF56-033E262CAD3E}.Release|Any CPU.Build.0 = Release|Any CPU
{26A98DA1-D89D-4A95-8152-349F404DA2E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{26A98DA1-D89D-4A95-8152-349F404DA2E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{26A98DA1-D89D-4A95-8152-349F404DA2E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -516,10 +512,6 @@ Global
{76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Release|Any CPU.Build.0 = Release|Any CPU
{1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}.Release|Any CPU.Build.0 = Release|Any CPU
{3B8519C1-2F51-4F12-A348-120AB91D4532}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3B8519C1-2F51-4F12-A348-120AB91D4532}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B8519C1-2F51-4F12-A348-120AB91D4532}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -536,6 +528,14 @@ Global
{62D392C9-81CF-487F-92E8-598B2AF3FDCE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{62D392C9-81CF-487F-92E8-598B2AF3FDCE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{62D392C9-81CF-487F-92E8-598B2AF3FDCE}.Release|Any CPU.Build.0 = Release|Any CPU
{6A710364-AE6D-40BD-968B-024311527AC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6A710364-AE6D-40BD-968B-024311527AC2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6A710364-AE6D-40BD-968B-024311527AC2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6A710364-AE6D-40BD-968B-024311527AC2}.Release|Any CPU.Build.0 = Release|Any CPU
{8B3E8405-DE18-4048-A459-9CA4AC3319A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8B3E8405-DE18-4048-A459-9CA4AC3319A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B3E8405-DE18-4048-A459-9CA4AC3319A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B3E8405-DE18-4048-A459-9CA4AC3319A2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -579,6 +579,7 @@ Global
{41B02319-965D-4945-8005-C1A3D1224165} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
{351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C}
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098}
@ -586,7 +587,6 @@ Global
{676D6BFD-029D-4E43-BFC7-3892265CE251} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{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}
{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
@ -594,12 +594,12 @@ Global
{EABE2161-989B-42BF-BD8D-1E34B20C21F1} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{76D39FF6-6B4F-46C4-93CD-E6FC4665739E} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}
{1F61B6F1-B881-4E27-A5B0-09D1F543F7AC} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}
{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
{3B8519C1-2F51-4F12-A348-120AB91D4532} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C90FE60B-B01E-4F35-91D6-379D6966030F} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{FED9A71D-00D7-4F40-A9E4-1229EEA28EEB} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{62D392C9-81CF-487F-92E8-598B2AF3FDCE} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{6A710364-AE6D-40BD-968B-024311527AC2} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{8B3E8405-DE18-4048-A459-9CA4AC3319A2} = {9B9E3891-2366-4253-A952-D08BCEB71098}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

0
samples/ControlCatalog.Web/App.razor → samples/ControlCatalog.Blazor.Web/App.razor

17
samples/ControlCatalog.Blazor.Web/App.razor.cs

@ -0,0 +1,17 @@
using Avalonia;
using Avalonia.Web.Blazor;
namespace ControlCatalog.Blazor.Web;
public partial class App
{
protected override void OnParametersSet()
{
AppBuilder.Configure<ControlCatalog.App>()
.UseBlazor()
// .With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering
.SetupWithSingleViewLifetime();
base.OnParametersSet();
}
}

29
samples/ControlCatalog.Blazor.Web/ControlCatalog.Blazor.Web.csproj

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<Nullable>enable</Nullable>
<EmccTotalMemory>16777216</EmccTotalMemory>
<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
<BlazorWebAssemblyPreserveCollationData>false</BlazorWebAssemblyPreserveCollationData>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.0-rc.1.22427.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.0-rc.1.22427.2" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
<ProjectReference Include="..\..\src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.csproj" />
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
</ItemGroup>
<Import Project="..\..\build\ReferenceCoreLibraries.props" />
<Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\src\Web\Avalonia.Web\Avalonia.Web.props" />
<Import Project="..\..\src\Web\Avalonia.Web\Avalonia.Web.targets" />
</Project>

0
samples/ControlCatalog.Web/Pages/Index.razor → samples/ControlCatalog.Blazor.Web/Pages/Index.razor

29
samples/ControlCatalog.Blazor.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.Blazor.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;
}
}

0
samples/ControlCatalog.Web/Properties/launchSettings.json → samples/ControlCatalog.Blazor.Web/Properties/launchSettings.json

0
samples/ControlCatalog.Web/Shared/MainLayout.razor → samples/ControlCatalog.Blazor.Web/Shared/MainLayout.razor

3
samples/ControlCatalog.Web/_Imports.razor → samples/ControlCatalog.Blazor.Web/_Imports.razor

@ -6,6 +6,5 @@
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using ControlCatalog.Web
@using ControlCatalog.Web.Shared
@using ControlCatalog.Blazor.Web.Shared
@using SkiaSharp

0
samples/ControlCatalog.Web/wwwroot/css/app.css → samples/ControlCatalog.Blazor.Web/wwwroot/css/app.css

0
samples/ControlCatalog.Web/wwwroot/favicon.ico → samples/ControlCatalog.Blazor.Web/wwwroot/favicon.ico

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 172 KiB

0
samples/ControlCatalog.Web/wwwroot/index.html → samples/ControlCatalog.Blazor.Web/wwwroot/index.html

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

@ -1,20 +0,0 @@
using Avalonia;
using Avalonia.Web.Blazor;
namespace ControlCatalog.Web;
public partial class App
{
protected override void OnParametersSet()
{
WebAppBuilder.Configure<ControlCatalog.App>()
.AfterSetup(_ =>
{
ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb();
})
.With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering
.SetupWithSingleViewLifetime();
base.OnParametersSet();
}
}

68
samples/ControlCatalog.Web/ControlCatalog.Web.csproj

@ -1,57 +1,45 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<!--Temporal hack that fixes compilation in VS-->
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<EmccTotalMemory>16777216</EmccTotalMemory>
<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
<BlazorWebAssemblyPreserveCollationData>false</BlazorWebAssemblyPreserveCollationData>
<TargetFramework>net7.0</TargetFramework>
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<WasmMainJSPath>main.js</WasmMainJSPath>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver>
<WasmBuildNative>true</WasmBuildNative>
<EmccFlags>-sVERBOSE -sERROR_ON_UNDEFINED_SYMBOLS=0</EmccFlags>
</PropertyGroup>
<!-- 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>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<RunAOTCompilation>true</RunAOTCompilation>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>
<WasmBuildNative>true</WasmBuildNative>
<InvariantGlobalization>true</InvariantGlobalization>
<WasmEnableSIMD>true</WasmEnableSIMD>
<EmccCompileOptimizationFlag>-O3</EmccCompileOptimizationFlag>
<EmccLinkOptimizationFlag>-O3</EmccLinkOptimizationFlag>
<RunAOTCompilation>false</RunAOTCompilation>
<DebuggerSupport>false</DebuggerSupport>
<EnableUnsafeBinaryFormatterSerialization>false</EnableUnsafeBinaryFormatterSerialization>
<EnableUnsafeUTF7Encoding>false</EnableUnsafeUTF7Encoding>
<EventSourceSupport>false</EventSourceSupport>
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
<InvariantGlobalization>true</InvariantGlobalization>
<MetadataUpdaterSupport>false</MetadataUpdaterSupport>
<UseNativeHttpHandler>true</UseNativeHttpHandler>
<UseSystemResourceKeys>true</UseSystemResourceKeys>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>link</TrimMode>
<TrimmerRemoveSymbols>true</TrimmerRemoveSymbols>
</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" />
<TrimmerRootDescriptor Include="Roots.xml" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
<ProjectReference Include="..\..\src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.csproj" />
<ProjectReference Include="..\..\src\Web\Avalonia.Web\Avalonia.Web.csproj" />
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
</ItemGroup>
<Import Project="..\..\build\ReferenceCoreLibraries.props" />
<Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.targets" />
<Import Project="..\..\src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.CompilationTuning.props" />
<ItemGroup>
<WasmExtraFilesToDeploy Include="index.html" />
<WasmExtraFilesToDeploy Include="main.js" />
<WasmExtraFilesToDeploy Include="embed.js" />
<WasmExtraFilesToDeploy Include="favicon.ico" />
<WasmExtraFilesToDeploy Include="Logo.svg" />
<WasmExtraFilesToDeploy Include="app.css" />
</ItemGroup>
<Import Project="..\..\src\Web\Avalonia.Web\Avalonia.Web.props" />
<Import Project="..\..\src\Web\Avalonia.Web\Avalonia.Web.targets" />
</Project>

32
samples/ControlCatalog.Web/EmbedSample.Browser.cs

@ -1,34 +1,42 @@
using System;
using Avalonia;
using System.Runtime.InteropServices.JavaScript;
using Avalonia.Platform;
using Avalonia.Web.Blazor;
using Avalonia.Web;
using ControlCatalog.Pages;
using Microsoft.JSInterop;
namespace ControlCatalog.Web;
public class EmbedSampleWeb : INativeDemoControl
{
public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault)
{
var runtime = AvaloniaLocator.Current.GetRequiredService<IJSInProcessRuntime>();
if (isSecond)
{
var iframe = runtime.Invoke<IJSInProcessObjectReference>("document.createElement", "iframe");
iframe.InvokeVoid("setAttribute", "src", "https://www.youtube.com/embed/kZCIporjJ70");
var iframe = EmbedInterop.CreateElement("iframe");
iframe.SetProperty("src", "https://www.youtube.com/embed/kZCIporjJ70");
return new JSObjectControlHandle(iframe);
}
else
{
// window.createAppButton source is defined in "app.js" file.
var button = runtime.Invoke<IJSInProcessObjectReference>("window.createAppButton");
var defaultHandle = (JSObjectControlHandle)createDefault();
_ = JSHost.ImportAsync("embed.js", "./embed.js").ContinueWith(_ =>
{
EmbedInterop.AddAppButton(defaultHandle.Object);
});
return new JSObjectControlHandle(button);
return defaultHandle;
}
}
}
internal static partial class EmbedInterop
{
[JSImport("globalThis.document.createElement")]
public static partial JSObject CreateElement(string tagName);
[JSImport("addAppButton", "embed.js")]
public static partial void AddAppButton(JSObject parentObject);
}

0
src/Web/Avalonia.Web.Sample/Logo.svg → samples/ControlCatalog.Web/Logo.svg

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

34
samples/ControlCatalog.Web/Program.cs

@ -1,29 +1,19 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Avalonia;
using Avalonia.Web;
using ControlCatalog;
using ControlCatalog.Web;
public class Program
internal partial class Program
{
public static async Task Main(string[] args)
private static void Main(string[] args)
{
await CreateHostBuilder(args).Build().RunAsync();
BuildAvaloniaApp()
.AfterSetup(_ =>
{
ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb();
}).SetupBrowserApp("out");
}
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;
}
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>();
}

6
samples/ControlCatalog.Web/Roots.xml

@ -0,0 +1,6 @@
<linker>
<assembly fullname="ControlCatalog" preserve="All" />
<assembly fullname="ControlCatalog.Web" preserve="All" />
<assembly fullname="Avalonia.Themes.Fluent" preserve="All" />
<assembly fullname="Avalonia.Themes.Simple" preserve="All" />
</linker>

23
src/Web/Avalonia.Web.Sample/app.css → samples/ControlCatalog.Web/app.css

@ -4,12 +4,15 @@
}
#avalonia-splash {
position: absolute;
position: relative;
height: 100%;
width: 100%;
color: whitesmoke;
background: #171C2C;
font-family: 'Nunito', sans-serif;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
}
#avalonia-splash a{
@ -24,15 +27,23 @@
}
.splash-close {
animation: fadeOut 1s forwards;
animation: slide 0.5s linear 1s forwards;
}
@keyframes fadeOut {
from {
opacity: 1;
@keyframes slide {
0% {
top: 0%;
}
to {
50% {
opacity: 80%;
}
100% {
top: 100%;
overflow: hidden;
opacity: 0;
display: none;
visibility: collapse;
}
}

0
src/Web/Avalonia.Web.Sample/embed.js → samples/ControlCatalog.Web/embed.js

0
src/Web/Avalonia.Web.Sample/favicon.ico → samples/ControlCatalog.Web/favicon.ico

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 172 KiB

2
src/Web/Avalonia.Web.Sample/index.html → samples/ControlCatalog.Web/index.html

@ -4,7 +4,7 @@
<html>
<head>
<title>Avalonia.Web.Sample</title>
<title>AvaloniaUI - ControlCatalog</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="modulepreload" href="./main.js" />

4
src/Web/Avalonia.Web.Sample/main.js → samples/ControlCatalog.Web/main.js

@ -2,7 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
import { dotnet } from './dotnet.js'
import { createAvaloniaRuntime } from './avalonia.js';
import { registerAvaloniaModule } from './avalonia.js';
const is_browser = typeof window != "undefined";
if (!is_browser) throw new Error(`Expected to be running in a browser`);
@ -12,7 +12,7 @@ const dotnetRuntime = await dotnet
.withApplicationArgumentsFromQuery()
.create();
await createAvaloniaRuntime(dotnetRuntime);
await registerAvaloniaModule(dotnetRuntime);
const config = dotnetRuntime.getConfig();

0
src/Web/Avalonia.Web.Sample/runtimeconfig.template.json → samples/ControlCatalog.Web/runtimeconfig.template.json

7
src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.CompilationTuning.props

@ -1,7 +0,0 @@
<Project>
<PropertyGroup>
<EmccTotalMemory>16777216</EmccTotalMemory>
<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
<BlazorWebAssemblyPreserveCollationData>false</BlazorWebAssemblyPreserveCollationData>
</PropertyGroup>
</Project>

53
src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj

@ -1,53 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<TargetFramework>net7.0</TargetFramework>
<PackageId>Avalonia.Web.Blazor</PackageId>
<LangVersion>preview</LangVersion>
<MSBuildEnableWorkloadResolver>false</MSBuildEnableWorkloadResolver>
<StaticWebAssetsDisableProjectBuildPropsFileGeneration>true</StaticWebAssetsDisableProjectBuildPropsFileGeneration>
<ResolveStaticWebAssetsInputsDependsOn>_IncludeGeneratedAvaloniaStaticFiles;$(ResolveStaticWebAssetsInputsDependsOn)</ResolveStaticWebAssetsInputsDependsOn>
</PropertyGroup>
<ItemGroup>
<SupportedPlatform Include="browser" />
</ItemGroup>
<Import Project="..\..\..\build\BuildTargets.targets" />
<Import Project="..\..\..\build\SkiaSharp.props" />
<Import Project="..\..\..\build\HarfBuzzSharp.props" />
<Import Project="..\..\..\build\NullableEnable.props" />
<ItemGroup>
<Content Include="*.props">
<Pack>true</Pack>
<PackagePath>build\</PackagePath>
</Content>
<Content Include="*.targets">
<Pack>true</Pack>
<PackagePath>build\;buildTransitive\</PackagePath>
</Content>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="7.0.0-*" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.8" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
<ProjectReference Include="..\Avalonia.Web\Avalonia.Web.csproj" />
</ItemGroup>
<Target Name="NpmInstall" Inputs="webapp/package.json" Outputs="webapp/node_modules/.install-stamp">
<Exec Command="npm install" WorkingDirectory="webapp" />
<!-- Write the stamp file, so incremental builds work -->
<Touch Files="webapp/node_modules/.install-stamp" AlwaysCreate="true" />
</Target>
<Target Name="NpmRunBuild" DependsOnTargets="NpmInstall" BeforeTargets="BeforeBuild">
<Exec Command="npm run build" WorkingDirectory="webapp" />
<Target Name="_IncludeGeneratedAvaloniaStaticFiles">
<ItemGroup>
<_AvaloniaWebAssets Include="$(MSBuildThisFileDirectory)../Avalonia.Web/wwwroot/**/*.*" />
</ItemGroup>
<DefineStaticWebAssets SourceId="$(PackageId)"
SourceType="Computed"
AssetKind="All"
AssetRole="Primary"
CopyToOutputDirectory="PreserveNewest"
CopyToPublishDirectory="PreserveNewest"
ContentRoot="$(MSBuildThisFileDirectory)../Avalonia.Web/wwwroot/"
BasePath="_content\$(PackageId)"
CandidateAssets="@(_AvaloniaWebAssets)"
RelativePathFilter="**.js">
<Output TaskParameter="Assets" ItemName="StaticWebAsset" />
</DefineStaticWebAssets>
</Target>
</Project>

4
src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.props

@ -1,4 +0,0 @@
<Project>
<Import Project="Microsoft.AspNetCore.StaticWebAssets.props" />
<Import Project="Avalonia.Web.Blazor.CompilationTuning.props" />
</Project>

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

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

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

@ -1,20 +0,0 @@
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(new StandardRuntimePlatform(),
builder => StandardRuntimePlatformServices.Register(builder.ApplicationType!.Assembly))
{
UseWindowingSubsystem(BlazorWindowingPlatform.Register);
}
}
}

44
src/Web/Avalonia.Web.Blazor/AvaloniaView.cs

@ -0,0 +1,44 @@
using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using System;
using Avalonia.Controls.ApplicationLifetimes;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using BrowserView = Avalonia.Web.AvaloniaView;
namespace Avalonia.Web.Blazor;
[SupportedOSPlatform("browser")]
public class AvaloniaView : ComponentBase
{
private BrowserView? _browserView;
private readonly string _containerId;
public AvaloniaView()
{
_containerId = "av_container_" + Guid.NewGuid();
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "div");
builder.AddAttribute(1, "id", _containerId);
builder.AddAttribute(2, "style", "width:100vw;height:100vh");
builder.CloseElement();
}
protected override async Task OnInitializedAsync()
{
if (OperatingSystem.IsBrowser())
{
await Avalonia.Web.Interop.AvaloniaModule.ImportMain();
_browserView = new BrowserView(_containerId);
if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime lifetime)
{
_browserView.Content = lifetime.MainView;
}
}
}
}

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

@ -1,67 +0,0 @@
<div id="container" @ref="_containerElement"
class="avalonia-container"
tabindex="0" oncontextmenu="return false;"
@onwheel="OnWheel"
@onkeydown="OnKeyDown"
@onkeyup="OnKeyUp"
@onkeyup:preventDefault="@KeyPreventDefault"
@onkeydown:preventDefault="@KeyPreventDefault"
@onpointerdown="OnPointerDown"
@onpointerup="OnPointerUp"
@onpointermove="OnPointerMove"
@onpointercancel="OnPointerCancel"
@onfocus="OnFocus">
<canvas id="htmlCanvas" @ref="_htmlCanvas" @attributes="AdditionalAttributes"/>
<div id="nativeControlsContainer" @ref="_nativeControlsContainer" />
<input id="inputElement" @ref="_inputElement" type="text"
spellcheck="false" onpaste="return false;"
oncopy="return false;"
oncut="return false;"
autocapitalize="none"/>
</div>
<style>
#container{
touch-action: none;
}
#htmlCanvas {
opacity: 1;
background-color: #ccc;
position: fixed;
width: 100vw;
height: 100vh;
top: 0px;
left: 0px;
z-index: 500;
}
#nativeControlsContainer {
position: fixed;
width: 100vw;
height: 100vh;
top: 0px;
left: 0px;
z-index: 700;
}
#inputElement {
padding: 0;
margin: 0;
position: absolute;
height: 20px;
z-index: 1000;
overflow: hidden;
caret-color: transparent;
border-top-style: hidden;
border-bottom-style: hidden;
border-right-style: hidden;
border-left-style: hidden;
outline: none;
background: transparent;
color: transparent;
}
</style>

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

@ -1,500 +0,0 @@
using System.Diagnostics;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Embedding;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Web.Blazor.Interop;
using Avalonia.Web.Blazor.Interop.Storage;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
using SkiaSharp;
using HTMLPointerEventArgs = Microsoft.AspNetCore.Components.Web.PointerEventArgs;
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 AvaloniaModule? _avaloniaModule = null;
private InputHelperInterop? _inputHelper = null;
private FocusHelperInterop? _canvasHelper = null;
private FocusHelperInterop? _containerHelper = null;
private NativeControlHostInterop? _nativeControlHost = null;
private StorageProviderInterop? _storageProvider = null;
private ElementReference _htmlCanvas;
private ElementReference _inputElement;
private ElementReference _containerElement;
private ElementReference _nativeControlsContainer;
private double _dpi = 1;
private SKSize _canvasSize = new (100, 100);
private GRContext? _context;
private GRGlInterface? _glInterface;
private const SKColorType ColorType = SKColorType.Rgba8888;
private bool _useGL;
private bool _inputElementFocused;
private ITextInputMethodClient? _client;
[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;
}
}
public bool KeyPreventDefault { get; set; }
public ITextInputMethodClient? Client => _client;
public bool IsActive => _client != null;
public bool IsComposing { get; private set; }
internal INativeControlHostImpl GetNativeControlHostImpl()
{
return _nativeControlHost ?? throw new InvalidOperationException("Blazor View wasn't initialized yet");
}
internal IStorageProvider GetStorageProvider()
{
return _storageProvider ?? throw new InvalidOperationException("Blazor View wasn't initialized yet");
}
private void OnPointerCancel(HTMLPointerEventArgs e)
{
if (e.PointerType == "touch")
{
_topLevelImpl.RawPointerEvent(RawPointerEventType.TouchCancel, e.PointerType, GetPointFromEventArgs(e),
GetModifiers(e), e.PointerId);
}
}
private void OnPointerMove(HTMLPointerEventArgs e)
{
var type = e.PointerType switch
{
"touch" => RawPointerEventType.TouchUpdate,
_ => RawPointerEventType.Move
};
_topLevelImpl.RawPointerEvent(type, e.PointerType, GetPointFromEventArgs(e), GetModifiers(e), e.PointerId);
}
private void OnPointerUp(HTMLPointerEventArgs e)
{
var type = e.PointerType switch
{
"touch" => RawPointerEventType.TouchEnd,
_ => e.Button switch
{
0 => RawPointerEventType.LeftButtonUp,
1 => RawPointerEventType.MiddleButtonUp,
2 => RawPointerEventType.RightButtonUp,
3 => RawPointerEventType.XButton1Up,
4 => RawPointerEventType.XButton2Up,
// 5 => Pen eraser button,
_ => RawPointerEventType.Move
}
};
_topLevelImpl.RawPointerEvent(type, e.PointerType, GetPointFromEventArgs(e), GetModifiers(e), e.PointerId);
}
private void OnPointerDown(HTMLPointerEventArgs e)
{
var type = e.PointerType switch
{
"touch" => RawPointerEventType.TouchBegin,
_ => e.Button switch
{
0 => RawPointerEventType.LeftButtonDown,
1 => RawPointerEventType.MiddleButtonDown,
2 => RawPointerEventType.RightButtonDown,
3 => RawPointerEventType.XButton1Down,
4 => RawPointerEventType.XButton2Down,
// 5 => Pen eraser button,
_ => RawPointerEventType.Move
}
};
_topLevelImpl.RawPointerEvent(type, e.PointerType, GetPointFromEventArgs(e), GetModifiers(e), e.PointerId);
}
private static RawPointerPoint GetPointFromEventArgs(HTMLPointerEventArgs args)
{
return new RawPointerPoint
{
Position = new Point(args.ClientX, args.ClientY),
Pressure = args.Pressure,
XTilt = args.TiltX,
YTilt = args.TiltY
// Twist = args.Twist - read from JS code directly when
};
}
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(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 |= e.Type == "pen" ? RawInputModifiers.PenBarrelButton : RawInputModifiers.RightMouseButton;
if ((e.Buttons & 4L) == 4)
modifiers |= RawInputModifiers.MiddleMouseButton;
if ((e.Buttons & 8L) == 8)
modifiers |= RawInputModifiers.XButton1MouseButton;
if ((e.Buttons & 16L) == 16)
modifiers |= RawInputModifiers.XButton2MouseButton;
if ((e.Buttons & 32L) == 32)
modifiers |= RawInputModifiers.PenEraser;
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)
{
KeyPreventDefault = _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, e.Code, e.Key, GetModifiers(e));
}
private void OnKeyUp(KeyboardEventArgs e)
{
KeyPreventDefault = _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, e.Code, e.Key, GetModifiers(e));
}
private void OnFocus(FocusEventArgs e)
{
// if focus has unexpectedly moved from the input element to the container element,
// shift it back to the input element
if (_inputElementFocused && _inputHelper is not null)
{
_inputHelper.Focus();
}
}
[Parameter(CaptureUnmatchedValues = true)]
public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
AvaloniaLocator.CurrentMutable.Bind<IJSInProcessRuntime>().ToConstant((IJSInProcessRuntime)Js);
_avaloniaModule = await AvaloniaModule.ImportAsync(Js);
_canvasHelper = new FocusHelperInterop(_avaloniaModule, _htmlCanvas);
_containerHelper = new FocusHelperInterop(_avaloniaModule, _containerElement);
_inputHelper = new InputHelperInterop(_avaloniaModule, _inputElement);
_inputHelper.CompositionEvent += InputHelperOnCompositionEvent;
_inputHelper.InputEvent += InputHelperOnInputEvent;
HideIme();
_canvasHelper.SetCursor("default");
_topLevelImpl.SetCssCursor = x =>
{
_inputHelper.SetCursor(x); //macOS
_canvasHelper.SetCursor(x); //windows
};
_nativeControlHost = new NativeControlHostInterop(_avaloniaModule, _nativeControlsContainer);
_storageProvider = await StorageProviderInterop.ImportAsync(Js);
Console.WriteLine("starting html canvas setup");
_interop = new SKHtmlCanvasInterop(_avaloniaModule, _htmlCanvas, OnRenderFrame);
Console.WriteLine("Interop created");
var skiaOptions = AvaloniaLocator.Current.GetService<SkiaOptions>();
_useGL = skiaOptions?.CustomGpuFactory != null;
if (_useGL)
{
_jsGlInfo = _interop.InitGL();
Console.WriteLine("jsglinfo created - init gl");
}
else
{
var rasterInitialized = _interop.InitRaster();
Console.WriteLine("raster initialized: {0}", rasterInitialized);
}
if (_useGL)
{
// create the SkiaSharp context
if (_context == null)
{
_glInterface = GRGlInterface.Create();
_context = GRContext.CreateGl(_glInterface);
// bump the default resource cache limit
_context.SetResourceCacheLimit(skiaOptions?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024);
Console.WriteLine("glcontext created and resource limit set");
}
_topLevelImpl.SetSurface(_context, _jsGlInfo!, ColorType,
new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi);
}
else
{
_topLevelImpl.SetSurface(ColorType,
new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData);
}
_interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
Threading.Dispatcher.UIThread.Post(async () =>
{
_interop.RequestAnimationFrame(true);
_sizeWatcher = new SizeWatcherInterop(_avaloniaModule, _htmlCanvas, OnSizeChanged);
_dpiWatcher = new DpiWatcherInterop(_avaloniaModule, OnDpiChanged);
_sizeWatcher.Start();
_topLevel.Prepare();
_topLevel.Renderer.Start();
});
}
}
private void InputHelperOnInputEvent(object? sender, WebInputEventArgs e)
{
if (IsComposing)
{
return;
}
_topLevelImpl.RawTextEvent(e.Data);
e.Handled = true;
}
private void InputHelperOnCompositionEvent(object? sender, WebCompositionEventArgs e)
{
if(_client == null)
{
return;
}
switch (e.Type)
{
case WebCompositionEventArgs.WebCompositionEventType.Start:
_client.SetPreeditText(null);
IsComposing = true;
break;
case WebCompositionEventArgs.WebCompositionEventType.Update:
_client.SetPreeditText(e.Data);
break;
case WebCompositionEventArgs.WebCompositionEventType.End:
IsComposing = false;
_client.SetPreeditText(null);
_topLevelImpl.RawTextEvent(e.Data);
break;
}
}
private void OnRenderFrame()
{
if (_useGL && (_jsGlInfo == null))
{
Console.WriteLine("nothing to render");
return;
}
if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0)
{
Console.WriteLine("nothing to render");
return;
}
ManualTriggerRenderTimer.Instance.RaiseTick();
}
public void Dispose()
{
_dpiWatcher?.Unsubscribe(OnDpiChanged);
_sizeWatcher?.Dispose();
_interop?.Dispose();
}
private void ForceBlit()
{
// Note: this is technically a hack, but it's a kinda unique use case when
// we want to blit the previous frame
// renderer doesn't have much control over the render target
// we render on the UI thread
// We also don't want to have it as a meaningful public API.
// Therefore we have InternalsVisibleTo hack here.
if (_topLevel.Renderer is CompositingRenderer dr)
{
dr.CompositionTarget.ImmediateUIThreadRender();
}
}
private void OnDpiChanged(double newDpi)
{
if (Math.Abs(_dpi - newDpi) > 0.0001)
{
_dpi = newDpi;
_interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
ForceBlit();
}
}
private void OnSizeChanged(SKSize newSize)
{
if (_canvasSize != newSize)
{
_canvasSize = newSize;
_interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
ForceBlit();
}
}
private void HideIme()
{
_inputHelper?.Hide();
_containerHelper?.Focus();
}
public void SetClient(ITextInputMethodClient? client)
{
if (_inputHelper is null)
{
return;
}
if(_client != null)
{
_client.SurroundingTextChanged -= SurroundingTextChanged;
}
if(client != null)
{
client.SurroundingTextChanged += SurroundingTextChanged;
}
_inputHelper.Clear();
_client = client;
if (IsActive && _client != null)
{
_inputHelper.Show();
_inputElementFocused = true;
_inputHelper.Focus();
var surroundingText = _client.SurroundingText;
_inputHelper?.SetSurroundingText(surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset);
}
else
{
_inputElementFocused = false;
HideIme();
}
}
private void SurroundingTextChanged(object? sender, EventArgs e)
{
if(_client != null)
{
var surroundingText = _client.SurroundingText;
_inputHelper?.SetSurroundingText(surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset);
}
}
public void SetCursorRect(Rect rect)
{
_inputHelper?.Focus();
var bounds = new PixelRect((int)rect.X, (int) rect.Y, (int) rect.Width, (int) rect.Height);
_inputHelper?.SetBounds(bounds, _client?.SurroundingText.CursorOffset ?? 0);
_inputHelper?.Focus();
}
public void SetOptions(TextInputOptions options)
{
}
public void Reset()
{
_inputHelper?.Clear();
_inputHelper?.SetSurroundingText("", 0, 0);
}
}
}

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

@ -1,31 +1,39 @@
using Avalonia.Controls;
using System.Runtime.Versioning;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Media;
namespace Avalonia.Web.Blazor
namespace Avalonia.Web.Blazor;
[SupportedOSPlatform("browser")]
public static class WebAppBuilder
{
public class BlazorSingleViewLifetime : ISingleViewApplicationLifetime
public static T SetupWithSingleViewLifetime<T>(
this T builder)
where T : AppBuilderBase<T>, new()
{
public Control? MainView { get; set; }
return builder.SetupWithLifetime(new BlazorSingleViewLifetime());
}
public static class WebAppBuilder
public static T UseBlazor<T>(this T builder) where T : AppBuilderBase<T>, new()
{
public static T SetupWithSingleViewLifetime<T>(
this T builder)
where T : AppBuilderBase<T>, new()
{
return builder.SetupWithLifetime(new BlazorSingleViewLifetime());
}
return builder
.UseBrowser()
.With(new BrowserPlatformOptions
{
FrameworkAssetPathResolver = new(filePath => $"/_content/Avalonia.Web.Blazor/{filePath}")
});
}
public static AvaloniaBlazorAppBuilder Configure<TApp>()
where TApp : Application, new()
{
var builder = AvaloniaBlazorAppBuilder.Configure<TApp>()
.UseSkia()
.With(new SkiaOptions { CustomGpuFactory = () => new BlazorSkiaGpu() });
public static AppBuilder Configure<TApp>()
where TApp : Application, new()
{
return AppBuilder.Configure<TApp>()
.UseBlazor();
}
return builder;
}
internal class BlazorSingleViewLifetime : ISingleViewApplicationLifetime
{
public Control? MainView { get; set; }
}
}

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

@ -1,25 +0,0 @@
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

@ -1,37 +0,0 @@
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; }
}
}

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

@ -1,39 +0,0 @@
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 => _blazorSkiaSurface.Size != _size;
}
}

87
src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs

@ -1,87 +0,0 @@
using System.Runtime.InteropServices;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Platform;
using Avalonia.Skia;
using SkiaSharp;
namespace Avalonia.Web.Blazor
{
internal class BlazorSkiaRasterSurface : IBlazorSkiaSurface, IFramebufferPlatformSurface, IDisposable
{
public SKColorType ColorType { get; set; }
public PixelSize Size { get; set; }
public double Scaling { get; set; }
private FramebufferData? _fbData;
private readonly Action<IntPtr, SKSizeI> _blitCallback;
private readonly Action _onDisposeAction;
public BlazorSkiaRasterSurface(
SKColorType colorType, PixelSize size, double scaling, Action<IntPtr, SKSizeI> blitCallback)
{
ColorType = colorType;
Size = size;
Scaling = scaling;
_blitCallback = blitCallback;
_onDisposeAction = Blit;
}
public void Dispose()
{
_fbData?.Dispose();
_fbData = null;
}
public ILockedFramebuffer Lock()
{
var bytesPerPixel = 4; // TODO: derive from ColorType
var dpi = Scaling * 96.0;
var width = (int)(Size.Width * Scaling);
var height = (int)(Size.Height * Scaling);
if (_fbData is null || _fbData?.Size.Width != width || _fbData?.Size.Height != height)
{
_fbData?.Dispose();
_fbData = new FramebufferData(width, height, bytesPerPixel);
}
var pixelFormat = ColorType.ToPixelFormat();
var data = _fbData.Value;
return new LockedFramebuffer(
data.Address, data.Size, data.RowBytes,
new Vector(dpi, dpi), pixelFormat, _onDisposeAction);
}
private void Blit()
{
if (_fbData != null)
{
var data = _fbData.Value;
_blitCallback(data.Address, new SKSizeI(data.Size.Width, data.Size.Height));
}
}
private readonly struct FramebufferData
{
public PixelSize Size { get; }
public int RowBytes { get; }
public IntPtr Address { get; }
public FramebufferData(int width, int height, int bytesPerPixel)
{
Size = new PixelSize(width, height);
RowBytes = width * bytesPerPixel;
Address = Marshal.AllocHGlobal(width * height * bytesPerPixel);
}
public void Dispose()
{
Marshal.FreeHGlobal(Address);
}
}
}
}

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

@ -1,30 +0,0 @@
using Avalonia.Web.Blazor.Interop;
using SkiaSharp;
namespace Avalonia.Web.Blazor
{
internal class BlazorSkiaSurface : IBlazorSkiaSurface
{
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; }
}
}

34
src/Web/Avalonia.Web.Blazor/ClipboardImpl.cs

@ -1,34 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor
{
internal class ClipboardImpl : IClipboard
{
public async Task<string> GetTextAsync()
{
return await AvaloniaLocator.Current.GetRequiredService<IJSInProcessRuntime>().
InvokeAsync<string>("navigator.clipboard.readText");
}
public async Task SetTextAsync(string text)
{
await AvaloniaLocator.Current.GetRequiredService<IJSInProcessRuntime>().
InvokeAsync<string>("navigator.clipboard.writeText",text);
}
public async Task ClearAsync() => await SetTextAsync("");
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());
}
}

93
src/Web/Avalonia.Web.Blazor/Cursor.cs

@ -1,93 +0,0 @@
using Avalonia.Input;
using Avalonia.Platform;
namespace Avalonia.Web.Blazor
{
public class CssCursor : ICursorImpl
{
public const string Default = "default";
public string? Value { get; set; }
public CssCursor(StandardCursorType type)
{
Value = ToKeyword(type);
}
/// <summary>
/// Create a cursor from base64 image
/// </summary>
public CssCursor(string base64, string format, PixelPoint hotspot, StandardCursorType fallback)
{
Value = $"url(\"data:image/{format};base64,{base64}\") {hotspot.X} {hotspot.Y}, {ToKeyword(fallback)}";
}
/// <summary>
/// Create a cursor from url to *.cur file.
/// </summary>
public CssCursor(string url, StandardCursorType fallback)
{
Value = $"url('{url}'), {ToKeyword(fallback)}";
}
/// <summary>
/// Create a cursor from png/svg and hotspot position
/// </summary>
public CssCursor(string url, PixelPoint hotSpot, StandardCursorType fallback)
{
Value = $"url('{url}') {hotSpot.X} {hotSpot.Y}, {ToKeyword(fallback)}";
}
private static string ToKeyword(StandardCursorType type) => type switch
{
StandardCursorType.Hand => "pointer",
StandardCursorType.Cross => "crosshair",
StandardCursorType.Help => "help",
StandardCursorType.Ibeam => "text",
StandardCursorType.No => "not-allowed",
StandardCursorType.None => "none",
StandardCursorType.Wait => "progress",
StandardCursorType.AppStarting => "wait",
StandardCursorType.DragMove => "move",
StandardCursorType.DragCopy => "copy",
StandardCursorType.DragLink => "alias",
StandardCursorType.UpArrow => "default",/*not found matching one*/
StandardCursorType.SizeWestEast => "ew-resize",
StandardCursorType.SizeNorthSouth => "ns-resize",
StandardCursorType.SizeAll => "move",
StandardCursorType.TopSide => "n-resize",
StandardCursorType.BottomSide => "s-resize",
StandardCursorType.LeftSide => "w-resize",
StandardCursorType.RightSide => "e-resize",
StandardCursorType.TopLeftCorner => "nw-resize",
StandardCursorType.TopRightCorner => "ne-resize",
StandardCursorType.BottomLeftCorner => "sw-resize",
StandardCursorType.BottomRightCorner => "se-resize",
_ => Default,
};
public void Dispose() {}
}
internal class CssCursorFactory : ICursorFactory
{
public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot)
{
using var imageStream = new MemoryStream();
cursor.Save(imageStream);
//not memory optimized because CryptoStream with ToBase64Transform is not supported in the browser.
var base64String = Convert.ToBase64String(imageStream.ToArray());
return new CssCursor(base64String, "png", hotSpot, StandardCursorType.Arrow);
}
ICursorImpl ICursorFactory.GetCursor(StandardCursorType cursorType)
{
return new CssCursor(cursorType);
}
}
}

9
src/Web/Avalonia.Web.Blazor/IBlazorSkiaSurface.cs

@ -1,9 +0,0 @@
namespace Avalonia.Web.Blazor
{
internal interface IBlazorSkiaSurface
{
public PixelSize Size { get; set; }
public double Scaling { get; set; }
}
}

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

@ -1,81 +0,0 @@
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();
}
[EditorBrowsable(EditorBrowsableState.Never)]
public class ActionHelper<T>
{
private readonly Action<T> action;
public ActionHelper(Action<T> action)
{
this.action = action;
}
[JSInvokable]
public void Invoke(T param1) => action?.Invoke(param1);
}
[EditorBrowsable(EditorBrowsableState.Never)]
public class ActionHelper<T1, T2>
{
private readonly Action<T1, T2> action;
public ActionHelper(Action<T1, T2> action)
{
this.action = action;
}
[JSInvokable]
public void Invoke(T1 p1, T2 p2) => action?.Invoke(p1, p2);
}
[EditorBrowsable(EditorBrowsableState.Never)]
public class ActionHelper<T1, T2, T3>
{
private readonly Action<T1, T2, T3> action;
public ActionHelper(Action<T1, T2, T3> action)
{
this.action = action;
}
[JSInvokable]
public void Invoke(T1 p1, T2 p2, T3 p3) => action?.Invoke(p1, p2, p3);
}
[EditorBrowsable(EditorBrowsableState.Never)]
public class ActionHelper<T1, T2, T3, T4>
{
private readonly Action<T1, T2, T3, T4> action;
public ActionHelper(Action<T1, T2, T3, T4> action)
{
this.action = action;
}
[JSInvokable]
public void Invoke(T1 p1, T2 p2, T3 p3, T4 p4) => action?.Invoke(p1, p2, p3, p4);
}
}

18
src/Web/Avalonia.Web.Blazor/Interop/AvaloniaModule.cs

@ -1,18 +0,0 @@
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor.Interop
{
internal class AvaloniaModule : JSModuleInterop
{
private AvaloniaModule(IJSRuntime js) : base(js, "./_content/Avalonia.Web.Blazor/Avalonia.js")
{
}
public static async Task<AvaloniaModule> ImportAsync(IJSRuntime js)
{
var interop = new AvaloniaModule(js);
await interop.ImportAsync();
return interop;
}
}
}

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

@ -1,72 +0,0 @@
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor.Interop
{
internal class DpiWatcherInterop : IDisposable
{
private const string StartSymbol = "DpiWatcher.start";
private const string StopSymbol = "DpiWatcher.stop";
private const string GetDpiSymbol = "DpiWatcher.getDpi";
private event Action<double>? callbacksEvent;
private readonly ActionHelper<float, float> _callbackHelper;
private readonly AvaloniaModule _module;
private DotNetObjectReference<ActionHelper<float, float>>? callbackReference;
public DpiWatcherInterop(AvaloniaModule module, Action<double>? callback = null)
{
_module = module;
_callbackHelper = new ActionHelper<float, float>((o, n) => callbacksEvent?.Invoke(n));
if (callback != null)
Subscribe(callback);
}
public void Dispose() => 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 _module.Invoke<double>(StartSymbol, callbackReference);
}
private void Stop()
{
if (callbackReference == null)
return;
_module.Invoke(StopSymbol);
callbackReference?.Dispose();
callbackReference = null;
}
public double GetDpi() => _module.Invoke<double>(GetDpiSymbol);
}
}

22
src/Web/Avalonia.Web.Blazor/Interop/FocusHelperInterop.cs

@ -1,22 +0,0 @@
using Microsoft.AspNetCore.Components;
namespace Avalonia.Web.Blazor.Interop;
internal class FocusHelperInterop
{
private const string FocusSymbol = "FocusHelper.focus";
private const string SetCursorSymbol = "FocusHelper.setCursor";
private readonly AvaloniaModule _module;
private readonly ElementReference _inputElement;
public FocusHelperInterop(AvaloniaModule module, ElementReference inputElement)
{
_module = module;
_inputElement = inputElement;
}
public void Focus() => _module.Invoke(FocusSymbol, _inputElement);
public void SetCursor(string kind) => _module.Invoke(SetCursorSymbol, _inputElement, kind);
}

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

@ -1,130 +0,0 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor.Interop
{
internal class WebCompositionEventArgs : EventArgs
{
public enum WebCompositionEventType
{
Start,
Update,
End
}
public WebCompositionEventArgs(string type, string data)
{
Type = type switch
{
"compositionstart" => WebCompositionEventType.Start,
"compositionupdate" => WebCompositionEventType.Update,
"compositionend" => WebCompositionEventType.End,
_ => Type
};
Data = data;
}
public WebCompositionEventType Type { get; }
public string Data { get; }
}
internal class WebInputEventArgs
{
public WebInputEventArgs(string type, string data)
{
Type = type;
Data = data;
}
public string Type { get; }
public string Data { get; }
public bool Handled { get; set; }
}
internal class InputHelperInterop
{
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 const string StartSymbol = "InputHelper.start";
private const string SetSurroundingTextSymbol = "InputHelper.setSurroundingText";
private const string SetBoundsSymbol = "InputHelper.setBounds";
private readonly AvaloniaModule _module;
private readonly ElementReference _inputElement;
private readonly ActionHelper<string, string> _compositionAction;
private readonly ActionHelper<string, string> _inputAction;
private DotNetObjectReference<ActionHelper<string, string>>? compositionActionReference;
private DotNetObjectReference<ActionHelper<string, string>>? inputActionReference;
public InputHelperInterop(AvaloniaModule module, ElementReference inputElement)
{
_module = module;
_inputElement = inputElement;
_compositionAction = new ActionHelper<string, string>(OnCompositionEvent);
_inputAction = new ActionHelper<string, string>(OnInputEvent);
Start();
}
public event EventHandler<WebCompositionEventArgs>? CompositionEvent;
public event EventHandler<WebInputEventArgs>? InputEvent;
private void OnCompositionEvent(string type, string data)
{
Console.WriteLine($"CompositionEvent Handler Helper {CompositionEvent == null} ");
CompositionEvent?.Invoke(this, new WebCompositionEventArgs(type, data));
}
private void OnInputEvent(string type, string data)
{
Console.WriteLine($"InputEvent Handler Helper {InputEvent == null} ");
var args = new WebInputEventArgs(type, data);
InputEvent?.Invoke(this, args);
}
public void Clear() => _module.Invoke(ClearSymbol, _inputElement);
public void Focus() => _module.Invoke(FocusSymbol, _inputElement);
public void SetCursor(string kind) => _module.Invoke(SetCursorSymbol, _inputElement, kind);
public void Hide() => _module.Invoke(HideSymbol, _inputElement);
public void Show() => _module.Invoke(ShowSymbol, _inputElement);
private void Start()
{
if(compositionActionReference != null)
{
return;
}
compositionActionReference = DotNetObjectReference.Create(_compositionAction);
inputActionReference = DotNetObjectReference.Create(_inputAction);
_module.Invoke(StartSymbol, _inputElement, compositionActionReference, inputActionReference);
}
public void SetSurroundingText(string text, int start, int end)
{
_module.Invoke(SetSurroundingTextSymbol, _inputElement, text, start, end);
}
public void SetBounds(PixelRect bounds, int caret)
{
_module.Invoke(SetBoundsSymbol, _inputElement, bounds.X, bounds.Y, bounds.Width, bounds.Height, caret);
}
}
}

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

@ -1,46 +0,0 @@
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.");
internal void Invoke(string identifier, params object?[]? args) =>
Module.InvokeVoid(identifier, args);
internal TValue Invoke<TValue>(string identifier, params object?[]? args) =>
Module.Invoke<TValue>(identifier, args);
internal ValueTask InvokeAsync(string identifier, params object?[]? args) =>
Module.InvokeVoidAsync(identifier, args);
internal ValueTask<TValue> InvokeAsync<TValue>(string identifier, params object?[]? args) =>
Module.InvokeAsync<TValue>(identifier, args);
protected virtual void OnDisposingModule() { }
}
}

144
src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs

@ -1,144 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor.Interop
{
internal class NativeControlHostInterop : INativeControlHostImpl
{
private const string CreateDefaultChildSymbol = "NativeControlHost.CreateDefaultChild";
private const string CreateAttachmentSymbol = "NativeControlHost.CreateAttachment";
private const string GetReferenceSymbol = "NativeControlHost.GetReference";
private readonly AvaloniaModule _module;
private readonly ElementReference _hostElement;
public NativeControlHostInterop(AvaloniaModule module, ElementReference element)
{
_module = module;
_hostElement = element;
}
public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent)
{
var element = _module.Invoke<IJSInProcessObjectReference>(CreateDefaultChildSymbol);
return new JSObjectControlHandle(element);
}
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func<IPlatformHandle, IPlatformHandle> create)
{
Attachment? a = null;
try
{
using var hostElementJsReference = _module.Invoke<IJSInProcessObjectReference>(GetReferenceSymbol, _hostElement);
var child = create(new JSObjectControlHandle(hostElementJsReference));
var attachmenetReference = _module.Invoke<IJSInProcessObjectReference>(CreateAttachmentSymbol);
// It has to be assigned to the variable before property setter is called so we dispose it on exception
#pragma warning disable IDE0017 // Simplify object initialization
a = new Attachment(attachmenetReference, child);
#pragma warning restore IDE0017 // Simplify object initialization
a.AttachedTo = this;
return a;
}
catch
{
a?.Dispose();
throw;
}
}
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle)
{
var attachmenetReference = _module.Invoke<IJSInProcessObjectReference>(CreateAttachmentSymbol);
var a = new Attachment(attachmenetReference, handle);
a.AttachedTo = this;
return a;
}
public bool IsCompatibleWith(IPlatformHandle handle) => handle is JSObjectControlHandle;
class Attachment : INativeControlHostControlTopLevelAttachment
{
private const string InitializeWithChildHandleSymbol = "InitializeWithChildHandle";
private const string AttachToSymbol = "AttachTo";
private const string ShowInBoundsSymbol = "ShowInBounds";
private const string HideWithSizeSymbol = "HideWithSize";
private const string ReleaseChildSymbol = "ReleaseChild";
private IJSInProcessObjectReference? _native;
private NativeControlHostInterop? _attachedTo;
public Attachment(IJSInProcessObjectReference native, IPlatformHandle handle)
{
_native = native;
_native.InvokeVoid(InitializeWithChildHandleSymbol, ((JSObjectControlHandle)handle).Object);
}
public void Dispose()
{
if (_native != null)
{
_native.InvokeVoid(ReleaseChildSymbol);
_native.Dispose();
_native = null;
}
}
public INativeControlHostImpl? AttachedTo
{
get => _attachedTo!;
set
{
CheckDisposed();
var host = (NativeControlHostInterop?)value;
if (host == null)
{
_native.InvokeVoid(AttachToSymbol);
}
else
{
_native.InvokeVoid(AttachToSymbol, host._hostElement);
}
_attachedTo = host;
}
}
public bool IsCompatibleWith(INativeControlHostImpl host) => host is NativeControlHostInterop;
public void HideWithSize(Size size)
{
CheckDisposed();
if (_attachedTo == null)
return;
_native.InvokeVoid(HideWithSizeSymbol, Math.Max(1, (float)size.Width), Math.Max(1, (float)size.Height));
}
public void ShowInBounds(Rect bounds)
{
CheckDisposed();
if (_attachedTo == null)
throw new InvalidOperationException("Native control isn't attached to a toplevel");
bounds = new Rect(bounds.X, bounds.Y, Math.Max(1, bounds.Width),
Math.Max(1, bounds.Height));
_native.InvokeVoid(ShowInBoundsSymbol, (float)bounds.X, (float)bounds.Y, (float)bounds.Width, (float)bounds.Height);
}
[MemberNotNull(nameof(_native))]
private void CheckDisposed()
{
if (_native == null)
throw new ObjectDisposedException(nameof(Attachment));
}
}
}
}

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

@ -1,76 +0,0 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using SkiaSharp;
namespace Avalonia.Web.Blazor.Interop
{
internal class SKHtmlCanvasInterop : IDisposable
{
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 SetCanvasSizeSymbol = "SKHtmlCanvas.setCanvasSize";
private const string PutImageDataSymbol = "SKHtmlCanvas.putImageData";
private readonly AvaloniaModule _module;
private readonly ElementReference _htmlCanvas;
private readonly string _htmlElementId;
private readonly ActionHelper _callbackHelper;
private DotNetObjectReference<ActionHelper>? callbackReference;
public SKHtmlCanvasInterop(AvaloniaModule module, ElementReference element, Action renderFrameCallback)
{
_module = module;
_htmlCanvas = element;
_htmlElementId = element.Id;
_callbackHelper = new ActionHelper(renderFrameCallback);
}
public void Dispose() => Deinit();
public GLInfo InitGL()
{
if (callbackReference != null)
throw new InvalidOperationException("Unable to initialize the same canvas more than once.");
callbackReference = DotNetObjectReference.Create(_callbackHelper);
return _module.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 _module.Invoke<bool>(InitRasterSymbol, _htmlCanvas, _htmlElementId, callbackReference);
}
public void Deinit()
{
if (callbackReference == null)
return;
_module.Invoke(DeinitSymbol, _htmlElementId);
callbackReference?.Dispose();
}
public void RequestAnimationFrame(bool enableRenderLoop) =>
_module.Invoke(RequestAnimationFrameSymbol, _htmlCanvas, enableRenderLoop);
public void SetCanvasSize(int rawWidth, int rawHeight) =>
_module.Invoke(SetCanvasSizeSymbol, _htmlCanvas, rawWidth, rawHeight);
public void PutImageData(IntPtr intPtr, SKSizeI rawSize) =>
_module.Invoke(PutImageDataSymbol, _htmlCanvas, intPtr.ToInt64(), rawSize.Width, rawSize.Height);
public record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth);
}
}

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

@ -1,50 +0,0 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using SkiaSharp;
namespace Avalonia.Web.Blazor.Interop
{
internal class SizeWatcherInterop : IDisposable
{
private const string ObserveSymbol = "SizeWatcher.observe";
private const string UnobserveSymbol = "SizeWatcher.unobserve";
private readonly AvaloniaModule _module;
private readonly ElementReference _htmlElement;
private readonly string _htmlElementId;
private readonly ActionHelper<float, float> _callbackHelper;
private DotNetObjectReference<ActionHelper<float, float>>? callbackReference;
public SizeWatcherInterop(AvaloniaModule module, ElementReference element, Action<SKSize> callback)
{
_module = module;
_htmlElement = element;
_htmlElementId = element.Id;
_callbackHelper = new ActionHelper<float, float>((x, y) => callback(new SKSize(x, y)));
}
public void Dispose() => Stop();
public void Start()
{
if (callbackReference != null)
return;
callbackReference = DotNetObjectReference.Create(_callbackHelper);
_module.Invoke(ObserveSymbol, _htmlElement, _htmlElementId, callbackReference);
}
public void Stop()
{
if (callbackReference == null)
return;
_module.Invoke(UnobserveSymbol, _htmlElementId);
callbackReference?.Dispose();
callbackReference = null;
}
}
}

225
src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs

@ -1,225 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Platform.Storage;
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor.Interop.Storage
{
internal record FilePickerAcceptType(string Description, IReadOnlyDictionary<string, IReadOnlyList<string>> Accept);
internal record FileProperties(ulong Size, long LastModified, string? Type);
internal class StorageProviderInterop : JSModuleInterop, IStorageProvider
{
private const string JsFilename = "./_content/Avalonia.Web.Blazor/Storage.js";
private const string PickerCancelMessage = "The user aborted a request";
public static async Task<StorageProviderInterop> ImportAsync(IJSRuntime js)
{
var interop = new StorageProviderInterop(js);
await interop.ImportAsync();
return interop;
}
public StorageProviderInterop(IJSRuntime js)
: base(js, JsFilename)
{
}
public bool CanOpen => Invoke<bool>("StorageProvider.canOpen");
public bool CanSave => Invoke<bool>("StorageProvider.canSave");
public bool CanPickFolder => Invoke<bool>("StorageProvider.canPickFolder");
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
try
{
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle;
var (types, exludeAll) = ConvertFileTypes(options.FileTypeFilter);
var items = await InvokeAsync<IJSInProcessObjectReference>("StorageProvider.openFileDialog", startIn, options.AllowMultiple, types, exludeAll);
var count = items.Invoke<int>("count");
return Enumerable.Range(0, count)
.Select(index => new JSStorageFile(items.Invoke<IJSInProcessObjectReference>("at", index)))
.ToArray();
}
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal))
{
return Array.Empty<IStorageFile>();
}
}
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
try
{
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle;
var (types, exludeAll) = ConvertFileTypes(options.FileTypeChoices);
var item = await InvokeAsync<IJSInProcessObjectReference>("StorageProvider.saveFileDialog", startIn, options.SuggestedFileName, types, exludeAll);
return item is not null ? new JSStorageFile(item) : null;
}
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal))
{
return null;
}
}
public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
try
{
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle;
var item = await InvokeAsync<IJSInProcessObjectReference>("StorageProvider.selectFolderDialog", startIn);
return item is not null ? new[] { new JSStorageFolder(item) } : Array.Empty<IStorageFolder>();
}
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal))
{
return Array.Empty<IStorageFolder>();
}
}
public async Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark)
{
var item = await InvokeAsync<IJSInProcessObjectReference>("StorageProvider.openBookmark", bookmark);
return item is not null ? new JSStorageFile(item) : null;
}
public async Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark)
{
var item = await InvokeAsync<IJSInProcessObjectReference>("StorageProvider.openBookmark", bookmark);
return item is not null ? new JSStorageFolder(item) : null;
}
private static (FilePickerAcceptType[]? types, bool excludeAllOption) ConvertFileTypes(IEnumerable<FilePickerFileType>? input)
{
var types = input?
.Where(t => t.MimeTypes?.Any() == true && t != FilePickerFileTypes.All)
.Select(t => new FilePickerAcceptType(t.Name, t.MimeTypes!
.ToDictionary(m => m, _ => (IReadOnlyList<string>)Array.Empty<string>())))
.ToArray();
if (types?.Length == 0)
{
types = null;
}
var inlcudeAll = input?.Contains(FilePickerFileTypes.All) == true || types is null;
return (types, !inlcudeAll);
}
}
internal abstract class JSStorageItem : IStorageBookmarkItem
{
internal IJSInProcessObjectReference? _fileHandle;
protected JSStorageItem(IJSInProcessObjectReference fileHandle)
{
_fileHandle = fileHandle ?? throw new ArgumentNullException(nameof(fileHandle));
}
internal IJSInProcessObjectReference FileHandle => _fileHandle ?? throw new ObjectDisposedException(nameof(JSStorageItem));
public string Name => FileHandle.Invoke<string>("getName");
public bool TryGetUri([NotNullWhen(true)] out Uri? uri)
{
uri = new Uri(Name, UriKind.Relative);
return false;
}
public async Task<StorageItemProperties> GetBasicPropertiesAsync()
{
var properties = await FileHandle.InvokeAsync<FileProperties?>("getProperties");
return new StorageItemProperties(
properties?.Size,
dateCreated: null,
dateModified: properties?.LastModified > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(properties.LastModified) : null);
}
public bool CanBookmark => true;
public Task<string?> SaveBookmarkAsync()
{
return FileHandle.InvokeAsync<string?>("saveBookmark").AsTask();
}
public Task<IStorageFolder?> GetParentAsync()
{
return Task.FromResult<IStorageFolder?>(null);
}
public Task ReleaseBookmarkAsync()
{
return FileHandle.InvokeAsync<string?>("deleteBookmark").AsTask();
}
public void Dispose()
{
_fileHandle?.Dispose();
_fileHandle = null;
}
}
internal class JSStorageFile : JSStorageItem, IStorageBookmarkFile
{
public JSStorageFile(IJSInProcessObjectReference fileHandle) : base(fileHandle)
{
}
public bool CanOpenRead => true;
public async Task<Stream> OpenReadAsync()
{
var stream = await FileHandle.InvokeAsync<IJSStreamReference>("openRead");
// Remove maxAllowedSize limit, as developer can decide if they read only small part or everything.
return await stream.OpenReadStreamAsync(long.MaxValue, CancellationToken.None);
}
public bool CanOpenWrite => true;
public async Task<Stream> OpenWriteAsync()
{
var properties = await FileHandle.InvokeAsync<FileProperties?>("getProperties");
var streamWriter = await FileHandle.InvokeAsync<IJSInProcessObjectReference>("openWrite");
return new JSWriteableStream(streamWriter, (long)(properties?.Size ?? 0));
}
}
internal class JSStorageFolder : JSStorageItem, IStorageBookmarkFolder
{
public JSStorageFolder(IJSInProcessObjectReference fileHandle) : base(fileHandle)
{
}
public async Task<IReadOnlyList<IStorageItem>> GetItemsAsync()
{
var items = await FileHandle.InvokeAsync<IJSInProcessObjectReference?>("getItems");
if (items is null)
{
return Array.Empty<IStorageItem>();
}
var count = items.Invoke<int>("count");
return Enumerable.Range(0, count)
.Select(index =>
{
var reference = items.Invoke<IJSInProcessObjectReference>("at", index);
return reference.Invoke<string>("getKind") switch
{
"directory" => (IStorageItem)new JSStorageFolder(reference),
"file" => new JSStorageFile(reference),
_ => null
};
})
.Where(i => i is not null)
.ToArray()!;
}
}
}

124
src/Web/Avalonia.Web.Blazor/Interop/Storage/WriteableStream.cs

@ -1,124 +0,0 @@
using System.Buffers;
using System.Text.Json.Serialization;
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor.Interop.Storage
{
// Loose wrapper implementaion of a stream on top of FileAPI FileSystemWritableFileStream
internal sealed class JSWriteableStream : Stream
{
private IJSInProcessObjectReference? _jSReference;
// Unfortunatelly we can't read current length/position, so we need to keep it C#-side only.
private long _length, _position;
internal JSWriteableStream(IJSInProcessObjectReference jSReference, long initialLength)
{
_jSReference = jSReference;
_length = initialLength;
}
private IJSInProcessObjectReference JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(JSWriteableStream));
public override bool CanRead => false;
public override bool CanSeek => true;
public override bool CanWrite => true;
public override long Length => _length;
public override long Position
{
get => _position;
set => Seek(_position, SeekOrigin.Begin);
}
public override void Flush()
{
// no-op
}
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
var position = origin switch
{
SeekOrigin.Current => _position + offset,
SeekOrigin.End => _length + offset,
_ => offset
};
JSReference.InvokeVoid("seek", position);
return position;
}
public override void SetLength(long value)
{
_length = value;
// See https://docs.w3cub.com/dom/filesystemwritablefilestream/truncate
// If the offset is smaller than the size, it remains unchanged. If the offset is larger than size, the offset is set to that size
if (_position > _length)
{
_position = _length;
}
JSReference.InvokeVoid("truncate", value);
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException("Synchronous writes are not supported.");
}
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
if (offset != 0 || count != buffer.Length)
{
// TODO, we need to pass prepared buffer to the JS
// Can't use ArrayPool as it can return bigger array than requested
// Can't use Span/Memory, as it's not supported by JS interop yet.
// Alternatively we can pass original buffer and offset+count, so it can be trimmed on the JS side (but is it more efficient tho?)
buffer = buffer.AsMemory(offset, count).ToArray();
}
return WriteAsyncInternal(buffer, cancellationToken).AsTask();
}
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
{
return WriteAsyncInternal(buffer.ToArray(), cancellationToken);
}
private ValueTask WriteAsyncInternal(byte[] buffer, CancellationToken _)
{
_position += buffer.Length;
return JSReference.InvokeVoidAsync("write", buffer);
}
protected override void Dispose(bool disposing)
{
if (_jSReference is { } jsReference)
{
_jSReference = null;
jsReference.InvokeVoid("close");
jsReference.Dispose();
}
}
public override async ValueTask DisposeAsync()
{
if (_jSReference is { } jsReference)
{
_jSReference = null;
await jsReference.InvokeVoidAsync("close");
await jsReference.DisposeAsync();
}
}
}
}

35
src/Web/Avalonia.Web.Blazor/JSObjectControlHandle.cs

@ -1,35 +0,0 @@
#nullable enable
using Avalonia.Controls.Platform;
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor
{
public class JSObjectControlHandle : INativeControlHostDestroyableControlHandle
{
internal const string ElementReferenceDescriptor = "JSObjectReference";
public JSObjectControlHandle(IJSObjectReference reference)
{
Object = reference;
}
public IJSObjectReference Object { get; }
public IntPtr Handle => throw new NotSupportedException();
public string? HandleDescriptor => ElementReferenceDescriptor;
public void Destroy()
{
if (Object is IJSInProcessObjectReference inProcess)
{
inProcess.Dispose();
}
else
{
_ = Object.DisposeAsync();
}
}
}
}

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

@ -1,127 +0,0 @@
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 }
};
}
}

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

@ -1,17 +0,0 @@
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;
public bool RunsInBackground => false;
}
}

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

@ -1,222 +0,0 @@
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.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Web.Blazor.Interop;
using SkiaSharp;
#nullable enable
namespace Avalonia.Web.Blazor
{
internal class RazorViewTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider
{
private Size _clientSize;
private IBlazorSkiaSurface? _currentSurface;
private IInputRoot? _inputRoot;
private readonly Stopwatch _sw = Stopwatch.StartNew();
private readonly AvaloniaView _avaloniaView;
private readonly TouchDevice _touchDevice;
private readonly PenDevice _penDevice;
private string _currentCursor = CssCursor.Default;
public RazorViewTopLevelImpl(AvaloniaView avaloniaView)
{
_avaloniaView = avaloniaView;
TransparencyLevel = WindowTransparencyLevel.None;
AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1);
_touchDevice = new TouchDevice();
_penDevice = new PenDevice();
}
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);
}
internal void SetSurface(SKColorType colorType, PixelSize size, double scaling, Action<IntPtr, SKSizeI> blitCallback)
{
_currentSurface = new BlazorSkiaRasterSurface(colorType, size, scaling, blitCallback);
}
public void SetClientSize(SKSize size, double dpi)
{
var newSize = new Size(size.Width, size.Height);
if (Math.Abs(RenderScaling - dpi) > 0.0001)
{
if (_currentSurface is { })
{
_currentSurface.Scaling = dpi;
}
ScalingChanged?.Invoke(dpi);
}
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 RawPointerEvent(
RawPointerEventType eventType, string pointerType,
RawPointerPoint p, RawInputModifiers modifiers, long touchPointId)
{
if (_inputRoot is { }
&& Input is { } input)
{
var device = GetPointerDevice(pointerType);
var args = device is TouchDevice ?
new RawTouchEventArgs(device, Timestamp, _inputRoot, eventType, p, modifiers, touchPointId) :
new RawPointerEventArgs(device, Timestamp, _inputRoot, eventType, p, modifiers)
{
RawPointerId = touchPointId
};
input.Invoke(args);
}
}
private IPointerDevice GetPointerDevice(string pointerType)
{
return pointerType switch
{
"touch" => _touchDevice,
"pen" => _penDevice,
_ => MouseDevice
};
}
public void RawMouseWheelEvent(Point p, Vector v, RawInputModifiers modifiers)
{
if (_inputRoot is { })
{
Input?.Invoke(new RawMouseWheelEventArgs(MouseDevice, Timestamp, _inputRoot, p, v, modifiers));
}
}
public bool RawKeyboardEvent(RawKeyEventType type, string code, string key, RawInputModifiers modifiers)
{
if (Keycodes.KeyCodes.TryGetValue(code, out var avkey))
{
if (_inputRoot is { })
{
var args = new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers);
Input?.Invoke(args);
return args.Handled;
}
}
else if (Keycodes.KeyCodes.TryGetValue(key, out avkey))
{
if (_inputRoot is { })
{
var args = new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers);
Input?.Invoke(args);
return args.Handled;
}
}
return false;
}
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.GetRequiredService<IRenderLoop>();
return new CompositingRenderer(root, new Compositor(loop, null));
}
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)
{
var val = (cursor as CssCursor)?.Value ?? CssCursor.Default;
if (_currentCursor != val)
{
SetCssCursor?.Invoke(val);
_currentCursor = val;
}
}
public IPopupImpl? CreatePopup()
{
return null;
}
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)
{
}
public Size ClientSize => _clientSize;
public Size? FrameSize => null;
public double RenderScaling => _currentSurface?.Scaling ?? 1;
public IEnumerable<object> Surfaces => new object[] { _currentSurface! };
public Action<string>? SetCssCursor { get; set; }
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 => _avaloniaView;
public INativeControlHostImpl? NativeControlHost => _avaloniaView.GetNativeControlHostImpl();
public IStorageProvider StorageProvider => _avaloniaView.GetStorageProvider();
}
}

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

@ -1,50 +0,0 @@
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 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 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) };
public Screen? ScreenFromPoint(PixelPoint point)
{
return ScreenHelper.ScreenFromPoint(point, AllScreens);
}
public Screen? ScreenFromRect(PixelRect rect)
{
return ScreenHelper.ScreenFromRect(rect, AllScreens);
}
public Screen? ScreenFromWindow(IWindowBaseImpl window)
{
return ScreenHelper.ScreenFromWindow(window, AllScreens);
}
}
}

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

@ -1,106 +0,0 @@
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 KeyboardDevice? s_keyboard;
public IWindowImpl CreateWindow() => throw new NotSupportedException();
IWindowImpl IWindowingPlatform.CreateEmbeddableWindow()
{
throw new NotImplementedException();
}
public ITrayIconImpl? CreateTrayIcon()
{
return null;
}
public static KeyboardDevice Keyboard => s_keyboard ??
throw new InvalidOperationException("BlazorWindowingPlatform not registered.");
public static void Register()
{
var instance = new BlazorWindowingPlatform();
s_keyboard = new KeyboardDevice();
AvaloniaLocator.CurrentMutable
.Bind<IClipboard>().ToSingleton<ClipboardImpl>()
.Bind<ICursorFactory>().ToSingleton<CssCursorFactory>()
.Bind<IKeyboardDevice>().ToConstant(s_keyboard)
.Bind<IPlatformSettings>().ToConstant(instance)
.Bind<IPlatformThreadingInterface>().ToConstant(instance)
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<IRenderTimer>().ToConstant(ManualTriggerRenderTimer.Instance)
.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 Size TouchDoubleClickSize => new Size(16, 16);
public TimeSpan TouchDoubleClickTime => DoubleClickTime;
public void RunLoop(CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
{
return GetRuntimePlatform()
.StartSystemTimer(interval, () =>
{
Dispatcher.UIThread.RunJobs(priority);
tick();
});
}
public void Signal(DispatcherPriority priority)
{
if (_signaled)
return;
_signaled = true;
IDisposable? disp = null;
disp = GetRuntimePlatform()
.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;
private static IRuntimePlatform GetRuntimePlatform()
{
return AvaloniaLocator.Current.GetRequiredService<IRuntimePlatform>();
}
}
}

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

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

16
src/Web/Avalonia.Web.Blazor/webapp/build.js

@ -1,16 +0,0 @@
require("esbuild").build({
entryPoints: [
"./modules/Avalonia.ts",
"./modules/Storage.ts"
],
outdir: "../wwwroot",
bundle: true,
minify: true,
format: "esm",
target: "es2016",
platform: "browser",
sourcemap: "linked",
loader: {".ts": "ts"}
})
.then(() => console.log("⚡ Done"))
.catch(() => process.exit(1));

7
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia.ts

@ -1,7 +0,0 @@
export { DpiWatcher } from "./Avalonia/DpiWatcher"
export { InputHelper } from "./Avalonia/InputHelper"
export { FocusHelper } from "./Avalonia/FocusHelper"
export { NativeControlHost } from "./Avalonia/NativeControlHost"
export { SizeWatcher } from "./Avalonia/SizeWatcher"
export { SKHtmlCanvas } from "./Avalonia/SKHtmlCanvas"
export { CaretHelper } from "./Avalonia/CaretHelper"

149
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.ts

@ -1,149 +0,0 @@
// Based on https://github.com/component/textarea-caret-position/blob/master/index.js
export class CaretHelper {
public static getCaretCoordinates(
element: HTMLInputElement | HTMLTextAreaElement,
position: number,
options?: { debug: boolean }
) {
if (!isBrowser) {
throw new Error(
"textarea-caret-position#getCaretCoordinates should only be called in a browser"
);
}
const debug = (options && options.debug) || false;
if (debug) {
const el = document.querySelector(
"#input-textarea-caret-position-mirror-div"
);
if (el) el.parentNode?.removeChild(el);
}
// The mirror div will replicate the textarea's style
const div = document.createElement("div");
div.id = "input-textarea-caret-position-mirror-div";
document.body.appendChild(div);
const style = div.style;
const computed = window.getComputedStyle
? window.getComputedStyle(element)
: ((element as any)["currentStyle"] as CSSStyleDeclaration); // currentStyle for IE < 9
const isInput = element.nodeName === "INPUT";
// Default textarea styles
style.whiteSpace = "pre-wrap";
if (!isInput) style.wordWrap = "break-word"; // only for textarea-s
// Position off-screen
style.position = "absolute"; // required to return coordinates properly
if (!debug) style.visibility = "hidden"; // not 'display: none' because we want rendering
// Transfer the element's properties to the div
properties.forEach((prop: string) => {
if (isInput && prop === "lineHeight") {
// Special case for <input>s because text is rendered centered and line height may be != height
if (computed.boxSizing === "border-box") {
const height = parseInt(computed.height);
const outerHeight =
parseInt(computed.paddingTop) +
parseInt(computed.paddingBottom) +
parseInt(computed.borderTopWidth) +
parseInt(computed.borderBottomWidth);
const targetHeight = outerHeight + parseInt(computed.lineHeight);
if (height > targetHeight) {
style.lineHeight = height - outerHeight + "px";
} else if (height === targetHeight) {
style.lineHeight = computed.lineHeight;
} else {
style.lineHeight = "0";
}
} else {
style.lineHeight = computed.height;
}
} else {
(style as any)[prop] = (computed as any)[prop];
}
});
if (isFirefox) {
// Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
if (element.scrollHeight > parseInt(computed.height))
style.overflowY = "scroll";
} else {
style.overflow = "hidden"; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
}
div.textContent = element.value.substring(0, position);
// The second special handling for input type="text" vs textarea:
// spaces need to be replaced with non-breaking spaces - https://stackoverflow.com/a/13402035/1269037
if (isInput) div.textContent = div.textContent.replace(/\s/g, "\u00a0");
const span = document.createElement("span");
// Wrapping must be replicated *exactly*, including when a long word gets
// onto the next line, with whitespace at the end of the line before (#7).
// The *only* reliable way to do that is to copy the *entire* rest of the
// textarea's content into the <span> created at the caret position.
// For inputs, just '.' would be enough, but no need to bother.
span.textContent = element.value.substring(position) || "."; // || because a completely empty faux span doesn't render at all
div.appendChild(span);
const coordinates = {
top: span.offsetTop + parseInt(computed["borderTopWidth"]),
left: span.offsetLeft + parseInt(computed["borderLeftWidth"]),
height: parseInt(computed["lineHeight"]),
};
if (debug) {
span.style.backgroundColor = "#aaa";
} else {
document.body.removeChild(div);
}
return coordinates;
}
}
var properties = [
"direction", // RTL support
"boxSizing",
"width", // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
"height",
"overflowX",
"overflowY", // copy the scrollbar for IE
"borderTopWidth",
"borderRightWidth",
"borderBottomWidth",
"borderLeftWidth",
"borderStyle",
"paddingTop",
"paddingRight",
"paddingBottom",
"paddingLeft",
// https://developer.mozilla.org/en-US/docs/Web/CSS/font
"fontStyle",
"fontVariant",
"fontWeight",
"fontStretch",
"fontSize",
"fontSizeAdjust",
"lineHeight",
"fontFamily",
"textAlign",
"textTransform",
"textIndent",
"textDecoration", // might not make a difference, but better be safe
"letterSpacing",
"wordSpacing",
"tabSize",
"MozTabSize",
];
const isBrowser = typeof window !== "undefined";
const isFirefox = isBrowser && (window as any).mozInnerScreenX != null;

40
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/DpiWatcher.ts

@ -1,40 +0,0 @@
export class DpiWatcher {
static lastDpi: number;
static timerId: number;
static callback?: DotNet.DotNetObject;
public static getDpi() {
return window.devicePixelRatio;
}
public static start(callback: DotNet.DotNetObject): 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);
}
}
}

9
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/FocusHelper.ts

@ -1,9 +0,0 @@
export class FocusHelper {
public static focus(inputElement: HTMLElement) {
inputElement.focus();
}
public static setCursor(inputElement: HTMLInputElement, kind: string) {
inputElement.style.cursor = kind;
}
}

86
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts

@ -1,86 +0,0 @@
import {CaretHelper} from "./CaretHelper";
export class InputHelper {
static inputCallback?: DotNet.DotNetObject;
static compositionCallback?: DotNet.DotNetObject
public static start(inputElement: HTMLInputElement, compositionCallback: DotNet.DotNetObject, inputCallback: DotNet.DotNetObject)
{
InputHelper.compositionCallback = compositionCallback;
inputElement.addEventListener('compositionstart', InputHelper.onCompositionEvent);
inputElement.addEventListener('compositionupdate', InputHelper.onCompositionEvent);
inputElement.addEventListener('compositionend', InputHelper.onCompositionEvent);
InputHelper.inputCallback = inputCallback;
inputElement.addEventListener('input', InputHelper.onInputEvent);
}
public static clear(inputElement: HTMLInputElement) {
inputElement.value = "";
}
public static focus(inputElement: HTMLInputElement) {
inputElement.focus();
}
public static setCursor(inputElement: HTMLInputElement, kind: string) {
inputElement.style.cursor = kind;
}
public static setBounds(inputElement: HTMLInputElement, x: number, y: number, caretWidth: number, caretHeight: number, caret: number) {
inputElement.style.left = (x).toFixed(0) + "px";
inputElement.style.top = (y).toFixed(0) + "px";
let {height, left, top} = CaretHelper.getCaretCoordinates(inputElement, caret);
inputElement.style.left = (x - left).toFixed(0) + "px";
inputElement.style.top = (y - top).toFixed(0) + "px";
}
public static hide(inputElement: HTMLInputElement) {
inputElement.style.display = 'none';
}
public static show(inputElement: HTMLInputElement) {
inputElement.style.display = 'block';
}
public static setSurroundingText(inputElement: HTMLInputElement, text: string, start: number, end: number) {
if (!inputElement) {
return;
}
inputElement.value = text;
inputElement.setSelectionRange(start, end);
inputElement.style.width = "20px";
inputElement.style.width = inputElement.scrollWidth + "px";
}
private static onCompositionEvent(ev: CompositionEvent)
{
if(!InputHelper.compositionCallback)
return;
switch (ev.type)
{
case "compositionstart":
case "compositionupdate":
case "compositionend":
InputHelper.compositionCallback.invokeMethod('Invoke', ev.type, ev.data);
break;
}
}
private static onInputEvent(ev: Event) {
if (!InputHelper.inputCallback)
return;
var inputEvent = ev as InputEvent;
InputHelper.inputCallback.invokeMethod('Invoke', ev.type, inputEvent.data);
}
}

61
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/NativeControlHost.ts

@ -1,61 +0,0 @@
export class NativeControlHost {
public static CreateDefaultChild(parent: HTMLElement): HTMLElement {
return document.createElement("div");
}
// Used to convert ElementReference to JSObjectReference.
// Is there a better way?
public static GetReference(element: Element): Element {
return element;
}
public static CreateAttachment(): NativeControlHostTopLevelAttachment {
return new NativeControlHostTopLevelAttachment();
}
}
class NativeControlHostTopLevelAttachment {
_child?: HTMLElement;
_host?: HTMLElement;
InitializeWithChildHandle(child: HTMLElement) {
this._child = child;
this._child.style.position = "absolute";
}
AttachTo(host: HTMLElement): void {
if (this._host && this._child) {
this._host.removeChild(this._child);
}
this._host = host;
if (this._host && this._child) {
this._host.appendChild(this._child);
}
}
ShowInBounds(x: number, y: number, width: number, height: number): void {
if (this._child) {
this._child.style.top = y + "px";
this._child.style.left = x + "px";
this._child.style.width = width + "px";
this._child.style.height = height + "px";
this._child.style.display = "block";
}
}
HideWithSize(width: number, height: number): void {
if (this._child) {
this._child.style.width = width + "px";
this._child.style.height = height + "px";
this._child.style.display = "none";
}
}
ReleaseChild(): void {
if (this._child) {
this._child = undefined;
}
}
}

255
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SKHtmlCanvas.ts

@ -1,255 +0,0 @@
// 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 | undefined
} & HTMLCanvasElement
export class SKHtmlCanvas {
static elements: Map<string, HTMLCanvasElement>;
htmlCanvas: HTMLCanvasElement;
glInfo?: SKGLViewInfo;
renderFrameCallback: DotNet.DotNetObject;
renderLoopEnabled: boolean = false;
renderLoopRequest: number = 0;
newWidth?: number;
newHeight?: number;
public static initGL(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObject): SKGLViewInfo | null {
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.DotNetObject): 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.DotNetObject): SKHtmlCanvas | null {
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.set(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.get(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) {
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop);
}
public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number) {
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.setCanvasSize(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.DotNetObject) {
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;
}
// 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 setCanvasSize(width: number, height: number) {
this.newWidth = width;
this.newHeight = height;
if (this.htmlCanvas.width != this.newWidth) {
this.htmlCanvas.width = this.newWidth;
}
if (this.htmlCanvas.height != this.newHeight) {
this.htmlCanvas.height = this.newHeight;
}
if (this.glInfo) {
// make current
GL.makeContextCurrent(this.glInfo.context);
}
}
public requestAnimationFrame(renderLoop?: boolean) {
// optionally update the render loop
if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop)
this.setEnableRenderLoop(renderLoop);
// 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);
}
if (this.htmlCanvas.width != this.newWidth) {
this.htmlCanvas.width = this.newWidth || 0;
}
if (this.htmlCanvas.height != this.newHeight) {
this.htmlCanvas.height = this.newHeight || 0;
}
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;
}
}

67
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SizeWatcher.ts

@ -1,67 +0,0 @@
type SizeWatcherElement = {
SizeWatcher: SizeWatcherInstance;
} & HTMLElement
type SizeWatcherInstance = {
callback: DotNet.DotNetObject;
}
export class SizeWatcher {
static observer: ResizeObserver;
static elements: Map<string, HTMLElement>;
public static observe(element: HTMLElement, elementId: string, callback: DotNet.DotNetObject) {
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.set(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.get(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);
}
}

1
src/Web/Avalonia.Web.Blazor/webapp/modules/Storage.ts

@ -1 +0,0 @@
export { StorageProvider } from "./Storage/StorageProvider"

79
src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/IndexedDbWrapper.ts

@ -1,79 +0,0 @@
class InnerDbConnection {
constructor(private database: IDBDatabase) { }
private openStore(store: string, mode: IDBTransactionMode): IDBObjectStore {
const tx = this.database.transaction(store, mode);
return tx.objectStore(store);
}
public put(store: string, obj: any, key?: IDBValidKey): Promise<IDBValidKey> {
const os = this.openStore(store, "readwrite");
return new Promise((resolve, reject) => {
const response = os.put(obj, key);
response.onsuccess = () => {
resolve(response.result);
};
response.onerror = () => {
reject(response.error);
};
});
}
public get(store: string, key: IDBValidKey): any {
const os = this.openStore(store, "readonly");
return new Promise((resolve, reject) => {
const response = os.get(key);
response.onsuccess = () => {
resolve(response.result);
};
response.onerror = () => {
reject(response.error);
};
});
}
public delete(store: string, key: IDBValidKey): Promise<void> {
const os = this.openStore(store, "readwrite");
return new Promise((resolve, reject) => {
const response = os.delete(key);
response.onsuccess = () => {
resolve();
};
response.onerror = () => {
reject(response.error);
};
});
}
public close() {
this.database.close();
}
}
export class IndexedDbWrapper {
constructor(private databaseName: string, private objectStores: [string]) {
}
public connect(): Promise<InnerDbConnection> {
const conn = window.indexedDB.open(this.databaseName, 1);
conn.onupgradeneeded = event => {
const db = (<IDBRequest<IDBDatabase>>event.target).result;
this.objectStores.forEach(store => {
db.createObjectStore(store);
});
};
return new Promise((resolve, reject) => {
conn.onsuccess = event => {
resolve(new InnerDbConnection((<IDBRequest<IDBDatabase>>event.target).result));
};
conn.onerror = event => {
reject((<IDBRequest<IDBDatabase>>event.target).error);
};
});
}
}

204
src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts

@ -1,204 +0,0 @@
import { IndexedDbWrapper } from "./IndexedDbWrapper";
declare global {
type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
type StartInDirectory = WellKnownDirectory | FileSystemHandle;
interface OpenFilePickerOptions {
startIn?: StartInDirectory
}
interface SaveFilePickerOptions {
startIn?: StartInDirectory
}
}
const fileBookmarksStore: string = "fileBookmarks";
const avaloniaDb = new IndexedDbWrapper("AvaloniaDb", [
fileBookmarksStore
]);
class StorageItem {
constructor(public handle: FileSystemHandle, private bookmarkId?: string) { }
public getName(): string {
return this.handle.name
}
public getKind(): string {
return this.handle.kind;
}
public async openRead(): Promise<Blob> {
if (!(this.handle instanceof FileSystemFileHandle)) {
throw new Error("StorageItem is not a file");
}
await this.verityPermissions('read');
const file = await this.handle.getFile();
return file;
}
public async openWrite(): Promise<FileSystemWritableFileStream> {
if (!(this.handle instanceof FileSystemFileHandle)) {
throw new Error("StorageItem is not a file");
}
await this.verityPermissions('readwrite');
return await this.handle.createWritable({ keepExistingData: true });
}
public async getProperties(): Promise<{ Size: number, LastModified: number, Type: string } | null> {
const file = this.handle instanceof FileSystemFileHandle
&& await this.handle.getFile();
if (!file) {
return null;
}
return {
Size: file.size,
LastModified: file.lastModified,
Type: file.type
}
}
public async getItems(): Promise<StorageItems> {
if (this.handle.kind !== "directory"){
return new StorageItems([]);
}
const items: StorageItem[] = [];
for await (const [key, value] of (this.handle as any).entries()) {
items.push(new StorageItem(value));
}
return new StorageItems(items);
}
private async verityPermissions(mode: FileSystemPermissionMode): Promise<void | never> {
if (await this.handle.queryPermission({ mode }) === 'granted') {
return;
}
if (await this.handle.requestPermission({ mode }) === "denied") {
throw new Error("Read permissions denied");
}
}
public async saveBookmark(): Promise<string> {
// If file was previously bookmarked, just return old one.
if (this.bookmarkId) {
return this.bookmarkId;
}
const connection = await avaloniaDb.connect();
try {
const key = await connection.put(fileBookmarksStore, this.handle, this.generateBookmarkId());
return <string>key;
}
finally {
connection.close();
}
}
public async deleteBookmark(): Promise<void> {
if (!this.bookmarkId) {
return;
}
const connection = await avaloniaDb.connect();
try {
const key = await connection.delete(fileBookmarksStore, this.bookmarkId);
}
finally {
connection.close();
}
}
private generateBookmarkId(): string {
return Date.now().toString(36) + Math.random().toString(36).substring(2);
}
}
class StorageItems {
constructor(private items: StorageItem[]) { }
public count(): number {
return this.items.length;
}
public at(index: number): StorageItem {
return this.items[index];
}
}
export class StorageProvider {
public static canOpen(): boolean {
return typeof window.showOpenFilePicker !== 'undefined';
}
public static canSave(): boolean {
return typeof window.showSaveFilePicker !== 'undefined';
}
public static canPickFolder(): boolean {
return typeof window.showDirectoryPicker !== 'undefined';
}
public static async selectFolderDialog(
startIn: StorageItem | null)
: Promise<StorageItem> {
// 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined.
const options: DirectoryPickerOptions = {
startIn: (startIn?.handle || undefined)
};
const handle = await window.showDirectoryPicker(options);
return new StorageItem(handle);
}
public static async openFileDialog(
startIn: StorageItem | null, multiple: boolean,
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean)
: Promise<StorageItems> {
const options: OpenFilePickerOptions = {
startIn: (startIn?.handle || undefined),
multiple,
excludeAcceptAllOption,
types: (types || undefined)
};
const handles = await window.showOpenFilePicker(options);
return new StorageItems(handles.map((handle: FileSystemHandle) => new StorageItem(handle)));
}
public static async saveFileDialog(
startIn: StorageItem | null, suggestedName: string | null,
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean)
: Promise<StorageItem> {
const options: SaveFilePickerOptions = {
startIn: (startIn?.handle || undefined),
suggestedName: (suggestedName || undefined),
excludeAcceptAllOption,
types: (types || undefined)
};
const handle = await window.showSaveFilePicker(options);
return new StorageItem(handle);
}
public static async openBookmark(key: string): Promise<StorageItem | null> {
const connection = await avaloniaDb.connect();
try {
const handle = await connection.get(fileBookmarksStore, key);
return handle && new StorageItem(handle, key);
}
finally {
connection.close();
}
}
}

13
src/Web/Avalonia.Web.Blazor/webapp/package.json

@ -1,13 +0,0 @@
{
"name": "avalonia.web",
"scripts": {
"prebuild": "tsc -noEmit",
"build": "node build.js"
},
"devDependencies": {
"@types/emscripten": "^1.39.6",
"@types/wicg-file-system-access": "^2020.9.5",
"typescript": "^4.7.4",
"esbuild": "^0.15.7"
}
}

18
src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json

@ -1,18 +0,0 @@
{
"compilerOptions": {
"target": "es2016",
"strict": true,
"sourceMap": true,
"outDir": "../wwwroot",
"noEmitOnError": true,
"isolatedModules": true, // we need it for esbuild
"lib": [
"dom",
"es2016",
"esnext.asynciterable"
]
},
"exclude": [
"node_modules"
]
}

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

@ -1,56 +0,0 @@
// 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>;
}
}

41
src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj

@ -1,41 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<WasmMainJSPath>main.js</WasmMainJSPath>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver>
<WasmBuildNative>true</WasmBuildNative>
<EmccFlags>-sVERBOSE -sERROR_ON_UNDEFINED_SYMBOLS=0</EmccFlags>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<RunAOTCompilation>true</RunAOTCompilation>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>
<WasmBuildNative>true</WasmBuildNative>
<InvariantGlobalization>true</InvariantGlobalization>
<WasmEnableSIMD>true</WasmEnableSIMD>
<EmccCompileOptimizationFlag>-O3</EmccCompileOptimizationFlag>
<EmccLinkOptimizationFlag>-O3</EmccLinkOptimizationFlag>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\samples\ControlCatalog\ControlCatalog.csproj" />
<ProjectReference Include="..\Avalonia.Web\Avalonia.Web.csproj" />
<ProjectReference Include="..\..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
</ItemGroup>
<ItemGroup>
<WasmExtraFilesToDeploy Include="index.html" />
<WasmExtraFilesToDeploy Include="main.js" />
<WasmExtraFilesToDeploy Include="embed.js" />
<WasmExtraFilesToDeploy Include="favicon.ico" />
<WasmExtraFilesToDeploy Include="Logo.svg" />
<WasmExtraFilesToDeploy Include="app.css" />
</ItemGroup>
<Import Project="..\Avalonia.Web\Avalonia.Web.props" />
<Import Project="..\Avalonia.Web\Avalonia.Web.targets" />
</Project>

44
src/Web/Avalonia.Web.Sample/EmbedSample.Browser.cs

@ -1,44 +0,0 @@
using System;
using System.Runtime.InteropServices.JavaScript;
using Avalonia;
using Avalonia.Platform;
using Avalonia.Web;
using ControlCatalog.Pages;
namespace ControlCatalog.Web;
public class EmbedSampleWeb : INativeDemoControl
{
public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault)
{
if (isSecond)
{
var iframe = EmbedInterop.CreateElement("iframe");
iframe.SetProperty("src", "https://www.youtube.com/embed/kZCIporjJ70");
return new JSObjectControlHandle(iframe);
}
else
{
var defaultHandle = (JSObjectControlHandle)createDefault();
_ = JSHost.ImportAsync("embed.js", "./embed.js").ContinueWith(_ =>
{
EmbedInterop.AddAppButton(defaultHandle.Object);
});
return defaultHandle;
}
}
}
internal static partial class EmbedInterop
{
[JSImport("globalThis.document.createElement")]
public static partial JSObject CreateElement(string tagName);
[JSImport("addAppButton", "embed.js")]
public static partial void AddAppButton(JSObject parentObject);
}

19
src/Web/Avalonia.Web.Sample/Program.cs

@ -1,19 +0,0 @@
using Avalonia;
using Avalonia.Web;
using ControlCatalog;
using ControlCatalog.Web;
internal partial class Program
{
private static void Main(string[] args)
{
BuildAvaloniaApp()
.AfterSetup(_ =>
{
ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb();
}).SetupBrowserApp("out");
}
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>();
}

8
src/Web/Avalonia.Web/Avalonia.Web.csproj

@ -39,10 +39,6 @@
</Content>
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<Target Name="NpmInstall" Inputs="webapp/package.json" Outputs="webapp/node_modules/.install-stamp">
<Exec Command="npm install" WorkingDirectory="webapp" />
<!-- Write the stamp file, so incremental builds work -->
@ -52,4 +48,8 @@
<Exec Command="npm run build" WorkingDirectory="webapp" />
</Target>
<ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.Web.Blazor, PublicKey=$(AvaloniaPublicKey)" />
</ItemGroup>
</Project>

22
src/Web/Avalonia.Web/AvaloniaView.cs

@ -37,16 +37,14 @@ namespace Avalonia.Web
private bool _useGL;
private ITextInputMethodClient? _client;
private static int _canvasCount;
public AvaloniaView(string divId)
: this(DomHelper.GetElementById(divId) ?? throw new Exception($"Element with id {divId} was not found in the html document."))
{
var host = DomHelper.GetElementById(divId);
if (host == null)
{
throw new Exception($"Element with id {divId} was not found in the html document.");
}
}
public AvaloniaView(JSObject host)
{
var hostContent = DomHelper.CreateAvaloniaHost(host);
if (hostContent == null)
{
@ -64,8 +62,6 @@ namespace Avalonia.Web
_splash = DomHelper.GetElementById("avalonia-splash");
_canvas.SetProperty("id", $"avaloniaCanvas{_canvasCount++}");
_topLevelImpl = new BrowserTopLevelImpl(this);
_topLevel = new WebEmbeddableControlRoot(_topLevelImpl, () =>
@ -137,7 +133,7 @@ namespace Avalonia.Web
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
DomHelper.ObserveSize(host, divId, OnSizeChanged);
DomHelper.ObserveSize(host, null, OnSizeChanged);
CanvasHelper.RequestAnimationFrame(_canvas, true);
}
@ -387,7 +383,7 @@ namespace Avalonia.Web
InputHelper.FocusElement(_containerElement);
}
public void SetClient(ITextInputMethodClient? client)
void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client)
{
Console.WriteLine("Set Client");
if (_client != null)
@ -431,18 +427,18 @@ namespace Avalonia.Web
}
}
public void SetCursorRect(Rect rect)
void ITextInputMethodImpl.SetCursorRect(Rect rect)
{
InputHelper.FocusElement(_inputElement);
InputHelper.SetBounds(_inputElement, (int)rect.X, (int)rect.Y, (int)rect.Width, (int)rect.Height, _client?.SurroundingText.CursorOffset ?? 0);
InputHelper.FocusElement(_inputElement);
}
public void SetOptions(TextInputOptions options)
void ITextInputMethodImpl.SetOptions(TextInputOptions options)
{
}
public void Reset()
void ITextInputMethodImpl.Reset()
{
InputHelper.ClearInputElement(_inputElement);
InputHelper.SetSurroundingText(_inputElement, "", 0, 0);

71
src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs

@ -1,41 +1,54 @@
using System.Runtime.InteropServices.JavaScript;
using System;
using System;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Media;
using Avalonia.Web.Skia;
using System.Runtime.Versioning;
namespace Avalonia.Web
namespace Avalonia.Web;
[SupportedOSPlatform("browser")]
public class BrowserSingleViewLifetime : ISingleViewApplicationLifetime
{
[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings
public class BrowserSingleViewLifetime : ISingleViewApplicationLifetime
{
public AvaloniaView? View;
public AvaloniaView? View;
public Control? MainView
{
get => View!.Content;
set => View!.Content = value;
}
public Control? MainView
{
get => View!.Content;
set => View!.Content = value;
}
}
public static partial class WebAppBuilder
{
public static T SetupBrowserApp<T>(
public class BrowserPlatformOptions
{
public Func<string, string> FrameworkAssetPathResolver { get; set; } = new(fileName => $"./{fileName}");
}
[SupportedOSPlatform("browser")]
public static class WebAppBuilder
{
public static T SetupBrowserApp<T>(
this T builder, string mainDivId)
where T : AppBuilderBase<T>, new()
{
var lifetime = new BrowserSingleViewLifetime();
return builder
.UseWindowingSubsystem(BrowserWindowingPlatform.Register)
.UseSkia()
.With(new SkiaOptions { CustomGpuFactory = () => new BrowserSkiaGpu() })
.AfterSetup(b =>
{
lifetime.View = new AvaloniaView(mainDivId);
})
.SetupWithLifetime(lifetime);
}
{
var lifetime = new BrowserSingleViewLifetime();
return builder
.UseBrowser()
.AfterSetup(b =>
{
lifetime.View = new AvaloniaView(mainDivId);
})
.SetupWithLifetime(lifetime);
}
public static T UseBrowser<T>(
this T builder)
where T : AppBuilderBase<T>, new()
{
return builder
.UseWindowingSubsystem(BrowserWindowingPlatform.Register)
.UseSkia()
.With(new SkiaOptions { CustomGpuFactory = () => new BrowserSkiaGpu() });
}
}

2
src/Web/Avalonia.Web/Cursor.cs

@ -5,7 +5,7 @@ using Avalonia.Platform;
namespace Avalonia.Web
{
public class CssCursor : ICursorImpl
internal class CssCursor : ICursorImpl
{
public const string Default = "default";
public string? Value { get; set; }

22
src/Web/Avalonia.Web/Interop/AvaloniaModule.cs

@ -0,0 +1,22 @@
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
namespace Avalonia.Web.Interop;
internal static class AvaloniaModule
{
public const string MainModuleName = "avalonia";
public const string StorageModuleName = "storage";
public static Task ImportMain()
{
var options = AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions();
return JSHost.ImportAsync(MainModuleName, options.FrameworkAssetPathResolver("avalonia.js"));
}
public static Task ImportStorage()
{
var options = AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions();
return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver("storage.js"));
}
}

6
src/Web/Avalonia.Web/Interop/CanvasHelper.cs

@ -29,13 +29,13 @@ internal static partial class CanvasHelper
return glInfo;
}
[JSImport("Canvas.requestAnimationFrame", "avalonia.ts")]
[JSImport("Canvas.requestAnimationFrame", AvaloniaModule.MainModuleName)]
public static partial void RequestAnimationFrame(JSObject canvas, bool renderLoop);
[JSImport("Canvas.setCanvasSize", "avalonia.ts")]
[JSImport("Canvas.setCanvasSize", AvaloniaModule.MainModuleName)]
public static partial void SetCanvasSize(JSObject canvas, int height, int width);
[JSImport("Canvas.initGL", "avalonia.ts")]
[JSImport("Canvas.initGL", AvaloniaModule.MainModuleName)]
private static partial JSObject InitGL(
JSObject canvas,
string canvasId,

10
src/Web/Avalonia.Web/Interop/DomHelper.cs

@ -8,20 +8,20 @@ internal static partial class DomHelper
[JSImport("globalThis.document.getElementById")]
internal static partial JSObject? GetElementById(string id);
[JSImport("AvaloniaDOM.createAvaloniaHost", "avalonia.ts")]
[JSImport("AvaloniaDOM.createAvaloniaHost", AvaloniaModule.MainModuleName)]
public static partial JSObject CreateAvaloniaHost(JSObject element);
[JSImport("AvaloniaDOM.addClass", "avalonia.ts")]
[JSImport("AvaloniaDOM.addClass", AvaloniaModule.MainModuleName)]
public static partial void AddCssClass(JSObject element, string className);
[JSImport("SizeWatcher.observe", "avalonia.ts")]
[JSImport("SizeWatcher.observe", AvaloniaModule.MainModuleName)]
public static partial JSObject ObserveSize(
JSObject canvas,
string canvasId,
string? canvasId,
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Number>>]
Action<int, int> onSizeChanged);
[JSImport("DpiWatcher.start", "avalonia.ts")]
[JSImport("DpiWatcher.start", AvaloniaModule.MainModuleName)]
public static partial double ObserveDpi(
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Number>>]
Action<double, double> onDpiChanged);

24
src/Web/Avalonia.Web/Interop/InputHelper.cs

@ -6,7 +6,7 @@ namespace Avalonia.Web.Interop;
internal static partial class InputHelper
{
[JSImport("InputHelper.subscribeKeyEvents", "avalonia.ts")]
[JSImport("InputHelper.subscribeKeyEvents", AvaloniaModule.MainModuleName)]
public static partial void SubscribeKeyEvents(
JSObject htmlElement,
[JSMarshalAs<JSType.Function<JSType.String, JSType.String, JSType.Number, JSType.Boolean>>]
@ -14,7 +14,7 @@ internal static partial class InputHelper
[JSMarshalAs<JSType.Function<JSType.String, JSType.String, JSType.Number, JSType.Boolean>>]
Func<string, string, int, bool> keyUp);
[JSImport("InputHelper.subscribeTextEvents", "avalonia.ts")]
[JSImport("InputHelper.subscribeTextEvents", AvaloniaModule.MainModuleName)]
public static partial void SubscribeTextEvents(
JSObject htmlElement,
[JSMarshalAs<JSType.Function<JSType.String, JSType.String, JSType.Boolean>>]
@ -26,7 +26,7 @@ internal static partial class InputHelper
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
Func<JSObject, bool> onCompositionEnd);
[JSImport("InputHelper.subscribePointerEvents", "avalonia.ts")]
[JSImport("InputHelper.subscribePointerEvents", AvaloniaModule.MainModuleName)]
public static partial void SubscribePointerEvents(
JSObject htmlElement,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
@ -39,35 +39,35 @@ internal static partial class InputHelper
Func<JSObject, bool> wheel);
[JSImport("InputHelper.subscribeInputEvents", "avalonia.ts")]
[JSImport("InputHelper.subscribeInputEvents", AvaloniaModule.MainModuleName)]
public static partial void SubscribeInputEvents(
JSObject htmlElement,
[JSMarshalAs<JSType.Function<JSType.String, JSType.Boolean>>]
Func<string, bool> input);
[JSImport("InputHelper.clearInput", "avalonia.ts")]
[JSImport("InputHelper.clearInput", AvaloniaModule.MainModuleName)]
public static partial void ClearInputElement(JSObject htmlElement);
[JSImport("InputHelper.isInputElement", "avalonia.ts")]
[JSImport("InputHelper.isInputElement", AvaloniaModule.MainModuleName)]
public static partial void IsInputElement(JSObject htmlElement);
[JSImport("InputHelper.focusElement", "avalonia.ts")]
[JSImport("InputHelper.focusElement", AvaloniaModule.MainModuleName)]
public static partial void FocusElement(JSObject htmlElement);
[JSImport("InputHelper.setCursor", "avalonia.ts")]
[JSImport("InputHelper.setCursor", AvaloniaModule.MainModuleName)]
public static partial void SetCursor(JSObject htmlElement, string kind);
[JSImport("InputHelper.hide", "avalonia.ts")]
[JSImport("InputHelper.hide", AvaloniaModule.MainModuleName)]
public static partial void HideElement(JSObject htmlElement);
[JSImport("InputHelper.show", "avalonia.ts")]
[JSImport("InputHelper.show", AvaloniaModule.MainModuleName)]
public static partial void ShowElement(JSObject htmlElement);
[JSImport("InputHelper.setSurroundingText", "avalonia.ts")]
[JSImport("InputHelper.setSurroundingText", AvaloniaModule.MainModuleName)]
public static partial void SetSurroundingText(JSObject htmlElement, string text, int start, int end);
[JSImport("InputHelper.setBounds", "avalonia.ts")]
[JSImport("InputHelper.setBounds", AvaloniaModule.MainModuleName)]
public static partial void SetBounds(JSObject htmlElement, int x, int y, int width, int height, int caret);
[JSImport("globalThis.navigator.clipboard.readText")]

14
src/Web/Avalonia.Web/Interop/NativeControlHostHelper.cs

@ -5,24 +5,24 @@ namespace Avalonia.Web.Interop;
internal static partial class NativeControlHostHelper
{
[JSImport("NativeControlHost.createDefaultChild", "avalonia.ts")]
[JSImport("NativeControlHost.createDefaultChild", AvaloniaModule.MainModuleName)]
internal static partial JSObject CreateDefaultChild(JSObject? parent);
[JSImport("NativeControlHost.createAttachment", "avalonia.ts")]
[JSImport("NativeControlHost.createAttachment", AvaloniaModule.MainModuleName)]
internal static partial JSObject CreateAttachment();
[JSImport("NativeControlHost.initializeWithChildHandle", "avalonia.ts")]
[JSImport("NativeControlHost.initializeWithChildHandle", AvaloniaModule.MainModuleName)]
internal static partial void InitializeWithChildHandle(JSObject element, JSObject child);
[JSImport("NativeControlHost.attachTo", "avalonia.ts")]
[JSImport("NativeControlHost.attachTo", AvaloniaModule.MainModuleName)]
internal static partial void AttachTo(JSObject element, JSObject? host);
[JSImport("NativeControlHost.showInBounds", "avalonia.ts")]
[JSImport("NativeControlHost.showInBounds", AvaloniaModule.MainModuleName)]
internal static partial void ShowInBounds(JSObject element, double x, double y, double width, double height);
[JSImport("NativeControlHost.hideWithSize", "avalonia.ts")]
[JSImport("NativeControlHost.hideWithSize", AvaloniaModule.MainModuleName)]
internal static partial void HideWithSize(JSObject element, double width, double height);
[JSImport("NativeControlHost.releaseChild", "avalonia.ts")]
[JSImport("NativeControlHost.releaseChild", AvaloniaModule.MainModuleName)]
internal static partial void ReleaseChild(JSObject element);
}

30
src/Web/Avalonia.Web/Interop/StorageHelper.cs

@ -5,51 +5,51 @@ namespace Avalonia.Web.Interop;
internal static partial class StorageHelper
{
[JSImport("Caniuse.canShowOpenFilePicker", "avalonia.ts")]
[JSImport("Caniuse.canShowOpenFilePicker", AvaloniaModule.MainModuleName)]
public static partial bool CanShowOpenFilePicker();
[JSImport("Caniuse.canShowSaveFilePicker", "avalonia.ts")]
[JSImport("Caniuse.canShowSaveFilePicker", AvaloniaModule.MainModuleName)]
public static partial bool CanShowSaveFilePicker();
[JSImport("Caniuse.canShowDirectoryPicker", "avalonia.ts")]
[JSImport("Caniuse.canShowDirectoryPicker", AvaloniaModule.MainModuleName)]
public static partial bool CanShowDirectoryPicker();
[JSImport("StorageProvider.selectFolderDialog", "storage.ts")]
[JSImport("StorageProvider.selectFolderDialog", AvaloniaModule.StorageModuleName)]
public static partial Task<JSObject?> SelectFolderDialog(JSObject? startIn);
[JSImport("StorageProvider.openFileDialog", "storage.ts")]
[JSImport("StorageProvider.openFileDialog", AvaloniaModule.StorageModuleName)]
public static partial Task<JSObject?> OpenFileDialog(JSObject? startIn, bool multiple,
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption);
[JSImport("StorageProvider.saveFileDialog", "storage.ts")]
[JSImport("StorageProvider.saveFileDialog", AvaloniaModule.StorageModuleName)]
public static partial Task<JSObject?> SaveFileDialog(JSObject? startIn, string? suggestedName,
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption);
[JSImport("StorageProvider.openBookmark", "storage.ts")]
[JSImport("StorageProvider.openBookmark", AvaloniaModule.StorageModuleName)]
public static partial Task<JSObject?> OpenBookmark(string key);
[JSImport("StorageItem.saveBookmark", "storage.ts")]
[JSImport("StorageItem.saveBookmark", AvaloniaModule.StorageModuleName)]
public static partial Task<string?> SaveBookmark(JSObject item);
[JSImport("StorageItem.deleteBookmark", "storage.ts")]
[JSImport("StorageItem.deleteBookmark", AvaloniaModule.StorageModuleName)]
public static partial Task DeleteBookmark(JSObject item);
[JSImport("StorageItem.getProperties", "storage.ts")]
[JSImport("StorageItem.getProperties", AvaloniaModule.StorageModuleName)]
public static partial Task<JSObject?> GetProperties(JSObject item);
[JSImport("StorageItem.openWrite", "storage.ts")]
[JSImport("StorageItem.openWrite", AvaloniaModule.StorageModuleName)]
public static partial Task<JSObject> OpenWrite(JSObject item);
[JSImport("StorageItem.openRead", "storage.ts")]
[JSImport("StorageItem.openRead", AvaloniaModule.StorageModuleName)]
public static partial Task<JSObject> OpenRead(JSObject item);
[JSImport("StorageItem.getItems", "storage.ts")]
[JSImport("StorageItem.getItems", AvaloniaModule.StorageModuleName)]
[return: JSMarshalAs<JSType.Promise<JSType.Object>>]
public static partial Task<JSObject> GetItems(JSObject item);
[JSImport("StorageItems.itemsArray", "storage.ts")]
[JSImport("StorageItems.itemsArray", AvaloniaModule.StorageModuleName)]
public static partial JSObject[] ItemsArray(JSObject item);
[JSImport("StorageProvider.createAcceptType", "storage.ts")]
[JSImport("StorageProvider.createAcceptType", AvaloniaModule.StorageModuleName)]
public static partial JSObject CreateAcceptType(string description, string[] mimeTypes);
}

16
src/Web/Avalonia.Web/Interop/StreamHelper.cs

@ -2,33 +2,33 @@
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
namespace Avalonia.Web.Storage;
namespace Avalonia.Web.Interop;
/// <summary>
/// Set of FileSystemWritableFileStream and Blob methods.
/// </summary>
internal static partial class StreamHelper
{
[JSImport("StreamHelper.seek", "avalonia.ts")]
[JSImport("StreamHelper.seek", AvaloniaModule.MainModuleName)]
public static partial void Seek(JSObject stream, [JSMarshalAs<JSType.Number>] long position);
[JSImport("StreamHelper.truncate", "avalonia.ts")]
[JSImport("StreamHelper.truncate", AvaloniaModule.MainModuleName)]
public static partial void Truncate(JSObject stream, [JSMarshalAs<JSType.Number>] long size);
[JSImport("StreamHelper.write", "avalonia.ts")]
[JSImport("StreamHelper.write", AvaloniaModule.MainModuleName)]
public static partial Task WriteAsync(JSObject stream, [JSMarshalAs<JSType.MemoryView>] ArraySegment<byte> data);
[JSImport("StreamHelper.close", "avalonia.ts")]
[JSImport("StreamHelper.close", AvaloniaModule.MainModuleName)]
public static partial Task CloseAsync(JSObject stream);
[JSImport("StreamHelper.byteLength", "avalonia.ts")]
[JSImport("StreamHelper.byteLength", AvaloniaModule.MainModuleName)]
[return: JSMarshalAs<JSType.Number>]
public static partial long ByteLength(JSObject stream);
[JSImport("StreamHelper.sliceArrayBuffer", "avalonia.ts")]
[JSImport("StreamHelper.sliceArrayBuffer", AvaloniaModule.MainModuleName)]
private static partial Task<JSObject> SliceToArrayBuffer(JSObject stream, [JSMarshalAs<JSType.Number>] long offset, int count);
[JSImport("StreamHelper.toMemoryView", "avalonia.ts")]
[JSImport("StreamHelper.toMemoryView", AvaloniaModule.MainModuleName)]
[return: JSMarshalAs<JSType.Array<JSType.Number>>]
private static partial byte[] ArrayBufferToMemoryView(JSObject stream);

2
src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs

@ -4,7 +4,7 @@ using Avalonia.Rendering;
namespace Avalonia.Web
{
public class ManualTriggerRenderTimer : IRenderTimer
internal class ManualTriggerRenderTimer : IRenderTimer
{
private static readonly Stopwatch s_sw = Stopwatch.StartNew();

2
src/Web/Avalonia.Web/Storage/BlobReadableStream.cs

@ -4,6 +4,8 @@ using System.Runtime.InteropServices.JavaScript;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Web.Interop;
namespace Avalonia.Web.Storage;
[System.Runtime.Versioning.SupportedOSPlatform("browser")]

12
src/Web/Avalonia.Web/Storage/BrowserStorageProvider.cs

@ -20,7 +20,7 @@ internal class BrowserStorageProvider : IStorageProvider
internal const string PickerCancelMessage = "The user aborted a request";
internal const string NoPermissionsMessage = "Permissions denied";
private readonly Lazy<Task<JSObject>> _lazyModule = new(() => JSHost.ImportAsync("storage.ts", "./storage.js"));
private readonly Lazy<Task> _lazyModule = new(() => AvaloniaModule.ImportStorage());
public bool CanOpen => StorageHelper.CanShowOpenFilePicker();
public bool CanSave => StorageHelper.CanShowSaveFilePicker();
@ -28,7 +28,7 @@ internal class BrowserStorageProvider : IStorageProvider
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
_ = await _lazyModule.Value;
await _lazyModule.Value;
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle;
var (types, exludeAll) = ConvertFileTypes(options.FileTypeFilter);
@ -62,7 +62,7 @@ internal class BrowserStorageProvider : IStorageProvider
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
_ = await _lazyModule.Value;
await _lazyModule.Value;
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle;
var (types, exludeAll) = ConvertFileTypes(options.FileTypeChoices);
@ -90,7 +90,7 @@ internal class BrowserStorageProvider : IStorageProvider
public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
_ = await _lazyModule.Value;
await _lazyModule.Value;
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle;
try
@ -106,14 +106,14 @@ internal class BrowserStorageProvider : IStorageProvider
public async Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark)
{
_ = await _lazyModule.Value;
await _lazyModule.Value;
var item = await StorageHelper.OpenBookmark(bookmark);
return item is not null ? new JSStorageFile(item) : null;
}
public async Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark)
{
_ = await _lazyModule.Value;
await _lazyModule.Value;
var item = await StorageHelper.OpenBookmark(bookmark);
return item is not null ? new JSStorageFolder(item) : null;
}

2
src/Web/Avalonia.Web/Storage/WriteableStream.cs

@ -4,6 +4,8 @@ using System.Runtime.InteropServices.JavaScript;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Web.Interop;
namespace Avalonia.Web.Storage;
[System.Runtime.Versioning.SupportedOSPlatform("browser")]

2
src/Web/Avalonia.Web/WindowingPlatform.cs

@ -8,7 +8,7 @@ using Avalonia.Threading;
namespace Avalonia.Web
{
public class BrowserWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface
internal class BrowserWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface
{
private bool _signaled;
private static KeyboardDevice? s_keyboard;

16
src/Web/Avalonia.Web/webapp/modules/avalonia.ts

@ -6,8 +6,8 @@ import { Caniuse } from "./avalonia/caniuse";
import { StreamHelper } from "./avalonia/stream";
import { NativeControlHost } from "./avalonia/nativeControlHost";
export async function createAvaloniaRuntime(api: RuntimeAPI): Promise<void> {
api.setModuleImports("avalonia.ts", {
async function registerAvaloniaModule(api: RuntimeAPI): Promise<void> {
api.setModuleImports("avalonia", {
Caniuse,
Canvas,
InputHelper,
@ -18,3 +18,15 @@ export async function createAvaloniaRuntime(api: RuntimeAPI): Promise<void> {
NativeControlHost
});
}
export {
Caniuse,
Canvas,
InputHelper,
SizeWatcher,
DpiWatcher,
AvaloniaDOM,
StreamHelper,
NativeControlHost,
registerAvaloniaModule
};

4
src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts

@ -211,7 +211,7 @@ export class SizeWatcher {
static observer: ResizeObserver;
static elements: Map<string, HTMLElement>;
public static observe(element: HTMLElement, elementId: string, callback: (width: number, height: number) => void): void {
public static observe(element: HTMLElement, elementId: string | undefined, callback: (width: number, height: number) => void): void {
if (!element || !callback) {
return;
}
@ -223,7 +223,7 @@ export class SizeWatcher {
callback
};
SizeWatcher.elements.set(elementId, element);
SizeWatcher.elements.set(elementId ?? element.id, element);
SizeWatcher.observer.observe(element);
SizeWatcher.invoke(element);

7
src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts

@ -4,13 +4,17 @@ export class AvaloniaDOM {
}
static createAvaloniaHost(host: HTMLElement) {
const randomIdPart = Math.random().toString(36).replace(/[^a-z]+/g, "").substr(2, 10);
// Root element
host.classList.add("avalonia-container");
host.tabIndex = 0;
host.oncontextmenu = function () { return false; };
host.style.overflow = "hidden";
// Rendering target canvas
const canvas = document.createElement("canvas");
canvas.id = `canvas${randomIdPart}`;
canvas.classList.add("avalonia-canvas");
canvas.style.backgroundColor = "#ccc";
canvas.style.width = "100%";
@ -19,6 +23,7 @@ export class AvaloniaDOM {
// Native controls host
const nativeHost = document.createElement("div");
canvas.id = `nativeHost${randomIdPart}`;
nativeHost.classList.add("avalonia-native-host");
nativeHost.style.left = "0px";
nativeHost.style.top = "0px";
@ -28,6 +33,7 @@ export class AvaloniaDOM {
// IME
const inputElement = document.createElement("input");
canvas.id = `input${randomIdPart}`;
inputElement.classList.add("avalonia-input-element");
inputElement.autocapitalize = "none";
inputElement.type = "text";
@ -42,6 +48,7 @@ export class AvaloniaDOM {
inputElement.style.color = "transparent";
inputElement.style.display = "none";
inputElement.style.height = "20px";
inputElement.style.zIndex = "-1";
inputElement.onpaste = function () { return false; };
inputElement.oncopy = function () { return false; };
inputElement.oncut = function () { return false; };

Loading…
Cancel
Save