Browse Source

Merge branch 'master' into features/Core/Logging/LogArea

pull/9113/head
Max Katz 4 years ago
committed by GitHub
parent
commit
c8d2d71aff
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitignore
  2. 18
      Avalonia.sln
  3. 30
      azure-pipelines.yml
  4. 4
      global.json
  5. 12
      nukebuild/BuildTasksPatcher.cs
  6. 8
      nukebuild/_build.csproj
  7. 2
      src/Avalonia.Base/Platform/Storage/IStorageFile.cs
  8. 1
      src/Avalonia.Base/Properties/AssemblyInfo.cs
  9. 2
      src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs
  10. 1
      src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs
  11. 1
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
  12. 41
      src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj
  13. 44
      src/Web/Avalonia.Web.Sample/EmbedSample.Browser.cs
  14. 5
      src/Web/Avalonia.Web.Sample/Logo.svg
  15. 19
      src/Web/Avalonia.Web.Sample/Program.cs
  16. 38
      src/Web/Avalonia.Web.Sample/app.css
  17. 11
      src/Web/Avalonia.Web.Sample/embed.js
  18. BIN
      src/Web/Avalonia.Web.Sample/favicon.ico
  19. 31
      src/Web/Avalonia.Web.Sample/index.html
  20. 19
      src/Web/Avalonia.Web.Sample/main.js
  21. 11
      src/Web/Avalonia.Web.Sample/runtimeconfig.template.json
  22. 55
      src/Web/Avalonia.Web/Avalonia.Web.csproj
  23. 5
      src/Web/Avalonia.Web/Avalonia.Web.props
  24. 7
      src/Web/Avalonia.Web/Avalonia.Web.targets
  25. 451
      src/Web/Avalonia.Web/AvaloniaView.cs
  26. 136
      src/Web/Avalonia.Web/BrowserNativeControlHost.cs
  27. 41
      src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs
  28. 226
      src/Web/Avalonia.Web/BrowserTopLevelImpl.cs
  29. 29
      src/Web/Avalonia.Web/ClipboardImpl.cs
  30. 95
      src/Web/Avalonia.Web/Cursor.cs
  31. 43
      src/Web/Avalonia.Web/Interop/CanvasHelper.cs
  32. 28
      src/Web/Avalonia.Web/Interop/DomHelper.cs
  33. 78
      src/Web/Avalonia.Web/Interop/InputHelper.cs
  34. 28
      src/Web/Avalonia.Web/Interop/NativeControlHostHelper.cs
  35. 55
      src/Web/Avalonia.Web/Interop/StorageHelper.cs
  36. 40
      src/Web/Avalonia.Web/Interop/StreamHelper.cs
  37. 30
      src/Web/Avalonia.Web/JSObjectControlHandle.cs
  38. 129
      src/Web/Avalonia.Web/Keycodes.cs
  39. 18
      src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs
  40. 26
      src/Web/Avalonia.Web/Skia/BrowserSkiaGpu.cs
  41. 36
      src/Web/Avalonia.Web/Skia/BrowserSkiaGpuRenderSession.cs
  42. 39
      src/Web/Avalonia.Web/Skia/BrowserSkiaGpuRenderTarget.cs
  43. 88
      src/Web/Avalonia.Web/Skia/BrowserSkiaRasterSurface.cs
  44. 30
      src/Web/Avalonia.Web/Skia/BrowserSkiaSurface.cs
  45. 9
      src/Web/Avalonia.Web/Skia/IBrowserSkiaSurface.cs
  46. 88
      src/Web/Avalonia.Web/Storage/BlobReadableStream.cs
  47. 257
      src/Web/Avalonia.Web/Storage/BrowserStorageProvider.cs
  48. 124
      src/Web/Avalonia.Web/Storage/WriteableStream.cs
  49. 68
      src/Web/Avalonia.Web/WebEmbeddableControlRoot.cs
  50. 48
      src/Web/Avalonia.Web/WinStubs.cs
  51. 105
      src/Web/Avalonia.Web/WindowingPlatform.cs
  52. 13
      src/Web/Avalonia.Web/interop.js
  53. 47
      src/Web/Avalonia.Web/webapp/.eslintrc.json
  54. 16
      src/Web/Avalonia.Web/webapp/build.js
  55. 20
      src/Web/Avalonia.Web/webapp/modules/avalonia.ts
  56. 13
      src/Web/Avalonia.Web/webapp/modules/avalonia/caniuse.ts
  57. 303
      src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts
  58. 149
      src/Web/Avalonia.Web/webapp/modules/avalonia/caretHelper.ts
  59. 60
      src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts
  60. 204
      src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts
  61. 55
      src/Web/Avalonia.Web/webapp/modules/avalonia/nativeControlHost.ts
  62. 40
      src/Web/Avalonia.Web/webapp/modules/avalonia/stream.ts
  63. 2
      src/Web/Avalonia.Web/webapp/modules/storage.ts
  64. 84
      src/Web/Avalonia.Web/webapp/modules/storage/indexedDb.ts
  65. 111
      src/Web/Avalonia.Web/webapp/modules/storage/storageItem.ts
  66. 70
      src/Web/Avalonia.Web/webapp/modules/storage/storageProvider.ts
  67. 2234
      src/Web/Avalonia.Web/webapp/package-lock.json
  68. 22
      src/Web/Avalonia.Web/webapp/package.json
  69. 19
      src/Web/Avalonia.Web/webapp/tsconfig.json
  70. 270
      src/Web/Avalonia.Web/webapp/types/dotnet.d.ts
  71. 6
      src/iOS/Avalonia.iOS/Avalonia.iOS.csproj
  72. 8
      src/iOS/Avalonia.iOS/CombinedSpan3.cs

1
.gitignore

