diff --git a/.gitignore b/.gitignore
index 84faae1806..61a3b53de1 100644
--- a/.gitignore
+++ b/.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
diff --git a/Avalonia.sln b/Avalonia.sln
index 68335c672c..81a9b43890 100644
--- a/Avalonia.sln
+++ b/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}
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index ba351b91a3..33b2dc670a 100644
--- a/azure-pipelines.yml
+++ b/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'
diff --git a/global.json b/global.json
index 7c2daee25f..44d4e10dbf 100644
--- a/global.json
+++ b/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",
diff --git a/nukebuild/BuildTasksPatcher.cs b/nukebuild/BuildTasksPatcher.cs
index e3766ae23f..5fd331035a 100644
--- a/nukebuild/BuildTasksPatcher.cs
+++ b/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
{
diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj
index 5db9579876..8c0d824298 100644
--- a/nukebuild/_build.csproj
+++ b/nukebuild/_build.csproj
@@ -4,9 +4,9 @@
false
False
- CS0649;CS0169
+ CS0649;CS0169;SYSLIB0011
1
- net6.0
+ net7.0
@@ -40,5 +40,9 @@
+
+
+
+
diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs
index 46aa6efa72..4aa84e3ec4 100644
--- a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs
+++ b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs
@@ -18,6 +18,7 @@ public interface IStorageFile : IStorageItem
///
/// Opens a stream for read access.
///
+ ///
Task OpenReadAsync();
///
@@ -28,5 +29,6 @@ public interface IStorageFile : IStorageItem
///
/// Opens stream for writing to the file.
///
+ ///
Task OpenWriteAsync();
}
diff --git a/src/Avalonia.Base/Properties/AssemblyInfo.cs b/src/Avalonia.Base/Properties/AssemblyInfo.cs
index c8368e6d7a..d5e01087ef 100644
--- a/src/Avalonia.Base/Properties/AssemblyInfo.cs
+++ b/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")]
diff --git a/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs b/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs
index bd3b86f06d..b8692bb771 100644
--- a/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs
+++ b/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;
diff --git a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs
index c20b2f656e..75758d1315 100644
--- a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs
+++ b/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;
diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
index 5b5951e800..0e64e98f1e 100644
--- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
+++ b/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);
diff --git a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj
new file mode 100644
index 0000000000..13aad8c13e
--- /dev/null
+++ b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj
@@ -0,0 +1,41 @@
+
+
+ net7.0
+ browser-wasm
+ main.js
+ Exe
+ true
+ true
+ true
+ -sVERBOSE -sERROR_ON_UNDEFINED_SYMBOLS=0
+
+
+
+ true
+ true
+ full
+ true
+ true
+ true
+ -O3
+ -O3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Web/Avalonia.Web.Sample/EmbedSample.Browser.cs b/src/Web/Avalonia.Web.Sample/EmbedSample.Browser.cs
new file mode 100644
index 0000000000..5baa4a6b35
--- /dev/null
+++ b/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 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);
+}
diff --git a/src/Web/Avalonia.Web.Sample/Logo.svg b/src/Web/Avalonia.Web.Sample/Logo.svg
new file mode 100644
index 0000000000..9685a23af1
--- /dev/null
+++ b/src/Web/Avalonia.Web.Sample/Logo.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs
new file mode 100644
index 0000000000..52acabb0fa
--- /dev/null
+++ b/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();
+}
diff --git a/src/Web/Avalonia.Web.Sample/app.css b/src/Web/Avalonia.Web.Sample/app.css
new file mode 100644
index 0000000000..04ea5bee19
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/src/Web/Avalonia.Web.Sample/embed.js b/src/Web/Avalonia.Web.Sample/embed.js
new file mode 100644
index 0000000000..f393c80314
--- /dev/null
+++ b/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;
+}
diff --git a/src/Web/Avalonia.Web.Sample/favicon.ico b/src/Web/Avalonia.Web.Sample/favicon.ico
new file mode 100644
index 0000000000..da8d49ff9b
Binary files /dev/null and b/src/Web/Avalonia.Web.Sample/favicon.ico differ
diff --git a/src/Web/Avalonia.Web.Sample/index.html b/src/Web/Avalonia.Web.Sample/index.html
new file mode 100644
index 0000000000..ee023790fb
--- /dev/null
+++ b/src/Web/Avalonia.Web.Sample/index.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+ Avalonia.Web.Sample
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Web/Avalonia.Web.Sample/main.js b/src/Web/Avalonia.Web.Sample/main.js
new file mode 100644
index 0000000000..3683aea181
--- /dev/null
+++ b/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!"]);
diff --git a/src/Web/Avalonia.Web.Sample/runtimeconfig.template.json b/src/Web/Avalonia.Web.Sample/runtimeconfig.template.json
new file mode 100644
index 0000000000..8f0557352c
--- /dev/null
+++ b/src/Web/Avalonia.Web.Sample/runtimeconfig.template.json
@@ -0,0 +1,11 @@
+{
+ "wasmHostProperties": {
+ "perHostConfig": [
+ {
+ "name": "browser",
+ "html-path": "index.html",
+ "Host": "browser"
+ }
+ ]
+ }
+}
diff --git a/src/Web/Avalonia.Web/Avalonia.Web.csproj b/src/Web/Avalonia.Web/Avalonia.Web.csproj
new file mode 100644
index 0000000000..a4756a5e2b
--- /dev/null
+++ b/src/Web/Avalonia.Web/Avalonia.Web.csproj
@@ -0,0 +1,55 @@
+
+
+ net7.0
+ preview
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ build\
+
+
+ true
+ build\;buildTransitive\
+
+
+ true
+ build/interop.js;buildTransitive/interop.js
+
+
+ true
+ build\wwwroot;buildTransitive\wwwroot
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Web/Avalonia.Web/Avalonia.Web.props b/src/Web/Avalonia.Web/Avalonia.Web.props
new file mode 100644
index 0000000000..6c975cd284
--- /dev/null
+++ b/src/Web/Avalonia.Web/Avalonia.Web.props
@@ -0,0 +1,5 @@
+
+
+ $(EmccExtraLDFlags) --js-library="$(MSBuildThisFileDirectory)\interop.js"
+
+
diff --git a/src/Web/Avalonia.Web/Avalonia.Web.targets b/src/Web/Avalonia.Web/Avalonia.Web.targets
new file mode 100644
index 0000000000..d1bec2aa93
--- /dev/null
+++ b/src/Web/Avalonia.Web/Avalonia.Web.targets
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs
new file mode 100644
index 0000000000..e81620ffde
--- /dev/null
+++ b/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();
+
+ _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);
+ }
+ }
+}
diff --git a/src/Web/Avalonia.Web/BrowserNativeControlHost.cs b/src/Web/Avalonia.Web/BrowserNativeControlHost.cs
new file mode 100644
index 0000000000..4cdcf627e6
--- /dev/null
+++ b/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 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));
+ }
+ }
+ }
+}
diff --git a/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs b/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs
new file mode 100644
index 0000000000..091ab3f68c
--- /dev/null
+++ b/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(
+ this T builder, string mainDivId)
+ where T : AppBuilderBase, 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);
+ }
+ }
+}
diff --git a/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs
new file mode 100644
index 0000000000..b955da6df2
--- /dev/null
+++ b/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