@ -215,3 +215,4 @@ src/Web/Avalonia.Web.Blazor/Interop/Typescript/*.js
node_modules
src/Web/Avalonia.Web.Blazor/webapp/package-lock.json
src/Web/Avalonia.Web.Blazor/wwwroot
src/Web/Avalonia.Web/wwwroot

18
Avalonia.sln

@ -212,7 +212,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ColorPick
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.Tests", "tests\Avalonia.DesignerSupport.Tests\Avalonia.DesignerSupport.Tests.csproj", "{EABE2161-989B-42BF-BD8D-1E34B20C21F1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevGenerators", "src\tools\DevGenerators\DevGenerators.csproj", "{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevGenerators", "src\tools\DevGenerators\DevGenerators.csproj", "{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}"
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}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MobileSandbox", "samples\MobileSandbox\MobileSandbox.csproj", "{3B8519C1-2F51-4F12-A348-120AB91D4532}"
EndProject
@ -407,9 +411,7 @@ Global
{BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|Any CPU.Build.0 = Release|Any CPU
{3F00BC43-5095-477F-93D8-E65B08179A00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3F00BC43-5095-477F-93D8-E65B08179A00}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3F00BC43-5095-477F-93D8-E65B08179A00}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3F00BC43-5095-477F-93D8-E65B08179A00}.Release|Any CPU.Build.0 = Release|Any CPU
{41B02319-965D-4945-8005-C1A3D1224165}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{41B02319-965D-4945-8005-C1A3D1224165}.Debug|Any CPU.Build.0 = Debug|Any CPU
{41B02319-965D-4945-8005-C1A3D1224165}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -510,6 +512,14 @@ Global
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.Build.0 = Release|Any CPU
{76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{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
@ -583,6 +593,8 @@ Global
{2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{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}

30
azure-pipelines.yml

@ -6,7 +6,6 @@ jobs:
variables:
SolutionDir: '$(Build.SourcesDirectory)'
steps:
- task: PowerShell@2
displayName: Get PR Number
inputs:
@ -35,6 +34,17 @@ jobs:
inputs:
version: 6.0.401
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12'
inputs:
version: 7.0.100-rc.1.22431.12
- task: CmdLine@2
displayName: 'Install Workloads'
inputs:
script: |
dotnet workload install wasm-tools wasm-experimental
- task: CmdLine@2
displayName: 'Run Build'
inputs:
@ -60,6 +70,17 @@ jobs:
displayName: 'Use .NET Core SDK 6.0.401'
inputs:
version: 6.0.401
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12'
inputs:
version: 7.0.100-rc.1.22431.12
- task: CmdLine@2
displayName: 'Install Workloads'
inputs:
script: |
dotnet workload install wasm-tools wasm-experimental
- task: CmdLine@2
displayName: 'Generate avalonia-native'
@ -121,11 +142,16 @@ jobs:
inputs:
version: 6.0.401
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12'
inputs:
version: 7.0.100-rc.1.22431.12
- task: CmdLine@2
displayName: 'Install Workloads'
inputs:
script: |
dotnet workload install android ios
dotnet workload install android ios wasm-tools wasm-experimental
- task: CmdLine@2
displayName: 'Install Nuke'

4
global.json

@ -1,8 +1,4 @@
{
"sdk": {
"version": "6.0.401",
"rollForward": "latestFeature"
},
"msbuild-sdks": {
"Microsoft.Build.Traversal": "1.0.43",
"MSBuild.Sdk.Extras": "3.0.22",

12
nukebuild/BuildTasksPatcher.cs

@ -17,8 +17,12 @@ public class BuildTasksPatcher
{
if (entry.Name == "Avalonia.Build.Tasks.dll")
{
var temp = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".dll");
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
var temp = Path.Combine(tempDir, Guid.NewGuid() + ".dll");
var output = temp + ".output";
File.Copy(typeof(Microsoft.Build.Framework.ITask).Assembly.GetModules()[0].FullyQualifiedName,
Path.Combine(tempDir, "Microsoft.Build.Framework.dll"));
var patched = new MemoryStream();
try
{
@ -57,10 +61,8 @@ public class BuildTasksPatcher
{
try
{
if (File.Exists(temp))
File.Delete(temp);
if (File.Exists(output))
File.Delete(output);
if(Directory.Exists(tempDir))
Directory.Delete(tempDir, true);
}
catch
{

8
nukebuild/_build.csproj

@ -4,9 +4,9 @@
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<RootNamespace></RootNamespace>
<IsPackable>False</IsPackable>
<NoWarn>CS0649;CS0169</NoWarn>
<NoWarn>CS0649;CS0169;SYSLIB0011</NoWarn>
<NukeTelemetryVersion>1</NukeTelemetryVersion>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>
<Import Project="..\build\JetBrains.dotMemoryUnit.props" />
@ -40,5 +40,9 @@
<ItemGroup>
<Compile Remove="il-repack\ILRepack\Application.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="Numerge\Numerge.Console\" />
</ItemGroup>
</Project>

2
src/Avalonia.Base/Platform/Storage/IStorageFile.cs

@ -18,6 +18,7 @@ public interface IStorageFile : IStorageItem
/// <summary>
/// Opens a stream for read access.
/// </summary>
/// <exception cref="System.UnauthorizedAccessException" />
Task<Stream> OpenReadAsync();
/// <summary>
@ -28,5 +29,6 @@ public interface IStorageFile : IStorageItem
/// <summary>
/// Opens stream for writing to the file.
/// </summary>
/// <exception cref="System.UnauthorizedAccessException" />
Task<Stream> OpenWriteAsync();
}

1
src/Avalonia.Base/Properties/AssemblyInfo.cs

@ -30,4 +30,5 @@ using Avalonia.Metadata;
[assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Web.Blazor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Web, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]

2
src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs

@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Xml.Linq;
using System.Linq;

1
src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text;
using Avalonia.Markup.Xaml.PortableXaml;
using Avalonia.Utilities;

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

@ -287,7 +287,6 @@ namespace Avalonia.Web.Blazor
// create the SkiaSharp context
if (_context == null)
{
Console.WriteLine("create glcontext");
_glInterface = GRGlInterface.Create();
_context = GRContext.CreateGl(_glInterface);

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

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

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

5
src/Web/Avalonia.Web.Sample/Logo.svg

@ -0,0 +1,5 @@
<svg width="35" height="35" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M30.4661 34.928C30.5364 34.928 30.6052 34.928 30.6754 34.928C32.8596 34.928 34.654 33.2918 34.9053 31.1752L34.9356 16.9955C34.6872 7.56697 26.9662 0 17.4777 0C7.83263 0 0.0137329 7.8189 0.0137329 17.464C0.0137329 27.0059 7.66618 34.7631 17.1687 34.928H30.4661Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.5239 5.948C12.0268 5.948 7.42967 9.80117 6.286 14.954C7.38092 15.2609 8.18385 16.2664 8.18385 17.4593C8.18385 18.6523 7.38092 19.6577 6.286 19.9647C7.42966 25.1175 12.0268 28.9706 17.5239 28.9706C19.525 28.9706 21.4068 28.4601 23.0462 27.562V28.8927H29.0352V17.9365C29.0407 17.7908 29.0352 17.6063 29.0352 17.4593C29.0352 11.1018 23.8814 5.948 17.5239 5.948ZM12.0098 17.4593C12.0098 14.414 14.4786 11.9452 17.5239 11.9452C20.5693 11.9452 23.038 14.414 23.038 17.4593C23.038 20.5047 20.5693 22.9734 17.5239 22.9734C14.4786 22.9734 12.0098 20.5047 12.0098 17.4593Z" fill="#8B44AC"/>
<path d="M7.36841 17.4517C7.36841 18.4691 6.54368 19.2938 5.52631 19.2938C4.50894 19.2938 3.6842 18.4691 3.6842 17.4517C3.6842 16.4343 4.50894 15.6096 5.52631 15.6096C6.54368 15.6096 7.36841 16.4343 7.36841 17.4517Z" fill="#8B44AC"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

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

@ -0,0 +1,19 @@
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>();
}

38
src/Web/Avalonia.Web.Sample/app.css

@ -0,0 +1,38 @@
#out {
height: 100vh;
width: 100vw
}
#avalonia-splash {
position: absolute;
height: 100%;
width: 100%;
color: whitesmoke;
background: #171C2C;
font-family: 'Nunito', sans-serif;
}
#avalonia-splash a{
color: whitesmoke;
text-decoration: none;
}
.center {
display: flex;
justify-content: center;
height: 250px;
}
.splash-close {
animation: fadeOut 1s forwards;
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}

11
src/Web/Avalonia.Web.Sample/embed.js

@ -0,0 +1,11 @@
export function addAppButton(parent) {
var button = globalThis.document.createElement('button');
button.innerText = 'Hello world';
var clickCount = 0;
button.onclick = () => {
clickCount++;
button.innerText = 'Click count ' + clickCount;
};
parent.appendChild(button);
return button;
}

BIN
src/Web/Avalonia.Web.Sample/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

31
src/Web/Avalonia.Web.Sample/index.html

@ -0,0 +1,31 @@
<!DOCTYPE html>
<!-- Licensed to the .NET Foundation under one or more agreements. -->
<!-- The .NET Foundation licenses this file to you under the MIT license. -->
<html>
<head>
<title>Avalonia.Web.Sample</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="modulepreload" href="./main.js" />
<link rel="modulepreload" href="./dotnet.js" />
<link rel="modulepreload" href="./avalonia.js" />
<link rel="stylesheet" href="./app.css" />
</head>
<body style="margin: 0px">
<div id="out">
<div id="avalonia-splash">
<div class="center">
<h2>Powered by</h2>
<a class="navbar-brand" href="https://www.avaloniaui.net/" target="_blank">
<img src="Logo.svg" alt="Avalonia Logo" width="30" height="24" />
Avalonia
</a>
</div>
</div>
</div>
<script type='module' src="./main.js"></script>
</body>
</html>

19
src/Web/Avalonia.Web.Sample/main.js

@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
import { dotnet } from './dotnet.js'
import { createAvaloniaRuntime } from './avalonia.js';
const is_browser = typeof window != "undefined";
if (!is_browser) throw new Error(`Expected to be running in a browser`);
const dotnetRuntime = await dotnet
.withDiagnosticTracing(false)
.withApplicationArgumentsFromQuery()
.create();
await createAvaloniaRuntime(dotnetRuntime);
const config = dotnetRuntime.getConfig();
await dotnetRuntime.runMainAndExit(config.mainAssemblyName, ["dotnet", "is", "great!"]);

11
src/Web/Avalonia.Web.Sample/runtimeconfig.template.json

@ -0,0 +1,11 @@
{
"wasmHostProperties": {
"perHostConfig": [
{
"name": "browser",
"html-path": "index.html",
"Host": "browser"
}
]
}
}

55
src/Web/Avalonia.Web/Avalonia.Web.csproj

@ -0,0 +1,55 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</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>
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="*.props">
<Pack>true</Pack>
<PackagePath>build\</PackagePath>
</Content>
<Content Include="*.targets">
<Pack>true</Pack>
<PackagePath>build\;buildTransitive\</PackagePath>
</Content>
<Content Include="interop.js">
<Pack>true</Pack>
<PackagePath>build/interop.js;buildTransitive/interop.js</PackagePath>
</Content>
<Content Include="wwwroot/**/*.*">
<Pack>true</Pack>
<PackagePath>build\wwwroot;buildTransitive\wwwroot</PackagePath>
</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 -->
<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>
</Project>

5
src/Web/Avalonia.Web/Avalonia.Web.props

@ -0,0 +1,5 @@
<Project>
<PropertyGroup>
<EmccExtraLDFlags>$(EmccExtraLDFlags) --js-library="$(MSBuildThisFileDirectory)\interop.js"</EmccExtraLDFlags>
</PropertyGroup>
</Project>

7
src/Web/Avalonia.Web/Avalonia.Web.targets

@ -0,0 +1,7 @@
<Project>
<ItemGroup>
<WasmExtraFilesToDeploy Include="$(MSBuildThisFileDirectory)/wwwroot/**/*.*" />
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\3.1.7\libHarfBuzzSharp.a" />
<NativeFileReference Include="$(SkiaSharpStaticLibraryPath)\3.1.7\libSkiaSharp.a" />
</ItemGroup>
</Project>

451
src/Web/Avalonia.Web/AvaloniaView.cs

@ -0,0 +1,451 @@
using System;
using System.Runtime.InteropServices.JavaScript;
using Avalonia.Controls;
using Avalonia.Controls.Embedding;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using Avalonia.Web.Interop;
using Avalonia.Web.Skia;
using SkiaSharp;
namespace Avalonia.Web
{
[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings
public partial class AvaloniaView : ITextInputMethodImpl
{
private readonly BrowserTopLevelImpl _topLevelImpl;
private EmbeddableControlRoot _topLevel;
private readonly JSObject _containerElement;
private readonly JSObject _canvas;
private readonly JSObject _nativeControlsContainer;
private readonly JSObject _inputElement;
private readonly JSObject? _splash;
private GLInfo? _jsGlInfo = null;
private double _dpi = 1;
private Size _canvasSize = new(100.0, 100.0);
private GRContext? _context;
private GRGlInterface? _glInterface;
private const SKColorType ColorType = SKColorType.Rgba8888;
private bool _useGL;
private ITextInputMethodClient? _client;
private static int _canvasCount;
public AvaloniaView(string divId)
{
var host = DomHelper.GetElementById(divId);
if (host == null)
{
throw new Exception($"Element with id {divId} was not found in the html document.");
}
var hostContent = DomHelper.CreateAvaloniaHost(host);
if (hostContent == null)
{
throw new InvalidOperationException("Avalonia WASM host wasn't initialized.");
}
_containerElement = hostContent.GetPropertyAsJSObject("host")
?? throw new InvalidOperationException("Host cannot be null");
_canvas = hostContent.GetPropertyAsJSObject("canvas")
?? throw new InvalidOperationException("Canvas cannot be null");
_nativeControlsContainer = hostContent.GetPropertyAsJSObject("nativeHost")
?? throw new InvalidOperationException("NativeHost cannot be null");
_inputElement = hostContent.GetPropertyAsJSObject("inputElement")
?? throw new InvalidOperationException("InputElement cannot be null");
_splash = DomHelper.GetElementById("avalonia-splash");
_canvas.SetProperty("id", $"avaloniaCanvas{_canvasCount++}");
_topLevelImpl = new BrowserTopLevelImpl(this);
_topLevel = new WebEmbeddableControlRoot(_topLevelImpl, () =>
{
Dispatcher.UIThread.Post(() =>
{
if (_splash != null)
{
DomHelper.AddCssClass(_splash, "splash-close");
}
});
});
_topLevelImpl.SetCssCursor = (cursor) =>
{
InputHelper.SetCursor(_containerElement, cursor); // macOS
InputHelper.SetCursor(_canvas, cursor); // windows
};
_topLevel.Prepare();
_topLevel.Renderer.Start();
InputHelper.SubscribeKeyEvents(
_containerElement,
OnKeyDown,
OnKeyUp);
InputHelper.SubscribeTextEvents(
_inputElement,
OnTextInput,
OnCompositionStart,
OnCompositionUpdate,
OnCompositionEnd);
InputHelper.SubscribePointerEvents(_containerElement, OnPointerMove, OnPointerDown, OnPointerUp, OnWheel);
var skiaOptions = AvaloniaLocator.Current.GetService<SkiaOptions>();
_dpi = DomHelper.ObserveDpi(OnDpiChanged);
_useGL = skiaOptions?.CustomGpuFactory != null;
if (_useGL)
{
_jsGlInfo = CanvasHelper.InitialiseGL(_canvas, OnRenderFrame);
// 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);
}
_topLevelImpl.Surfaces = new[] { new BrowserSkiaSurface(_context, _jsGlInfo, ColorType, new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, GRSurfaceOrigin.BottomLeft) };
}
else
{
//var rasterInitialized = _interop.InitRaster();
//Console.WriteLine("raster initialized: {0}", rasterInitialized);
//_topLevelImpl.SetSurface(ColorType,
// new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData);
}
CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
DomHelper.ObserveSize(host, divId, OnSizeChanged);
CanvasHelper.RequestAnimationFrame(_canvas, true);
}
private static RawPointerPoint ExtractRawPointerFromJSArgs(JSObject args)
{
var point = new RawPointerPoint
{
Position = new Point(args.GetPropertyAsDouble("offsetX"), args.GetPropertyAsDouble("offsetY")),
Pressure = (float)args.GetPropertyAsDouble("pressure"),
XTilt = (float)args.GetPropertyAsDouble("tiltX"),
YTilt = (float)args.GetPropertyAsDouble("tiltY"),
Twist = (float)args.GetPropertyAsDouble("twist")
};
return point;
}
private bool OnPointerMove(JSObject args)
{
var type = args.GetPropertyAsString("pointertype");
var point = ExtractRawPointerFromJSArgs(args);
return _topLevelImpl.RawPointerEvent(RawPointerEventType.Move, type!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"));
}
private bool OnPointerDown(JSObject args)
{
var pointerType = args.GetPropertyAsString("pointerType");
var type = pointerType switch
{
"touch" => RawPointerEventType.TouchBegin,
_ => args.GetPropertyAsInt32("button") switch
{
0 => RawPointerEventType.LeftButtonDown,
1 => RawPointerEventType.MiddleButtonDown,
2 => RawPointerEventType.RightButtonDown,
3 => RawPointerEventType.XButton1Down,
4 => RawPointerEventType.XButton2Down,
// 5 => Pen eraser button,
_ => RawPointerEventType.Move
}
};
var point = ExtractRawPointerFromJSArgs(args);
return _topLevelImpl.RawPointerEvent(type, pointerType!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"));
}
private bool OnPointerUp(JSObject args)
{
var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse";
var type = pointerType switch
{
"touch" => RawPointerEventType.TouchEnd,
_ => args.GetPropertyAsInt32("button") switch
{
0 => RawPointerEventType.LeftButtonUp,
1 => RawPointerEventType.MiddleButtonUp,
2 => RawPointerEventType.RightButtonUp,
3 => RawPointerEventType.XButton1Up,
4 => RawPointerEventType.XButton2Up,
// 5 => Pen eraser button,
_ => RawPointerEventType.Move
}
};
var point = ExtractRawPointerFromJSArgs(args);
return _topLevelImpl.RawPointerEvent(type, pointerType, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"));
}
private bool OnWheel(JSObject args)
{
return _topLevelImpl.RawMouseWheelEvent(new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")),
new Vector(-(args.GetPropertyAsDouble("deltaX") / 50), -(args.GetPropertyAsDouble("deltaY") / 50)), GetModifiers(args));
}
private static RawInputModifiers GetModifiers(JSObject e)
{
var modifiers = RawInputModifiers.None;
if (e.GetPropertyAsBoolean("ctrlKey"))
modifiers |= RawInputModifiers.Control;
if (e.GetPropertyAsBoolean("altKey"))
modifiers |= RawInputModifiers.Alt;
if (e.GetPropertyAsBoolean("shiftKey"))
modifiers |= RawInputModifiers.Shift;
if (e.GetPropertyAsBoolean("metaKey"))
modifiers |= RawInputModifiers.Meta;
var buttons = e.GetPropertyAsInt32("buttons");
if ((buttons & 1L) == 1)
modifiers |= RawInputModifiers.LeftMouseButton;
if ((buttons & 2L) == 2)
modifiers |= e.GetPropertyAsString("type") == "pen" ? RawInputModifiers.PenBarrelButton : RawInputModifiers.RightMouseButton;
if ((buttons & 4L) == 4)
modifiers |= RawInputModifiers.MiddleMouseButton;
if ((buttons & 8L) == 8)
modifiers |= RawInputModifiers.XButton1MouseButton;
if ((buttons & 16L) == 16)
modifiers |= RawInputModifiers.XButton2MouseButton;
if ((buttons & 32L) == 32)
modifiers |= RawInputModifiers.PenEraser;
return modifiers;
}
private bool OnKeyDown (string code, string key, int modifier)
{
return _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)modifier);
}
private bool OnKeyUp(string code, string key, int modifier)
{
return _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, code, key, (RawInputModifiers)modifier);
}
private bool OnTextInput (string type, string? data)
{
if(data == null || IsComposing)
{
return false;
}
return _topLevelImpl.RawTextEvent(data);
}
private bool OnCompositionStart (JSObject args)
{
if (_client == null)
return false;
_client.SetPreeditText(null);
IsComposing = true;
return false;
}
private bool OnCompositionUpdate(JSObject args)
{
if (_client == null)
return false;
_client.SetPreeditText(args.GetPropertyAsString("data"));
return false;
}
private bool OnCompositionEnd(JSObject args)
{
if (_client == null)
return false;
IsComposing = false;
_client.SetPreeditText(null);
_topLevelImpl.RawTextEvent(args.GetPropertyAsString("data")!);
return false;
}
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 Control? Content
{
get => (Control)_topLevel.Content!;
set => _topLevel.Content = value;
}
public bool IsComposing { get; private set; }
internal INativeControlHostImpl GetNativeControlHostImpl()
{
return new BrowserNativeControlHost(_nativeControlsContainer);
}
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 oldDpi, double newDpi)
{
if (Math.Abs(_dpi - newDpi) > 0.0001)
{
_dpi = newDpi;
CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
ForceBlit();
}
}
private void OnSizeChanged(int height, int width)
{
var newSize = new Size(height, width);
if (_canvasSize != newSize)
{
_canvasSize = newSize;
CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
ForceBlit();
}
}
private void HideIme()
{
InputHelper.HideElement(_inputElement);
InputHelper.FocusElement(_containerElement);
}
public void SetClient(ITextInputMethodClient? client)
{
Console.WriteLine("Set Client");
if (_client != null)
{
_client.SurroundingTextChanged -= SurroundingTextChanged;
}
if (client != null)
{
client.SurroundingTextChanged += SurroundingTextChanged;
}
InputHelper.ClearInputElement(_inputElement);
_client = client;
if (_client != null)
{
InputHelper.ShowElement(_inputElement);
InputHelper.FocusElement(_inputElement);
var surroundingText = _client.SurroundingText;
InputHelper.SetSurroundingText(_inputElement, surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset);
Console.WriteLine("Shown, focused and surrounded.");
}
else
{
HideIme();
}
}
private void SurroundingTextChanged(object? sender, EventArgs e)
{
if (_client != null)
{
var surroundingText = _client.SurroundingText;
InputHelper.SetSurroundingText(_inputElement, surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset);
}
}
public void 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)
{
}
public void Reset()
{
InputHelper.ClearInputElement(_inputElement);
InputHelper.SetSurroundingText(_inputElement, "", 0, 0);
}
}
}

136
src/Web/Avalonia.Web/BrowserNativeControlHost.cs

@ -0,0 +1,136 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices.JavaScript;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
using Avalonia.Web.Interop;
namespace Avalonia.Web
{
internal class BrowserNativeControlHost : INativeControlHostImpl
{
private readonly JSObject _hostElement;
public BrowserNativeControlHost(JSObject element)
{
_hostElement = element;
}
public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent)
{
var element = NativeControlHostHelper.CreateDefaultChild(null);
return new JSObjectControlHandle(element);
}
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func<IPlatformHandle, IPlatformHandle> create)
{
Attachment? a = null;
try
{
var child = create(new JSObjectControlHandle(_hostElement));
var attachmenetReference = NativeControlHostHelper.CreateAttachment();
// 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 = NativeControlHostHelper.CreateAttachment();
var a = new Attachment(attachmenetReference, handle);
a.AttachedTo = this;
return a;
}
public bool IsCompatibleWith(IPlatformHandle handle) => handle is JSObjectControlHandle;
private 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 JSObject? _native;
private BrowserNativeControlHost? _attachedTo;
public Attachment(JSObject native, IPlatformHandle handle)
{
_native = native;
NativeControlHostHelper.InitializeWithChildHandle(_native, ((JSObjectControlHandle)handle).Object);
}
public void Dispose()
{
if (_native != null)
{
NativeControlHostHelper.ReleaseChild(_native);
_native.Dispose();
_native = null;
}
}
public INativeControlHostImpl? AttachedTo
{
get => _attachedTo!;
set
{
CheckDisposed();
var host = (BrowserNativeControlHost?)value;
if (host == null)
{
NativeControlHostHelper.AttachTo(_native, null);
}
else
{
NativeControlHostHelper.AttachTo(_native, host._hostElement);
}
_attachedTo = host;
}
}
public bool IsCompatibleWith(INativeControlHostImpl host) => host is BrowserNativeControlHost;
public void HideWithSize(Size size)
{
CheckDisposed();
if (_attachedTo == null)
return;
NativeControlHostHelper.HideWithSize(_native, Math.Max(1, size.Width), Math.Max(1, 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));
NativeControlHostHelper.ShowInBounds(_native, bounds.X, bounds.Y, bounds.Width, bounds.Height);
}
[MemberNotNull(nameof(_native))]
private void CheckDisposed()
{
if (_native == null)
throw new ObjectDisposedException(nameof(Attachment));
}
}
}
}

41
src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs

@ -0,0 +1,41 @@
using System.Runtime.InteropServices.JavaScript;
using System;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Media;
using Avalonia.Web.Skia;
namespace Avalonia.Web
{
[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings
public class BrowserSingleViewLifetime : ISingleViewApplicationLifetime
{
public AvaloniaView? View;
public Control? MainView
{
get => View!.Content;
set => View!.Content = value;
}
}
public static partial 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);
}
}
}

226
src/Web/Avalonia.Web/BrowserTopLevelImpl.cs

@ -0,0 +1,226 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
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.Skia;
using Avalonia.Web.Storage;
namespace Avalonia.Web
{
[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings
internal class BrowserTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider
{
private Size _clientSize;
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 BrowserTopLevelImpl(AvaloniaView avaloniaView)
{
Surfaces = Enumerable.Empty<object>();
_avaloniaView = avaloniaView;
TransparencyLevel = WindowTransparencyLevel.None;
AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1);
_touchDevice = new TouchDevice();
_penDevice = new PenDevice();
NativeControlHost = _avaloniaView.GetNativeControlHostImpl();
}
public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds;
public void SetClientSize(Size newSize, double dpi)
{
if (Math.Abs(RenderScaling - dpi) > 0.0001)
{
if (Surfaces.FirstOrDefault() is BrowserSkiaSurface surface)
{
surface.Scaling = dpi;
}
ScalingChanged?.Invoke(dpi);
}
if (newSize != _clientSize)
{
_clientSize = newSize;
if (Surfaces.FirstOrDefault() is BrowserSkiaSurface surface)
{
surface.Size = new PixelSize((int)newSize.Width, (int)newSize.Height);
}
Resized?.Invoke(newSize, PlatformResizeReason.User);
}
}
public bool 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);
return args.Handled;
}
return false;
}
private IPointerDevice GetPointerDevice(string pointerType)
{
return pointerType switch
{
"touch" => _touchDevice,
"pen" => _penDevice,
_ => MouseDevice
};
}
public bool RawMouseWheelEvent(Point p, Vector v, RawInputModifiers modifiers)
{
if (_inputRoot is { })
{
var args = new RawMouseWheelEventArgs(MouseDevice, Timestamp, _inputRoot, p, v, modifiers);
Input?.Invoke(args);
return args.Handled;
}
return false;
}
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 bool RawTextEvent(string text)
{
if (_inputRoot is { })
{
var args = new RawTextInputEventArgs(KeyboardDevice, Timestamp, _inputRoot, text);
Input?.Invoke(args);
return args.Handled;
}
return false;
}
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 => (Surfaces.FirstOrDefault() as BrowserSkiaSurface)?.Scaling ?? 1;
public IEnumerable<object> Surfaces { get; set; }
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; } = BrowserWindowingPlatform.Keyboard;
public WindowTransparencyLevel TransparencyLevel { get; }
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; }
public ITextInputMethodImpl TextInputMethod => _avaloniaView;
public INativeControlHostImpl? NativeControlHost { get; }
public IStorageProvider StorageProvider { get; } = new BrowserStorageProvider();
}
}

29
src/Web/Avalonia.Web/ClipboardImpl.cs

@ -0,0 +1,29 @@
using System;
using System.Threading.Tasks;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Web.Interop;
namespace Avalonia.Web
{
internal class ClipboardImpl : IClipboard
{
public Task<string> GetTextAsync()
{
return InputHelper.ReadClipboardTextAsync();
}
public Task SetTextAsync(string text)
{
return InputHelper.WriteClipboardTextAsync(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());
}
}

95
src/Web/Avalonia.Web/Cursor.cs

@ -0,0 +1,95 @@
using System;
using System.IO;
using Avalonia.Input;
using Avalonia.Platform;
namespace Avalonia.Web
{
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);
}
}
}

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

@ -0,0 +1,43 @@
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.JavaScript;
namespace Avalonia.Web.Interop;
internal record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth);
[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings
internal static partial class CanvasHelper
{
[DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)]
static extern JSObject InterceptGLObject();
public static GLInfo InitialiseGL(JSObject canvas, Action renderFrameCallback)
{
InterceptGLObject();
var info = InitGL(canvas, canvas.GetPropertyAsString("id")!, renderFrameCallback);
var glInfo = new GLInfo(
info.GetPropertyAsInt32("context"),
(uint)info.GetPropertyAsInt32("fboId"),
info.GetPropertyAsInt32("stencil"),
info.GetPropertyAsInt32("sample"),
info.GetPropertyAsInt32("depth"));
return glInfo;
}
[JSImport("Canvas.requestAnimationFrame", "avalonia.ts")]
public static partial void RequestAnimationFrame(JSObject canvas, bool renderLoop);
[JSImport("Canvas.setCanvasSize", "avalonia.ts")]
public static partial void SetCanvasSize(JSObject canvas, int height, int width);
[JSImport("Canvas.initGL", "avalonia.ts")]
private static partial JSObject InitGL(
JSObject canvas,
string canvasId,
[JSMarshalAs<JSType.Function>] Action renderFrameCallback);
}

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

@ -0,0 +1,28 @@
using System;
using System.Runtime.InteropServices.JavaScript;
namespace Avalonia.Web.Interop;
internal static partial class DomHelper
{
[JSImport("globalThis.document.getElementById")]
internal static partial JSObject? GetElementById(string id);
[JSImport("AvaloniaDOM.createAvaloniaHost", "avalonia.ts")]
public static partial JSObject CreateAvaloniaHost(JSObject element);
[JSImport("AvaloniaDOM.addClass", "avalonia.ts")]
public static partial void AddCssClass(JSObject element, string className);
[JSImport("SizeWatcher.observe", "avalonia.ts")]
public static partial JSObject ObserveSize(
JSObject canvas,
string canvasId,
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Number>>]
Action<int, int> onSizeChanged);
[JSImport("DpiWatcher.start", "avalonia.ts")]
public static partial double ObserveDpi(
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Number>>]
Action<double, double> onDpiChanged);
}

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

@ -0,0 +1,78 @@
using System;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
namespace Avalonia.Web.Interop;
internal static partial class InputHelper
{
[JSImport("InputHelper.subscribeKeyEvents", "avalonia.ts")]
public static partial void SubscribeKeyEvents(
JSObject htmlElement,
[JSMarshalAs<JSType.Function<JSType.String, JSType.String, JSType.Number, JSType.Boolean>>]
Func<string, string, int, bool> keyDown,
[JSMarshalAs<JSType.Function<JSType.String, JSType.String, JSType.Number, JSType.Boolean>>]
Func<string, string, int, bool> keyUp);
[JSImport("InputHelper.subscribeTextEvents", "avalonia.ts")]
public static partial void SubscribeTextEvents(
JSObject htmlElement,
[JSMarshalAs<JSType.Function<JSType.String, JSType.String, JSType.Boolean>>]
Func<string, string?, bool> onInput,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
Func<JSObject, bool> onCompositionStart,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
Func<JSObject, bool> onCompositionUpdate,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
Func<JSObject, bool> onCompositionEnd);
[JSImport("InputHelper.subscribePointerEvents", "avalonia.ts")]
public static partial void SubscribePointerEvents(
JSObject htmlElement,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
Func<JSObject, bool> pointerMove,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
Func<JSObject, bool> pointerDown,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
Func<JSObject, bool> pointerUp,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
Func<JSObject, bool> wheel);
[JSImport("InputHelper.subscribeInputEvents", "avalonia.ts")]
public static partial void SubscribeInputEvents(
JSObject htmlElement,
[JSMarshalAs<JSType.Function<JSType.String, JSType.Boolean>>]
Func<string, bool> input);
[JSImport("InputHelper.clearInput", "avalonia.ts")]
public static partial void ClearInputElement(JSObject htmlElement);
[JSImport("InputHelper.isInputElement", "avalonia.ts")]
public static partial void IsInputElement(JSObject htmlElement);
[JSImport("InputHelper.focusElement", "avalonia.ts")]
public static partial void FocusElement(JSObject htmlElement);
[JSImport("InputHelper.setCursor", "avalonia.ts")]
public static partial void SetCursor(JSObject htmlElement, string kind);
[JSImport("InputHelper.hide", "avalonia.ts")]
public static partial void HideElement(JSObject htmlElement);
[JSImport("InputHelper.show", "avalonia.ts")]
public static partial void ShowElement(JSObject htmlElement);
[JSImport("InputHelper.setSurroundingText", "avalonia.ts")]
public static partial void SetSurroundingText(JSObject htmlElement, string text, int start, int end);
[JSImport("InputHelper.setBounds", "avalonia.ts")]
public static partial void SetBounds(JSObject htmlElement, int x, int y, int width, int height, int caret);
[JSImport("globalThis.navigator.clipboard.readText")]
public static partial Task<string> ReadClipboardTextAsync();
[JSImport("globalThis.navigator.clipboard.writeText")]
public static partial Task WriteClipboardTextAsync(string text);
}

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

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

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

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

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

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

30
src/Web/Avalonia.Web/JSObjectControlHandle.cs

@ -0,0 +1,30 @@
using System;
using System.Runtime.InteropServices.JavaScript;
using Avalonia.Controls.Platform;
namespace Avalonia.Web;
public class JSObjectControlHandle : INativeControlHostDestroyableControlHandle
{
internal const string ElementReferenceDescriptor = "JSObject";
public JSObjectControlHandle(JSObject reference)
{
Object = reference;
}
public JSObject Object { get; }
public IntPtr Handle => throw new NotSupportedException();
public string? HandleDescriptor => ElementReferenceDescriptor;
public void Destroy()
{
if (Object is JSObject inProcess && !inProcess.IsDisposed)
{
inProcess.Dispose();
}
}
}

129
src/Web/Avalonia.Web/Keycodes.cs

@ -0,0 +1,129 @@
using System.Collections.Generic;
using Avalonia.Input;
namespace Avalonia.Web
{
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 }
};
}
}

18
src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs

@ -0,0 +1,18 @@
using System;
using System.Diagnostics;
using Avalonia.Rendering;
namespace Avalonia.Web
{
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;
}
}

26
src/Web/Avalonia.Web/Skia/BrowserSkiaGpu.cs

@ -0,0 +1,26 @@
using System.Collections.Generic;
using Avalonia.Skia;
namespace Avalonia.Web.Skia
{
public class BrowserSkiaGpu : ISkiaGpu
{
public ISkiaGpuRenderTarget? TryCreateRenderTarget(IEnumerable<object> surfaces)
{
foreach (var surface in surfaces)
{
if (surface is BrowserSkiaSurface browserSkiaSurface)
{
return new BrowserSkiaGpuRenderTarget(browserSkiaSurface);
}
}
return null;
}
public ISkiaSurface? TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session)
{
return null;
}
}
}

36
src/Web/Avalonia.Web/Skia/BrowserSkiaGpuRenderSession.cs

@ -0,0 +1,36 @@
using Avalonia.Skia;
using SkiaSharp;
namespace Avalonia.Web.Skia
{
internal class BrowserSkiaGpuRenderSession : ISkiaGpuRenderSession
{
private readonly SKSurface _surface;
public BrowserSkiaGpuRenderSession(BrowserSkiaSurface browserSkiaSurface, GRBackendRenderTarget renderTarget)
{
_surface = SKSurface.Create(browserSkiaSurface.Context, renderTarget, browserSkiaSurface.Origin, browserSkiaSurface.ColorType);
GrContext = browserSkiaSurface.Context;
ScaleFactor = browserSkiaSurface.Scaling;
SurfaceOrigin = browserSkiaSurface.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/Skia/BrowserSkiaGpuRenderTarget.cs

@ -0,0 +1,39 @@
using Avalonia.Skia;
using SkiaSharp;
namespace Avalonia.Web.Skia
{
internal class BrowserSkiaGpuRenderTarget : ISkiaGpuRenderTarget
{
private readonly GRBackendRenderTarget _renderTarget;
private readonly BrowserSkiaSurface _browserSkiaSurface;
private readonly PixelSize _size;
public BrowserSkiaGpuRenderTarget(BrowserSkiaSurface browserSkiaSurface)
{
_size = browserSkiaSurface.Size;
var glFbInfo = new GRGlFramebufferInfo(browserSkiaSurface.GlInfo.FboId, browserSkiaSurface.ColorType.ToGlSizedFormat());
{
_browserSkiaSurface = browserSkiaSurface;
_renderTarget = new GRBackendRenderTarget(
(int)(browserSkiaSurface.Size.Width * browserSkiaSurface.Scaling),
(int)(browserSkiaSurface.Size.Height * browserSkiaSurface.Scaling),
browserSkiaSurface.GlInfo.Samples,
browserSkiaSurface.GlInfo.Stencils, glFbInfo);
}
}
public void Dispose()
{
_renderTarget.Dispose();
}
public ISkiaGpuRenderSession BeginRenderingSession()
{
return new BrowserSkiaGpuRenderSession(_browserSkiaSurface, _renderTarget);
}
public bool IsCorrupted => _browserSkiaSurface.Size != _size;
}
}

88
src/Web/Avalonia.Web/Skia/BrowserSkiaRasterSurface.cs

@ -0,0 +1,88 @@
using System;
using System.Runtime.InteropServices;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Platform;
using Avalonia.Skia;
using SkiaSharp;
namespace Avalonia.Web.Skia
{
internal class BrowserSkiaRasterSurface : IBrowserSkiaSurface, 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 BrowserSkiaRasterSurface(
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/Skia/BrowserSkiaSurface.cs

@ -0,0 +1,30 @@
using Avalonia.Web.Interop;
using SkiaSharp;
namespace Avalonia.Web.Skia
{
internal class BrowserSkiaSurface : IBrowserSkiaSurface
{
public BrowserSkiaSurface(GRContext context, 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 GLInfo GlInfo { get; set; }
}
}

9
src/Web/Avalonia.Web/Skia/IBrowserSkiaSurface.cs

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

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

@ -0,0 +1,88 @@
using System;
using System.IO;
using System.Runtime.InteropServices.JavaScript;
using System.Threading;
using System.Threading.Tasks;
namespace Avalonia.Web.Storage;
[System.Runtime.Versioning.SupportedOSPlatform("browser")]
internal class BlobReadableStream : Stream
{
private JSObject? _jSReference;
private long _position;
private readonly long _length;
public BlobReadableStream(JSObject jsStreamReference)
{
_jSReference = jsStreamReference;
_position = 0;
_length = StreamHelper.ByteLength(JSReference);
}
private JSObject JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(WriteableStream));
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => _length;
public override long Position
{
get => _position;
set => throw new NotSupportedException();
}
public override void Flush() { }
public override long Seek(long offset, SeekOrigin origin)
{
return _position = origin switch
{
SeekOrigin.Current => _position + offset,
SeekOrigin.End => _length + offset,
_ => offset
};
}
public override void SetLength(long value)
=> throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
public override int Read(byte[] buffer, int offset, int count)
{
throw new InvalidOperationException("Browser supports only ReadAsync");
}
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
=> await ReadAsync(buffer.AsMemory(offset, count), cancellationToken);
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
var numBytesToRead = (int)Math.Min(buffer.Length, Length - _position);
var bytesRead = await StreamHelper.SliceAsync(JSReference, _position, numBytesToRead);
if (bytesRead.Length != numBytesToRead)
{
throw new EndOfStreamException("Failed to read the requested number of bytes from the stream.");
}
_position += bytesRead.Length;
bytesRead.CopyTo(buffer);
return bytesRead.Length;
}
protected override void Dispose(bool disposing)
{
if (_jSReference is { } jsReference)
{
_jSReference = null;
jsReference.Dispose();
}
}
}

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

@ -0,0 +1,257 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Avalonia.Platform.Storage;
using Avalonia.Web.Interop;
namespace Avalonia.Web.Storage;
internal record FilePickerAcceptType(string Description, IReadOnlyDictionary<string, IReadOnlyList<string>> Accept);
[SupportedOSPlatform("browser")]
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"));
public bool CanOpen => StorageHelper.CanShowOpenFilePicker();
public bool CanSave => StorageHelper.CanShowSaveFilePicker();
public bool CanPickFolder => StorageHelper.CanShowDirectoryPicker();
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
_ = await _lazyModule.Value;
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle;
var (types, exludeAll) = ConvertFileTypes(options.FileTypeFilter);
try
{
using var items = await StorageHelper.OpenFileDialog(startIn, options.AllowMultiple, types, exludeAll);
if (items is null)
{
return Array.Empty<IStorageFile>();
}
var itemsArray = StorageHelper.ItemsArray(items);
return itemsArray.Select(item => new JSStorageFile(item)).ToArray();
}
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal))
{
return Array.Empty<IStorageFile>();
}
finally
{
if (types is not null)
{
foreach (var type in types)
{
type.Dispose();
}
}
}
}
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
_ = await _lazyModule.Value;
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle;
var (types, exludeAll) = ConvertFileTypes(options.FileTypeChoices);
try
{
var item = await StorageHelper.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;
}
finally
{
if (types is not null)
{
foreach (var type in types)
{
type.Dispose();
}
}
}
}
public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
_ = await _lazyModule.Value;
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle;
try
{
var item = await StorageHelper.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)
{
_ = 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;
var item = await StorageHelper.OpenBookmark(bookmark);
return item is not null ? new JSStorageFolder(item) : null;
}
private static (JSObject[]? types, bool excludeAllOption) ConvertFileTypes(IEnumerable<FilePickerFileType>? input)
{
var types = input?
.Where(t => t.MimeTypes?.Any() == true && t != FilePickerFileTypes.All)
.Select(t => StorageHelper.CreateAcceptType(t.Name, t.MimeTypes!.ToArray()))
.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 JSObject? _fileHandle;
protected JSStorageItem(JSObject fileHandle)
{
_fileHandle = fileHandle ?? throw new ArgumentNullException(nameof(fileHandle));
}
internal JSObject FileHandle => _fileHandle ?? throw new ObjectDisposedException(nameof(JSStorageItem));
public string Name => FileHandle.GetPropertyAsString("name") ?? string.Empty;
public bool TryGetUri([NotNullWhen(true)] out Uri? uri)
{
uri = new Uri(Name, UriKind.Relative);
return false;
}
public async Task<StorageItemProperties> GetBasicPropertiesAsync()
{
using var properties = await StorageHelper.GetProperties(FileHandle);
var size = (long?)properties?.GetPropertyAsDouble("Size");
var lastModified = (long?)properties?.GetPropertyAsDouble("LastModified");
return new StorageItemProperties(
(ulong?)size,
dateCreated: null,
dateModified: lastModified > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(lastModified.Value) : null);
}
public bool CanBookmark => true;
public Task<string?> SaveBookmarkAsync()
{
return StorageHelper.SaveBookmark(FileHandle);
}
public Task<IStorageFolder?> GetParentAsync()
{
return Task.FromResult<IStorageFolder?>(null);
}
public Task ReleaseBookmarkAsync()
{
return StorageHelper.DeleteBookmark(FileHandle);
}
public void Dispose()
{
_fileHandle?.Dispose();
_fileHandle = null;
}
}
internal class JSStorageFile : JSStorageItem, IStorageBookmarkFile
{
public JSStorageFile(JSObject fileHandle) : base(fileHandle)
{
}
public bool CanOpenRead => true;
public async Task<Stream> OpenReadAsync()
{
try
{
var blob = await StorageHelper.OpenRead(FileHandle);
return new BlobReadableStream(blob);
}
catch (JSException ex) when (ex.Message == BrowserStorageProvider.NoPermissionsMessage)
{
throw new UnauthorizedAccessException("User denied permissions to open the file", ex);
}
}
public bool CanOpenWrite => true;
public async Task<Stream> OpenWriteAsync()
{
try
{
using var properties = await StorageHelper.GetProperties(FileHandle);
var streamWriter = await StorageHelper.OpenWrite(FileHandle);
var size = (long?)properties?.GetPropertyAsDouble("Size") ?? 0;
return new WriteableStream(streamWriter, size);
}
catch (JSException ex) when (ex.Message == BrowserStorageProvider.NoPermissionsMessage)
{
throw new UnauthorizedAccessException("User denied permissions to open the file", ex);
}
}
}
internal class JSStorageFolder : JSStorageItem, IStorageBookmarkFolder
{
public JSStorageFolder(JSObject fileHandle) : base(fileHandle)
{
}
public async Task<IReadOnlyList<IStorageItem>> GetItemsAsync()
{
using var items = await StorageHelper.GetItems(FileHandle);
if (items is null)
{
return Array.Empty<IStorageItem>();
}
var itemsArray = StorageHelper.ItemsArray(items);
return itemsArray
.Select(reference => reference.GetPropertyAsString("kind") switch
{
"directory" => (IStorageItem)new JSStorageFolder(reference),
"file" => new JSStorageFile(reference),
_ => null
})
.Where(i => i is not null)
.ToArray()!;
}
}

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

@ -0,0 +1,124 @@
using System;
using System.IO;
using System.Runtime.InteropServices.JavaScript;
using System.Threading;
using System.Threading.Tasks;
namespace Avalonia.Web.Storage;
[System.Runtime.Versioning.SupportedOSPlatform("browser")]
// Loose wrapper implementaion of a stream on top of FileAPI FileSystemWritableFileStream
internal sealed class WriteableStream : Stream
{
private JSObject? _jSReference;
// Unfortunatelly we can't read current length/position, so we need to keep it C#-side only.
private long _length, _position;
internal WriteableStream(JSObject jSReference, long initialLength)
{
_jSReference = jSReference;
_length = initialLength;
}
private JSObject JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(WriteableStream));
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
};
StreamHelper.Seek(JSReference, 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;
}
StreamHelper.Truncate(JSReference, value);
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new InvalidOperationException("Browser supports only WriteAsync");
}
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
{
return new ValueTask(WriteAsyncInternal(buffer.ToArray(), cancellationToken));
}
private Task WriteAsyncInternal(byte[] buffer, CancellationToken _)
{
_position += buffer.Length;
return StreamHelper.WriteAsync(JSReference, buffer);
}
protected override void Dispose(bool disposing)
{
if (_jSReference is { } jsReference)
{
_jSReference = null;
try
{
_ = StreamHelper.CloseAsync(jsReference);
}
finally
{
jsReference.Dispose();
}
}
}
public override async ValueTask DisposeAsync()
{
if (_jSReference is { } jsReference)
{
_jSReference = null;
try
{
await StreamHelper.CloseAsync(jsReference);
}
finally
{
jsReference.Dispose();
}
}
}
}

68
src/Web/Avalonia.Web/WebEmbeddableControlRoot.cs

@ -0,0 +1,68 @@
using System;
using Avalonia.Controls.Embedding;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
namespace Avalonia.Web
{
internal class WebEmbeddableControlRoot : EmbeddableControlRoot
{
class SplashScreenCloseCustomDrawingOperation : ICustomDrawOperation
{
private bool _hasRendered;
private Action _onFirstRender;
public SplashScreenCloseCustomDrawingOperation(Action onFirstRender)
{
_onFirstRender = onFirstRender;
}
public Rect Bounds => Rect.Empty;
public bool HasRendered => _hasRendered;
public void Dispose()
{
}
public bool Equals(ICustomDrawOperation? other)
{
return false;
}
public bool HitTest(Point p)
{
return false;
}
public void Render(IDrawingContextImpl context)
{
_hasRendered = true;
_onFirstRender();
}
}
public WebEmbeddableControlRoot(ITopLevelImpl impl, Action onFirstRender) : base(impl)
{
_splashCloseOp = new SplashScreenCloseCustomDrawingOperation(() =>
{
_splashCloseOp = null;
onFirstRender();
});
}
private SplashScreenCloseCustomDrawingOperation? _splashCloseOp;
public override void Render(DrawingContext context)
{
base.Render(context);
if (_splashCloseOp != null)
{
context.Custom(_splashCloseOp);
}
}
}
}

48
src/Web/Avalonia.Web/WinStubs.cs

@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.IO;
using Avalonia.Platform;
#nullable enable
namespace Avalonia.Web
{
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);
}
}
}

105
src/Web/Avalonia.Web/WindowingPlatform.cs

@ -0,0 +1,105 @@
using System;
using System.Threading;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Threading;
namespace Avalonia.Web
{
public class BrowserWindowingPlatform : 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("BrowserWindowingPlatform not registered.");
public static void Register()
{
var instance = new BrowserWindowingPlatform();
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; // Browser is single threaded.
}
}
public event Action<DispatcherPriority?>? Signaled;
private static IRuntimePlatform GetRuntimePlatform()
{
return AvaloniaLocator.Current.GetRequiredService<IRuntimePlatform>();
}
}
}

13
src/Web/Avalonia.Web/interop.js

@ -0,0 +1,13 @@
var LibraryExample = {
// Internal functions
$EXAMPLE: {
internal_func: function () {
}
},
InterceptGLObject: function () {
globalThis.AvaloniaGL = GL
}
}
autoAddDeps(LibraryExample, '$EXAMPLE')
mergeInto(LibraryManager.library, LibraryExample)

47
src/Web/Avalonia.Web/webapp/.eslintrc.json

@ -0,0 +1,47 @@
{
"env": {
"browser": true,
"es6": true
},
"extends": "standard-with-typescript",
"overrides": [],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"project": [
"tsconfig.json"
]
},
"rules": {
"indent": [
"warn",
4
],
"@typescript-eslint/indent": [
"warn",
4
],
"quotes": ["warn", "double"],
"semi": ["error", "always"],
"@typescript-eslint/quotes": ["warn", "double"],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-extraneous-class": "off",
"@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/space-before-function-paren": "off",
"@typescript-eslint/semi": ["error", "always"],
"@typescript-eslint/member-delimiter-style": [
"error",
{
"multiline": {
"delimiter": "semi",
"requireLast": true
},
"singleline": {
"delimiter": "semi",
"requireLast": false
}
}
]
},
"ignorePatterns": ["types/*"]
}

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

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

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

@ -0,0 +1,20 @@
import { RuntimeAPI } from "../types/dotnet";
import { SizeWatcher, DpiWatcher, Canvas } from "./avalonia/canvas";
import { InputHelper } from "./avalonia/input";
import { AvaloniaDOM } from "./avalonia/dom";
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", {
Caniuse,
Canvas,
InputHelper,
SizeWatcher,
DpiWatcher,
AvaloniaDOM,
StreamHelper,
NativeControlHost
});
}

13
src/Web/Avalonia.Web/webapp/modules/avalonia/caniuse.ts

@ -0,0 +1,13 @@
export class Caniuse {
public static canShowOpenFilePicker(): boolean {
return typeof window.showOpenFilePicker !== "undefined";
}
public static canShowSaveFilePicker(): boolean {
return typeof window.showSaveFilePicker !== "undefined";
}
public static canShowDirectoryPicker(): boolean {
return typeof window.showDirectoryPicker !== "undefined";
}
}

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

@ -0,0 +1,303 @@
interface SKGLViewInfo {
context: WebGLRenderingContext | WebGL2RenderingContext | undefined;
fboId: number;
stencil: number;
sample: number;
depth: number;
}
type CanvasElement = {
Canvas: Canvas | undefined;
} & HTMLCanvasElement;
export class Canvas {
static elements: Map<string, HTMLCanvasElement>;
htmlCanvas: HTMLCanvasElement;
glInfo?: SKGLViewInfo;
renderFrameCallback: () => void;
renderLoopEnabled: boolean = false;
renderLoopRequest: number = 0;
newWidth?: number;
newHeight?: number;
public static initGL(element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): SKGLViewInfo | null {
const view = Canvas.init(true, element, elementId, renderFrameCallback);
if (!view || !view.glInfo) {
return null;
}
return view.glInfo;
}
static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): Canvas | null {
const htmlCanvas = element as CanvasElement;
if (!htmlCanvas) {
console.error("No canvas element was provided.");
return null;
}
if (!Canvas.elements) {
Canvas.elements = new Map<string, HTMLCanvasElement>();
}
Canvas.elements.set(elementId, element);
const view = new Canvas(useGL, element, renderFrameCallback);
htmlCanvas.Canvas = view;
return view;
}
public constructor(useGL: boolean, element: HTMLCanvasElement, renderFrameCallback: () => void) {
this.htmlCanvas = element;
this.renderFrameCallback = renderFrameCallback;
if (useGL) {
const ctx = Canvas.createWebGLContext(element);
if (!ctx) {
console.error("Failed to create WebGL context");
return;
}
const GL = (globalThis as any).AvaloniaGL;
// make current
GL.makeContextCurrent(ctx);
const GLctx = GL.currentContext.GLctx as WebGLRenderingContext;
// 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 setEnableRenderLoop(enable: boolean): void {
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 requestAnimationFrame(renderLoop?: boolean): void {
// 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) {
const GL = (globalThis as any).AvaloniaGL;
// 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();
this.renderLoopRequest = 0;
// we may want to draw the next frame
if (this.renderLoopEnabled) {
this.requestAnimationFrame();
}
});
}
public setCanvasSize(width: number, height: number): void {
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) {
const GL = (globalThis as any).AvaloniaGL;
// make current
GL.makeContextCurrent(this.glInfo.context);
}
}
public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number): void {
const htmlCanvas = element as CanvasElement;
if (!htmlCanvas || !htmlCanvas.Canvas) {
return;
}
htmlCanvas.Canvas.setCanvasSize(width, height);
}
public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean): void {
const htmlCanvas = element as CanvasElement;
if (!htmlCanvas || !htmlCanvas.Canvas) {
return;
}
htmlCanvas.Canvas.requestAnimationFrame(renderLoop);
}
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
};
const GL = (globalThis as any).AvaloniaGL;
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;
}
}
type SizeWatcherElement = {
SizeWatcher: SizeWatcherInstance;
} & HTMLElement;
interface SizeWatcherInstance {
callback: (width: number, height: number) => void;
}
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 {
if (!element || !callback) {
return;
}
SizeWatcher.init();
const watcherElement = element as SizeWatcherElement;
watcherElement.SizeWatcher = {
callback
};
SizeWatcher.elements.set(elementId, element);
SizeWatcher.observer.observe(element);
SizeWatcher.invoke(element);
}
public static unobserve(elementId: string): void {
if (!elementId || !SizeWatcher.observer) {
return;
}
const element = SizeWatcher.elements.get(elementId);
if (element) {
SizeWatcher.elements.delete(elementId);
SizeWatcher.observer.unobserve(element);
}
}
static init(): void {
if (SizeWatcher.observer) {
return;
}
SizeWatcher.elements = new Map<string, HTMLElement>();
SizeWatcher.observer = new ResizeObserver((entries) => {
for (const entry of entries) {
SizeWatcher.invoke(entry.target);
}
});
}
static invoke(element: Element): void {
const watcherElement = element as SizeWatcherElement;
const instance = watcherElement.SizeWatcher;
if (!instance || !instance.callback) {
return;
}
return instance.callback(element.clientWidth, element.clientHeight);
}
}
export class DpiWatcher {
static lastDpi: number;
static timerId: number;
static callback: (old: number, newdpi: number) => void;
public static getDpi(): number {
return window.devicePixelRatio;
}
public static start(callback: (old: number, newdpi: number) => void): number {
DpiWatcher.lastDpi = window.devicePixelRatio;
DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000);
DpiWatcher.callback = callback;
return DpiWatcher.lastDpi;
}
public static stop(): void {
window.clearInterval(DpiWatcher.timerId);
}
static update(): void {
if (!DpiWatcher.callback) {
return;
}
const currentDpi = window.devicePixelRatio;
const lastDpi = DpiWatcher.lastDpi;
DpiWatcher.lastDpi = currentDpi;
if (Math.abs(lastDpi - currentDpi) > 0.001) {
DpiWatcher.callback(lastDpi, currentDpi);
}
}
}

149
src/Web/Avalonia.Web/webapp/modules/avalonia/caretHelper.ts

@ -0,0 +1,149 @@
// 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?.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 - http://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;
}
}
const 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;

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

@ -0,0 +1,60 @@
export class AvaloniaDOM {
public static addClass(element: HTMLElement, className: string): void {
element.classList.add(className);
}
static createAvaloniaHost(host: HTMLElement) {
// Root element
host.classList.add("avalonia-container");
host.tabIndex = 0;
host.oncontextmenu = function () { return false; };
// Rendering target canvas
const canvas = document.createElement("canvas");
canvas.classList.add("avalonia-canvas");
canvas.style.backgroundColor = "#ccc";
canvas.style.width = "100%";
canvas.style.height = "100%";
canvas.style.position = "absolute";
// Native controls host
const nativeHost = document.createElement("div");
nativeHost.classList.add("avalonia-native-host");
nativeHost.style.left = "0px";
nativeHost.style.top = "0px";
nativeHost.style.width = "100%";
nativeHost.style.height = "100%";
nativeHost.style.position = "absolute";
// IME
const inputElement = document.createElement("input");
inputElement.classList.add("avalonia-input-element");
inputElement.autocapitalize = "none";
inputElement.type = "text";
inputElement.spellcheck = false;
inputElement.style.padding = "0";
inputElement.style.margin = "0";
inputElement.style.position = "absolute";
inputElement.style.overflow = "hidden";
inputElement.style.borderStyle = "hidden";
inputElement.style.outline = "none";
inputElement.style.background = "transparent";
inputElement.style.color = "transparent";
inputElement.style.display = "none";
inputElement.style.height = "20px";
inputElement.onpaste = function () { return false; };
inputElement.oncopy = function () { return false; };
inputElement.oncut = function () { return false; };
host.prepend(inputElement);
host.prepend(nativeHost);
host.prepend(canvas);
return {
host,
canvas,
nativeHost,
inputElement
};
}
}

204
src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts

@ -0,0 +1,204 @@
import { CaretHelper } from "./caretHelper";
enum RawInputModifiers {
None = 0,
Alt = 1,
Control = 2,
Shift = 4,
Meta = 8,
LeftMouseButton = 16,
RightMouseButton = 32,
MiddleMouseButton = 64,
XButton1MouseButton = 128,
XButton2MouseButton = 256,
KeyboardMask = Alt | Control | Shift | Meta,
PenInverted = 512,
PenEraser = 1024,
PenBarrelButton = 2048
}
export class InputHelper {
public static subscribeKeyEvents(
element: HTMLInputElement,
keyDownCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean,
keyUpCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean) {
const keyDownHandler = (args: KeyboardEvent) => {
if (keyDownCallback(args.code, args.key, this.getModifiers(args))) {
args.preventDefault();
}
};
element.addEventListener("keydown", keyDownHandler);
const keyUpHandler = (args: KeyboardEvent) => {
if (keyUpCallback(args.code, args.key, this.getModifiers(args))) {
args.preventDefault();
}
};
element.addEventListener("keyup", keyUpHandler);
return () => {
element.removeEventListener("keydown", keyDownHandler);
element.removeEventListener("keyup", keyUpHandler);
};
}
public static subscribeTextEvents(
element: HTMLInputElement,
inputCallback: (type: string, data: string | null) => boolean,
compositionStartCallback: (args: CompositionEvent) => boolean,
compositionUpdateCallback: (args: CompositionEvent) => boolean,
compositionEndCallback: (args: CompositionEvent) => boolean) {
const inputHandler = (args: Event) => {
const inputEvent = args as InputEvent;
// todo check cast
if (inputCallback(inputEvent.type, inputEvent.data)) {
args.preventDefault();
}
};
element.addEventListener("input", inputHandler);
const compositionStartHandler = (args: CompositionEvent) => {
if (compositionStartCallback(args)) {
args.preventDefault();
}
};
element.addEventListener("compositionstart", compositionStartHandler);
const compositionUpdateHandler = (args: CompositionEvent) => {
if (compositionUpdateCallback(args)) {
args.preventDefault();
}
};
element.addEventListener("compositionupdate", compositionUpdateHandler);
const compositionEndHandler = (args: CompositionEvent) => {
if (compositionEndCallback(args)) {
args.preventDefault();
}
};
element.addEventListener("compositionend", compositionEndHandler);
return () => {
element.removeEventListener("input", inputHandler);
element.removeEventListener("compositionstart", compositionStartHandler);
element.removeEventListener("compositionupdate", compositionUpdateHandler);
element.removeEventListener("compositionend", compositionEndHandler);
};
}
public static subscribePointerEvents(
element: HTMLInputElement,
pointerMoveCallback: (args: PointerEvent) => boolean,
pointerDownCallback: (args: PointerEvent) => boolean,
pointerUpCallback: (args: PointerEvent) => boolean,
wheelCallback: (args: WheelEvent) => boolean
) {
const pointerMoveHandler = (args: PointerEvent) => {
if (pointerMoveCallback(args)) {
args.preventDefault();
}
};
const pointerDownHandler = (args: PointerEvent) => {
if (pointerDownCallback(args)) {
args.preventDefault();
}
};
const pointerUpHandler = (args: PointerEvent) => {
if (pointerUpCallback(args)) {
args.preventDefault();
}
};
const wheelHandler = (args: WheelEvent) => {
if (wheelCallback(args)) {
args.preventDefault();
}
};
element.addEventListener("pointermove", pointerMoveHandler);
element.addEventListener("pointerdown", pointerDownHandler);
element.addEventListener("pointerup", pointerUpHandler);
element.addEventListener("wheel", wheelHandler);
return () => {
element.removeEventListener("pointerover", pointerMoveHandler);
element.removeEventListener("pointerdown", pointerDownHandler);
element.removeEventListener("pointerup", pointerUpHandler);
element.removeEventListener("wheel", wheelHandler);
};
}
public static subscribeInputEvents(
element: HTMLInputElement,
inputCallback: (value: string) => boolean
) {
const inputHandler = (args: Event) => {
if (inputCallback((args as any).value)) {
args.preventDefault();
}
};
element.addEventListener("input", inputHandler);
return () => {
element.removeEventListener("input", inputHandler);
};
}
public static clearInput(inputElement: HTMLInputElement) {
inputElement.value = "";
}
public static focusElement(inputElement: HTMLElement) {
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";
const { 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 getModifiers(args: KeyboardEvent): RawInputModifiers {
let modifiers = RawInputModifiers.None;
if (args.ctrlKey) { modifiers |= RawInputModifiers.Control; }
if (args.altKey) { modifiers |= RawInputModifiers.Alt; }
if (args.shiftKey) { modifiers |= RawInputModifiers.Shift; }
if (args.metaKey) { modifiers |= RawInputModifiers.Meta; }
return modifiers;
}
}

55
src/Web/Avalonia.Web/webapp/modules/avalonia/nativeControlHost.ts

@ -0,0 +1,55 @@
class NativeControlHostTopLevelAttachment {
_child?: HTMLElement;
_host?: HTMLElement;
}
export class NativeControlHost {
public static createDefaultChild(parent?: HTMLElement): HTMLElement {
return document.createElement("div");
}
public static createAttachment(): NativeControlHostTopLevelAttachment {
return new NativeControlHostTopLevelAttachment();
}
public static initializeWithChildHandle(element: NativeControlHostTopLevelAttachment, child: HTMLElement): void {
element._child = child;
element._child.style.position = "absolute";
}
public static attachTo(element: NativeControlHostTopLevelAttachment, host?: HTMLElement): void {
if (element._host && element._child) {
element._host.removeChild(element._child);
}
element._host = host;
if (element._host && element._child) {
element._host.appendChild(element._child);
}
}
public static showInBounds(element: NativeControlHostTopLevelAttachment, x: number, y: number, width: number, height: number): void {
if (element._child) {
element._child.style.top = `${y}px`;
element._child.style.left = `${x}px`;
element._child.style.width = `${width}px`;
element._child.style.height = `${height}px`;
element._child.style.display = "block";
}
}
public static hideWithSize(element: NativeControlHostTopLevelAttachment, width: number, height: number): void {
if (element._child) {
element._child.style.width = `${width}px`;
element._child.style.height = `${height}px`;
element._child.style.display = "none";
}
}
public static releaseChild(element: NativeControlHostTopLevelAttachment): void {
if (element._child) {
element._child = undefined;
}
}
}

40
src/Web/Avalonia.Web/webapp/modules/avalonia/stream.ts

@ -0,0 +1,40 @@
import { IMemoryView } from "../../types/dotnet";
export class StreamHelper {
public static async seek(stream: FileSystemWritableFileStream, position: number) {
return await stream.seek(position);
}
public static async truncate(stream: FileSystemWritableFileStream, size: number) {
return await stream.truncate(size);
}
public static async close(stream: FileSystemWritableFileStream) {
return await stream.close();
}
public static async write(stream: FileSystemWritableFileStream, span: IMemoryView) {
const array = new Uint8Array(span.byteLength);
span.copyTo(array);
const data: WriteParams = {
type: "write",
data: array
};
return await stream.write(data);
}
public static byteLength(stream: Blob) {
return stream.size;
}
public static async sliceArrayBuffer(stream: Blob, offset: number, count: number) {
const buffer = await stream.slice(offset, offset + count).arrayBuffer();
return new Uint8Array(buffer);
}
public static toMemoryView(buffer: Uint8Array): Uint8Array {
return buffer;
}
}

2
src/Web/Avalonia.Web/webapp/modules/storage.ts

@ -0,0 +1,2 @@
export { StorageItem, StorageItems } from "./storage/storageItem";
export { StorageProvider } from "./storage/storageProvider";

84
src/Web/Avalonia.Web/webapp/modules/storage/indexedDb.ts

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

111
src/Web/Avalonia.Web/webapp/modules/storage/storageItem.ts

@ -0,0 +1,111 @@
import { avaloniaDb, fileBookmarksStore } from "./indexedDb";
export class StorageItem {
constructor(public handle: FileSystemHandle, private readonly bookmarkId?: string) { }
public get name(): string {
return this.handle.name;
}
public get kind(): string {
return this.handle.kind;
}
public static async openRead(item: StorageItem): Promise<Blob> {
if (!(item.handle instanceof FileSystemFileHandle)) {
throw new Error("StorageItem is not a file");
}
await item.verityPermissions("read");
const file = await item.handle.getFile();
return file;
}
public static async openWrite(item: StorageItem): Promise<FileSystemWritableFileStream> {
if (!(item.handle instanceof FileSystemFileHandle)) {
throw new Error("StorageItem is not a file");
}
await item.verityPermissions("readwrite");
return await item.handle.createWritable({ keepExistingData: true });
}
public static async getProperties(item: StorageItem): Promise<{ Size: number; LastModified: number; Type: string } | null> {
const file = item.handle instanceof FileSystemFileHandle &&
await item.handle.getFile();
if (!file) {
return null;
}
return {
Size: file.size,
LastModified: file.lastModified,
Type: file.type
};
}
public static async getItems(item: StorageItem): Promise<StorageItems> {
if (item.handle.kind !== "directory") {
return new StorageItems([]);
}
const items: StorageItem[] = [];
for await (const [, value] of (item.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("Permissions denied");
}
}
public static async saveBookmark(item: StorageItem): Promise<string> {
// If file was previously bookmarked, just return old one.
if (item.bookmarkId) {
return item.bookmarkId;
}
const connection = await avaloniaDb.connect();
try {
const key = await connection.put(fileBookmarksStore, item.handle, item.generateBookmarkId());
return key as string;
} finally {
connection.close();
}
}
public static async deleteBookmark(item: StorageItem): Promise<void> {
if (!item.bookmarkId) {
return;
}
const connection = await avaloniaDb.connect();
try {
await connection.delete(fileBookmarksStore, item.bookmarkId);
} finally {
connection.close();
}
}
private generateBookmarkId(): string {
return Date.now().toString(36) + Math.random().toString(36).substring(2);
}
}
export class StorageItems {
constructor(private readonly items: StorageItem[]) { }
public static itemsArray(instance: StorageItems): StorageItem[] {
return instance.items;
}
}

70
src/Web/Avalonia.Web/webapp/modules/storage/storageProvider.ts

@ -0,0 +1,70 @@
import { avaloniaDb, fileBookmarksStore } from "./indexedDb";
import { StorageItem, StorageItems } from "./storageItem";
declare global {
type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
type StartInDirectory = WellKnownDirectory | FileSystemHandle;
interface OpenFilePickerOptions {
startIn?: StartInDirectory;
}
interface SaveFilePickerOptions {
startIn?: StartInDirectory;
}
}
export class StorageProvider {
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();
}
}
public static createAcceptType(description: string, mimeTypes: string[]): FilePickerAcceptType {
const accept: Record<string, string[]> = {};
mimeTypes.forEach(a => { accept[a] = []; });
return { description, accept };
}
}

2234
src/Web/Avalonia.Web/webapp/package-lock.json

File diff suppressed because it is too large

22
src/Web/Avalonia.Web/webapp/package.json

@ -0,0 +1,22 @@
{
"name": "avalonia.web",
"scripts": {
"typecheck": "npx tsc -noEmit",
"eslint": "npx eslint . --fix",
"prebuild": "npm-run-all typecheck eslint",
"build": "node build.js"
},
"devDependencies": {
"@types/emscripten": "^1.39.6",
"@types/wicg-file-system-access": "^2020.9.5",
"@typescript-eslint/eslint-plugin": "^5.38.1",
"esbuild": "^0.15.7",
"eslint": "^8.24.0",
"eslint-config-standard-with-typescript": "^23.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.3.0",
"eslint-plugin-promise": "^6.0.1",
"npm-run-all": "^4.1.5",
"typescript": "^4.8.3"
}
}

19
src/Web/Avalonia.Web/webapp/tsconfig.json

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

270
src/Web/Avalonia.Web/webapp/types/dotnet.d.ts

@ -0,0 +1,270 @@
// See https://raw.githubusercontent.com/dotnet/runtime/main/src/mono/wasm/runtime/dotnet.d.ts
//! Licensed to the .NET Foundation under one or more agreements.
//! The .NET Foundation licenses this file to you under the MIT license.
//!
//! This is generated file, see src/mono/wasm/runtime/rollup.config.js
//! This is not considered public API with backward compatibility guarantees.
interface DotnetHostBuilder {
withConfig(config: MonoConfig): DotnetHostBuilder;
withConfigSrc(configSrc: string): DotnetHostBuilder;
withApplicationArguments(...args: string[]): DotnetHostBuilder;
withEnvironmentVariable(name: string, value: string): DotnetHostBuilder;
withEnvironmentVariables(variables: {
[i: string]: string;
}): DotnetHostBuilder;
withVirtualWorkingDirectory(vfsPath: string): DotnetHostBuilder;
withDiagnosticTracing(enabled: boolean): DotnetHostBuilder;
withDebugging(level: number): DotnetHostBuilder;
withMainAssembly(mainAssemblyName: string): DotnetHostBuilder;
withApplicationArgumentsFromQuery(): DotnetHostBuilder;
create(): Promise<RuntimeAPI>;
run(): Promise<number>;
}
declare interface NativePointer {
__brandNativePointer: "NativePointer";
}
declare interface VoidPtr extends NativePointer {
__brand: "VoidPtr";
}
declare interface CharPtr extends NativePointer {
__brand: "CharPtr";
}
declare interface Int32Ptr extends NativePointer {
__brand: "Int32Ptr";
}
declare interface EmscriptenModule {
HEAP8: Int8Array;
HEAP16: Int16Array;
HEAP32: Int32Array;
HEAPU8: Uint8Array;
HEAPU16: Uint16Array;
HEAPU32: Uint32Array;
HEAPF32: Float32Array;
HEAPF64: Float64Array;
_malloc(size: number): VoidPtr;
_free(ptr: VoidPtr): void;
print(message: string): void;
printErr(message: string): void;
ccall<T>(ident: string, returnType?: string | null, argTypes?: string[], args?: any[], opts?: any): T;
cwrap<T extends Function>(ident: string, returnType: string, argTypes?: string[], opts?: any): T;
cwrap<T extends Function>(ident: string, ...args: any[]): T;
setValue(ptr: VoidPtr, value: number, type: string, noSafe?: number | boolean): void;
setValue(ptr: Int32Ptr, value: number, type: string, noSafe?: number | boolean): void;
getValue(ptr: number, type: string, noSafe?: number | boolean): number;
UTF8ToString(ptr: CharPtr, maxBytesToRead?: number): string;
UTF8ArrayToString(u8Array: Uint8Array, idx?: number, maxBytesToRead?: number): string;
FS_createPath(parent: string, path: string, canRead?: boolean, canWrite?: boolean): string;
FS_createDataFile(parent: string, name: string, data: TypedArray, canRead: boolean, canWrite: boolean, canOwn?: boolean): string;
FS_readFile(filename: string, opts: any): any;
removeRunDependency(id: string): void;
addRunDependency(id: string): void;
stackSave(): VoidPtr;
stackRestore(stack: VoidPtr): void;
stackAlloc(size: number): VoidPtr;
ready: Promise<unknown>;
instantiateWasm?: InstantiateWasmCallBack;
preInit?: (() => any)[] | (() => any);
preRun?: (() => any)[] | (() => any);
onRuntimeInitialized?: () => any;
postRun?: (() => any)[] | (() => any);
onAbort?: {
(error: any): void;
};
}
declare type InstantiateWasmSuccessCallback = (instance: WebAssembly.Instance, module: WebAssembly.Module) => void;
declare type InstantiateWasmCallBack = (imports: WebAssembly.Imports, successCallback: InstantiateWasmSuccessCallback) => any;
declare type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array;
declare type MonoConfig = {
/**
* The subfolder containing managed assemblies and pdbs. This is relative to dotnet.js script.
*/
assemblyRootFolder?: string;
/**
* A list of assets to load along with the runtime.
*/
assets?: AssetEntry[];
/**
* Additional search locations for assets.
*/
remoteSources?: string[];
/**
* It will not fail the startup is .pdb files can't be downloaded
*/
ignorePdbLoadErrors?: boolean;
/**
* We are throttling parallel downloads in order to avoid net::ERR_INSUFFICIENT_RESOURCES on chrome. The default value is 16.
*/
maxParallelDownloads?: number;
/**
* Name of the assembly with main entrypoint
*/
mainAssemblyName?: string;
/**
* Configures the runtime's globalization mode
*/
globalizationMode?: GlobalizationMode;
/**
* debugLevel > 0 enables debugging and sets the debug log level to debugLevel
* debugLevel == 0 disables debugging and enables interpreter optimizations
* debugLevel < 0 enabled debugging and disables debug logging.
*/
debugLevel?: number;
/**
* Enables diagnostic log messages during startup
*/
diagnosticTracing?: boolean;
/**
* Dictionary-style Object containing environment variables
*/
environmentVariables?: {
[i: string]: string;
};
/**
* initial number of workers to add to the emscripten pthread pool
*/
pthreadPoolSize?: number;
};
interface ResourceRequest {
name: string;
behavior: AssetBehaviours;
resolvedUrl?: string;
hash?: string;
}
interface LoadingResource {
name: string;
url: string;
response: Promise<Response>;
}
interface AssetEntry extends ResourceRequest {
/**
* If specified, overrides the path of the asset in the virtual filesystem and similar data structures once downloaded.
*/
virtualPath?: string;
/**
* Culture code
*/
culture?: string;
/**
* If true, an attempt will be made to load the asset from each location in MonoConfig.remoteSources.
*/
loadRemote?: boolean;
/**
* If true, the runtime startup would not fail if the asset download was not successful.
*/
isOptional?: boolean;
/**
* If provided, runtime doesn't have to fetch the data.
* Runtime would set the buffer to null after instantiation to free the memory.
*/
buffer?: ArrayBuffer;
/**
* It's metadata + fetch-like Promise<Response>
* If provided, the runtime doesn't have to initiate the download. It would just await the response.
*/
pendingDownload?: LoadingResource;
}
declare type AssetBehaviours = "resource" | "assembly" | "pdb" | "heap" | "icu" | "vfs" | "dotnetwasm" | "js-module-threads";
declare type GlobalizationMode = "icu" | // load ICU globalization data from any runtime assets with behavior "icu".
"invariant" | // operate in invariant globalization mode.
"auto";
declare type DotnetModuleConfig = {
disableDotnet6Compatibility?: boolean;
config?: MonoConfig;
configSrc?: string;
onConfigLoaded?: (config: MonoConfig) => void | Promise<void>;
onDotnetReady?: () => void | Promise<void>;
imports?: any;
exports?: string[];
downloadResource?: (request: ResourceRequest) => LoadingResource | undefined;
} & Partial<EmscriptenModule>;
declare type APIType = {
runMain: (mainAssemblyName: string, args: string[]) => Promise<number>;
runMainAndExit: (mainAssemblyName: string, args: string[]) => Promise<number>;
setEnvironmentVariable: (name: string, value: string) => void;
getAssemblyExports(assemblyName: string): Promise<any>;
setModuleImports(moduleName: string, moduleImports: any): void;
getConfig: () => MonoConfig;
setHeapB32: (offset: NativePointer, value: number | boolean) => void;
setHeapU8: (offset: NativePointer, value: number) => void;
setHeapU16: (offset: NativePointer, value: number) => void;
setHeapU32: (offset: NativePointer, value: NativePointer | number) => void;
setHeapI8: (offset: NativePointer, value: number) => void;
setHeapI16: (offset: NativePointer, value: number) => void;
setHeapI32: (offset: NativePointer, value: number) => void;
setHeapI52: (offset: NativePointer, value: number) => void;
setHeapU52: (offset: NativePointer, value: number) => void;
setHeapI64Big: (offset: NativePointer, value: bigint) => void;
setHeapF32: (offset: NativePointer, value: number) => void;
setHeapF64: (offset: NativePointer, value: number) => void;
getHeapB32: (offset: NativePointer) => boolean;
getHeapU8: (offset: NativePointer) => number;
getHeapU16: (offset: NativePointer) => number;
getHeapU32: (offset: NativePointer) => number;
getHeapI8: (offset: NativePointer) => number;
getHeapI16: (offset: NativePointer) => number;
getHeapI32: (offset: NativePointer) => number;
getHeapI52: (offset: NativePointer) => number;
getHeapU52: (offset: NativePointer) => number;
getHeapI64Big: (offset: NativePointer) => bigint;
getHeapF32: (offset: NativePointer) => number;
getHeapF64: (offset: NativePointer) => number;
};
declare type RuntimeAPI = {
/**
* @deprecated Please use API object instead. See also MONOType in dotnet-legacy.d.ts
*/
MONO: any;
/**
* @deprecated Please use API object instead. See also BINDINGType in dotnet-legacy.d.ts
*/
BINDING: any;
INTERNAL: any;
Module: EmscriptenModule;
runtimeId: number;
runtimeBuildInfo: {
productVersion: string;
gitHash: string;
buildConfiguration: string;
};
} & APIType;
declare type ModuleAPI = {
dotnet: DotnetHostBuilder;
exit: (code: number, reason?: any) => void;
};
declare function createDotnetRuntime(moduleFactory: DotnetModuleConfig | ((api: RuntimeAPI) => DotnetModuleConfig)): Promise<RuntimeAPI>;
declare type CreateDotnetRuntimeType = typeof createDotnetRuntime;
declare global {
function getDotnetRuntime(runtimeId: number): RuntimeAPI | undefined;
}
declare const dotnet: ModuleAPI["dotnet"];
declare const exit: ModuleAPI["exit"];
export { CreateDotnetRuntimeType, DotnetModuleConfig, EmscriptenModule, ModuleAPI, MonoConfig, RuntimeAPI, createDotnetRuntime as default, dotnet, exit };
export interface IMemoryView {
/**
* copies elements from provided source to the wasm memory.
* target has to have the elements of the same type as the underlying C# array.
* same as TypedArray.set()
*/
set(source: TypedArray, targetOffset?: number): void;
/**
* copies elements from wasm memory to provided target.
* target has to have the elements of the same type as the underlying C# array.
*/
copyTo(target: TypedArray, sourceOffset?: number): void;
/**
* same as TypedArray.slice()
*/
slice(start?: number, end?: number): TypedArray;
get length(): number;
get byteLength(): number;
}

6
src/iOS/Avalonia.iOS/Avalonia.iOS.csproj

@ -4,6 +4,12 @@
<SupportedOSPlatformVersion>13.0</SupportedOSPlatformVersion>
<MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn></NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<NoWarn></NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />

8
src/iOS/Avalonia.iOS/CombinedSpan3.cs

@ -16,7 +16,7 @@ internal ref struct CombinedSpan3<T>
public int Length => Span1.Length + Span2.Length + Span3.Length;
void CopyFromSpan(ReadOnlySpan<T> from, ref int offset, ref Span<T> to)
void CopyFromSpan(ReadOnlySpan<T> from, int offset, ref Span<T> to)
{
if(to.Length == 0)
return;
@ -33,8 +33,8 @@ internal ref struct CombinedSpan3<T>
public void CopyTo(Span<T> to, int offset)
{
CopyFromSpan(Span1, ref offset, ref to);
CopyFromSpan(Span2, ref offset, ref to);
CopyFromSpan(Span3, ref offset, ref to);
CopyFromSpan(Span1, offset, ref to);
CopyFromSpan(Span2, offset, ref to);
CopyFromSpan(Span3, offset, ref to);
}
}

Loading…
Cancel
Save