diff --git a/api/Avalonia.Browser.nupkg.xml b/api/Avalonia.Browser.nupkg.xml
new file mode 100644
index 0000000000..16bdbfe25d
--- /dev/null
+++ b/api/Avalonia.Browser.nupkg.xml
@@ -0,0 +1,22 @@
+
+
+
+
+ CP0002
+ M:Avalonia.Browser.AvaloniaView.get_IsComposing
+ baseline/net7.0/Avalonia.Browser.dll
+ target/net8.0-browser1.0/Avalonia.Browser.dll
+
+
+ CP0002
+ M:Avalonia.Browser.AvaloniaView.OnDragEvent(System.Runtime.InteropServices.JavaScript.JSObject)
+ baseline/net7.0/Avalonia.Browser.dll
+ target/net8.0-browser1.0/Avalonia.Browser.dll
+
+
+ CP0008
+ T:Avalonia.Browser.AvaloniaView
+ baseline/net7.0/Avalonia.Browser.dll
+ target/net8.0-browser1.0/Avalonia.Browser.dll
+
+
\ No newline at end of file
diff --git a/samples/ControlCatalog.Browser/AppBundle/Logo.svg b/samples/ControlCatalog.Browser/AppBundle/Logo.svg
deleted file mode 100644
index 9685a23af1..0000000000
--- a/samples/ControlCatalog.Browser/AppBundle/Logo.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/samples/ControlCatalog.Browser/AppBundle/app.css b/samples/ControlCatalog.Browser/AppBundle/app.css
deleted file mode 100644
index e14dfe4487..0000000000
--- a/samples/ControlCatalog.Browser/AppBundle/app.css
+++ /dev/null
@@ -1,74 +0,0 @@
-:root {
- --sat: env(safe-area-inset-top);
- --sar: env(safe-area-inset-right);
- --sab: env(safe-area-inset-bottom);
- --sal: env(safe-area-inset-left);
-}
-
-/* HTML styles for the splash screen */
-
-.highlight {
- color: white;
- font-size: 2.5rem;
- display: block;
-}
-
-.purple {
- color: #8b44ac;
-}
-
-.icon {
- opacity: 0.05;
- height: 35%;
- width: 35%;
- position: absolute;
- background-repeat: no-repeat;
- right: 0px;
- bottom: 0px;
- margin-right: 3%;
- margin-bottom: 5%;
- z-index: 5000;
- background-position: right bottom;
- pointer-events: none;
-}
-
-#avalonia-splash a {
- color: whitesmoke;
- text-decoration: none;
-}
-
-.center {
- display: flex;
- justify-content: center;
- align-items: center;
- height: 100vh;
-}
-
-#avalonia-splash {
- position: relative;
- height: 100%;
- width: 100%;
- color: whitesmoke;
- background: #1b2a4e;
- font-family: 'Nunito', sans-serif;
- background-position: center;
- background-size: cover;
- background-repeat: no-repeat;
- justify-content: center;
- align-items: center;
-}
-
-.splash-close {
- animation: fadeout 0.25s linear forwards;
-}
-
-@keyframes fadeout {
- 0% {
- opacity: 100%;
- }
-
- 100% {
- opacity: 0;
- visibility: collapse;
- }
-}
diff --git a/samples/ControlCatalog.Browser/AppBundle/index.html b/samples/ControlCatalog.Browser/AppBundle/index.html
deleted file mode 100644
index b35acaed5c..0000000000
--- a/samples/ControlCatalog.Browser/AppBundle/index.html
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
- AvaloniaUI - ControlCatalog
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj b/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj
index cc4087d2f0..f223eb0725 100644
--- a/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj
+++ b/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj
@@ -2,10 +2,9 @@
$(AvsCurrentBrowserTargetFramework)
browser-wasm
- AppBundle/main.js
+ wwwroot/main.js
Exe
true
- ./
@@ -15,7 +14,7 @@
-
+
diff --git a/samples/ControlCatalog.Browser/Program.cs b/samples/ControlCatalog.Browser/Program.cs
index 919df5103c..c50f1dcbdd 100644
--- a/samples/ControlCatalog.Browser/Program.cs
+++ b/samples/ControlCatalog.Browser/Program.cs
@@ -1,40 +1,108 @@
+using System;
using System.Diagnostics;
+using System.Linq;
using System.Runtime.Versioning;
using System.Threading.Tasks;
+using System.Web;
using Avalonia;
using Avalonia.Browser;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Logging;
+using Avalonia.Rendering;
+using Avalonia.Threading;
using ControlCatalog;
using ControlCatalog.Browser;
[assembly:SupportedOSPlatform("browser")]
+#nullable enable
internal partial class Program
{
public static async Task Main(string[] args)
{
Trace.Listeners.Add(new ConsoleTraceListener());
-
+
+ var options = ParseArgs(args) ?? new BrowserPlatformOptions();
+
await BuildAvaloniaApp()
- .LogToTrace(LogEventLevel.Warning)
+ .LogToTrace()
.AfterSetup(_ =>
{
ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb();
})
- .StartBrowserAppAsync("out");
+ .StartBrowserAppAsync("out", options);
+
+ if (Application.Current!.ApplicationLifetime is ISingleTopLevelApplicationLifetime lifetime)
+ {
+ lifetime.TopLevel!.RendererDiagnostics.DebugOverlays = RendererDebugOverlays.Fps;
+ }
}
- // Example without a ISingleViewApplicationLifetime
- // private static AvaloniaView _avaloniaView;
+ // Test with multiple AvaloniaView at once.
+ // private static AvaloniaView _avaloniaView1;
+ // private static AvaloniaView _avaloniaView2;
// public static async Task Main(string[] args)
// {
+ // Trace.Listeners.Add(new ConsoleTraceListener());
+ //
+ // var options = ParseArgs(args) ?? new BrowserPlatformOptions();
+ //
// await BuildAvaloniaApp()
- // .SetupBrowserApp();
+ // .LogToTrace()
+ // .SetupBrowserAppAsync(options);
+ //
+ // _avaloniaView1 = new AvaloniaView("out1");
+ // _avaloniaView1.Content = new TextBlock { Text = "Hello" };
//
- // _avaloniaView = new AvaloniaView("out");
- // _avaloniaView.Content = new TextBlock { Text = "Hello world" };
+ // _avaloniaView2 = new AvaloniaView("out2");
+ // _avaloniaView2.Content = new TextBlock { Text = "World" };
+ //
+ // Dispatcher.UIThread.Invoke(() =>
+ // {
+ // var topLevel = TopLevel.GetTopLevel(_avaloniaView1.Content);
+ // topLevel!.RendererDiagnostics.DebugOverlays = RendererDebugOverlays.Fps;
+ // });
// }
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure();
+
+ private static BrowserPlatformOptions? ParseArgs(string[] args)
+ {
+ try
+ {
+ if (args.Length == 0
+ || !Uri.TryCreate(args[0], UriKind.Absolute, out var uri)
+ || uri.Query.Length <= 1)
+ {
+ uri = new Uri("http://localhost");
+ }
+
+ var queryParams = HttpUtility.ParseQueryString(uri.Query);
+ var options = new BrowserPlatformOptions();
+
+ if (bool.TryParse(queryParams[nameof(options.PreferFileDialogPolyfill)], out var preferDialogsPolyfill))
+ {
+ options.PreferFileDialogPolyfill = preferDialogsPolyfill;
+ }
+
+ if (queryParams[nameof(options.RenderingMode)] is { } renderingModePairs)
+ {
+ options.RenderingMode = renderingModePairs
+ .Split(';', StringSplitOptions.RemoveEmptyEntries)
+ .Select(entry => Enum.Parse(entry, true))
+ .ToArray();
+ }
+
+ Console.WriteLine("DemoBrowserPlatformOptions.PreferFileDialogPolyfill: " + options.PreferFileDialogPolyfill);
+ Console.WriteLine("DemoBrowserPlatformOptions.RenderingMode: " + string.Join(";", options.RenderingMode));
+ return options;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("ParseArgs of DemoBrowserPlatformOptions failed: " + ex);
+ return null;
+ }
+ }
}
diff --git a/samples/ControlCatalog.Browser/wwwroot/Logo.svg b/samples/ControlCatalog.Browser/wwwroot/Logo.svg
new file mode 100644
index 0000000000..3e18ea1958
--- /dev/null
+++ b/samples/ControlCatalog.Browser/wwwroot/Logo.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/ControlCatalog.Browser/wwwroot/app.css b/samples/ControlCatalog.Browser/wwwroot/app.css
new file mode 100644
index 0000000000..77f2051221
--- /dev/null
+++ b/samples/ControlCatalog.Browser/wwwroot/app.css
@@ -0,0 +1,38 @@
+/* HTML styles for the splash screen */
+.avalonia-splash {
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ background: #1b2a4e;
+ font-family: 'Nunito', sans-serif;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ pointer-events: none;
+}
+
+.avalonia-splash h2 {
+ font-size: 1.5rem;
+ color: #8b44ac;
+}
+
+.avalonia-splash a {
+ color: white;
+ text-decoration: none;
+ font-size: 2.5rem;
+ display: block;
+}
+
+.avalonia-splash img {
+ opacity: 0.05;
+ height: 35%;
+ position: absolute;
+ right: 3%;
+ bottom: 3%;
+}
+
+.avalonia-splash.splash-close {
+ transition: opacity 200ms, display 200ms;
+ display: none;
+ opacity: 0;
+}
diff --git a/samples/ControlCatalog.Browser/AppBundle/embed.js b/samples/ControlCatalog.Browser/wwwroot/embed.js
similarity index 100%
rename from samples/ControlCatalog.Browser/AppBundle/embed.js
rename to samples/ControlCatalog.Browser/wwwroot/embed.js
diff --git a/samples/ControlCatalog.Browser/AppBundle/favicon.ico b/samples/ControlCatalog.Browser/wwwroot/favicon.ico
similarity index 100%
rename from samples/ControlCatalog.Browser/AppBundle/favicon.ico
rename to samples/ControlCatalog.Browser/wwwroot/favicon.ico
diff --git a/samples/ControlCatalog.Browser/wwwroot/index.html b/samples/ControlCatalog.Browser/wwwroot/index.html
new file mode 100644
index 0000000000..d8bf05fe3c
--- /dev/null
+++ b/samples/ControlCatalog.Browser/wwwroot/index.html
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+ AvaloniaUI - ControlCatalog
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog.Browser/AppBundle/main.js b/samples/ControlCatalog.Browser/wwwroot/main.js
similarity index 63%
rename from samples/ControlCatalog.Browser/AppBundle/main.js
rename to samples/ControlCatalog.Browser/wwwroot/main.js
index 9eae9fd740..35c8245b01 100644
--- a/samples/ControlCatalog.Browser/AppBundle/main.js
+++ b/samples/ControlCatalog.Browser/wwwroot/main.js
@@ -1,17 +1,15 @@
// 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' // NET 7
-//import { dotnet } from './_framework/dotnet.js' // NET 8+
+import { dotnet } from './_framework/dotnet.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();
const config = dotnetRuntime.getConfig();
-await dotnetRuntime.runMainAndExit(config.mainAssemblyName, [globalThis.location.href]);
+await dotnetRuntime.runMain(config.mainAssemblyName, [globalThis.location.href]);
diff --git a/samples/MobileSandbox.Browser/Logo.svg b/samples/MobileSandbox.Browser/Logo.svg
index 9685a23af1..2560af46cf 100644
--- a/samples/MobileSandbox.Browser/Logo.svg
+++ b/samples/MobileSandbox.Browser/Logo.svg
@@ -1,5 +1,5 @@
-
-
-
-
+
+
+
+
diff --git a/samples/MobileSandbox.Browser/app.css b/samples/MobileSandbox.Browser/app.css
index 0e6ab12461..77f2051221 100644
--- a/samples/MobileSandbox.Browser/app.css
+++ b/samples/MobileSandbox.Browser/app.css
@@ -1,56 +1,38 @@
-:root {
- --sat: env(safe-area-inset-top);
- --sar: env(safe-area-inset-right);
- --sab: env(safe-area-inset-bottom);
- --sal: env(safe-area-inset-left);
-}
-
-#out {
- height: 100vh;
- width: 100vw
-}
-
-#avalonia-splash {
- position: relative;
+/* HTML styles for the splash screen */
+.avalonia-splash {
+ position: absolute;
height: 100%;
width: 100%;
- color: whitesmoke;
- background: #171C2C;
+ background: #1b2a4e;
font-family: 'Nunito', sans-serif;
- background-position: center;
- background-size: cover;
- background-repeat: no-repeat;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ pointer-events: none;
}
-#avalonia-splash a{
- color: whitesmoke;
- text-decoration: none;
+.avalonia-splash h2 {
+ font-size: 1.5rem;
+ color: #8b44ac;
}
-.center {
- display: flex;
- justify-content: center;
- height: 250px;
+.avalonia-splash a {
+ color: white;
+ text-decoration: none;
+ font-size: 2.5rem;
+ display: block;
}
-.splash-close {
- animation: slide 0.5s linear 1s forwards;
+.avalonia-splash img {
+ opacity: 0.05;
+ height: 35%;
+ position: absolute;
+ right: 3%;
+ bottom: 3%;
}
-@keyframes slide {
- 0% {
- top: 0%;
- }
-
- 50% {
- opacity: 80%;
- }
-
- 100% {
- top: 100%;
- overflow: hidden;
- opacity: 0;
- display: none;
- visibility: collapse;
- }
+.avalonia-splash.splash-close {
+ transition: opacity 200ms, display 200ms;
+ display: none;
+ opacity: 0;
}
diff --git a/src/Avalonia.Base/Data/Core/TargetTypeConverter.cs b/src/Avalonia.Base/Data/Core/TargetTypeConverter.cs
index 7a4b0544bd..2efc5b42bd 100644
--- a/src/Avalonia.Base/Data/Core/TargetTypeConverter.cs
+++ b/src/Avalonia.Base/Data/Core/TargetTypeConverter.cs
@@ -68,6 +68,7 @@ internal abstract class TargetTypeConverter
#pragma warning disable IL2026
#pragma warning disable IL2067
+#pragma warning disable IL2072
// TODO: TypeConverters are not trimming friendly in some edge cases, we probably need
// to make compiled bindings emit conversion code at compile-time.
var toTypeConverter = TypeDescriptor.GetConverter(t);
diff --git a/src/Browser/Avalonia.Browser/AvaloniaView.cs b/src/Browser/Avalonia.Browser/AvaloniaView.cs
index e2a6212e13..ff397a14b8 100644
--- a/src/Browser/Avalonia.Browser/AvaloniaView.cs
+++ b/src/Browser/Avalonia.Browser/AvaloniaView.cs
@@ -1,143 +1,59 @@
using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Reflection;
using System.Runtime.InteropServices.JavaScript;
using Avalonia.Browser.Interop;
-using Avalonia.Browser.Skia;
-using Avalonia.Collections.Pooled;
using Avalonia.Controls;
using Avalonia.Controls.Embedding;
-using Avalonia.Controls.Platform;
-using Avalonia.Input;
-using Avalonia.Input.Raw;
-using Avalonia.Input.TextInput;
-using Avalonia.Logging;
-using Avalonia.Media;
-using Avalonia.Platform;
-using Avalonia.Rendering.Composition;
-using Avalonia.Threading;
-using SkiaSharp;
-using static System.Runtime.CompilerServices.RuntimeHelpers;
namespace Avalonia.Browser
{
- public class AvaloniaView : ITextInputMethodImpl
+ public class AvaloniaView
{
- private static readonly PooledList s_intermediatePointsPooledList = new(ClearMode.Never);
- 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 TextInputMethodClient? _client;
+ private readonly EmbeddableControlRoot _topLevel;
/// ID of the html element where avalonia content should be rendered.
public AvaloniaView(string divId)
- : this(DomHelper.GetElementById(divId) ?? throw new Exception($"Element with id '{divId}' was not found in the html document."))
+ : this(DomHelper.GetElementById(divId) ??
+ throw new Exception($"Element with id '{divId}' was not found in the html document."))
{
}
+ /// JSObject holding a div element where avalonia content should be rendered.
public AvaloniaView(JSObject host)
{
+ if (host is null)
+ {
+ throw new ArgumentNullException(nameof(host));
+ }
+
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");
+ var nativeControlsContainer = hostContent.GetPropertyAsJSObject("nativeHost")
+ ?? throw new InvalidOperationException("NativeHost cannot be null");
+ var inputElement = hostContent.GetPropertyAsJSObject("inputElement")
+ ?? throw new InvalidOperationException("InputElement cannot be null");
- _splash = DomHelper.GetElementById("avalonia-splash");
+ var topLevelImpl = new BrowserTopLevelImpl(host, nativeControlsContainer, inputElement);
+ _topLevel = new EmbeddableControlRoot(topLevelImpl);
- _topLevelImpl = new BrowserTopLevelImpl(this, _containerElement);
- _topLevelImpl.SetCssCursor = (cursor) =>
- {
- InputHelper.SetCursor(_containerElement, cursor);
- };
-
- _topLevel = new EmbeddableControlRoot(_topLevelImpl);
_topLevel.Prepare();
- _topLevel.Renderer.Start();
- if (_splash != null)
- {
- _topLevel.RequestAnimationFrame(_ => DomHelper.AddCssClass(_splash, "splash-close"));
- }
-
- InputHelper.InitializeBackgroundHandlers();
-
- InputHelper.SubscribeKeyEvents(
- _containerElement,
- OnKeyDown,
- OnKeyUp);
-
- InputHelper.SubscribeTextEvents(
- _inputElement,
- OnBeforeInput,
- OnCompositionStart,
- OnCompositionUpdate,
- OnCompositionEnd);
-
- InputHelper.SubscribePointerEvents(_containerElement, OnPointerMove, OnPointerDown, OnPointerUp,
- OnPointerCancel, OnWheel);
-
- InputHelper.SubscribeDropEvents(_containerElement, OnDragEvent);
-
- var skiaOptions = AvaloniaLocator.Current.GetService();
-
- _useGL = AvaloniaLocator.Current.GetService() != null;
-
- if (_useGL)
- {
- _jsGlInfo = CanvasHelper.InitialiseGL(_canvas, OnRenderFrame);
- // create the SkiaSharp context
- if (_context == null)
+ _topLevel.GotFocus += (_, _) => InputHelper.FocusElement(host);
+ _topLevel.Renderer.Start(); // TODO: use Start+StopRenderer() instead.
+ _topLevel.RequestAnimationFrame(_ =>
+ {
+ // Try to get local splash-screen of the specific host.
+ // If couldn't find - get global one by ID for compatibility.
+ var splash = DomHelper.GetElementsByClassName("avalonia-splash", host)
+ ?? DomHelper.GetElementById("avalonia-splash");
+ if (splash is not null)
{
- _glInterface = GRGlInterface.Create();
- _context = GRContext.CreateGl(_glInterface);
-
- // bump the default resource cache limit
- _context.SetResourceCacheLimit(skiaOptions?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024);
+ DomHelper.AddCssClass(splash, "splash-close");
+ splash.Dispose();
}
-
- _topLevelImpl.Surfaces = new[]
- {
- new BrowserSkiaSurface(_context, _jsGlInfo, ColorType,
- new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _canvasSize,_dpi,
- GRSurfaceOrigin.BottomLeft)
- };
- }
- else
- {
- Logger.TryGet(LogEventLevel.Error, LogArea.BrowserPlatform)?
- .Log(this, "[Avalonia]: Unable to initialize Canvas surface.");
- }
-
- DomHelper.ObserveSize(host, OnSizeOrDpiChanged);
-
- CanvasHelper.RequestAnimationFrame(_canvas, true);
-
- InputHelper.FocusElement(_containerElement);
+ });
}
public Control? Content
@@ -146,402 +62,6 @@ namespace Avalonia.Browser
set => _topLevel.Content = value;
}
- public bool IsComposing { get; private set; }
-
internal TopLevel TopLevel => _topLevel;
-
- 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 pointerType = args.GetPropertyAsString("pointerType");
- var point = ExtractRawPointerFromJSArgs(args);
- var type = pointerType switch
- {
- "touch" => RawPointerEventType.TouchUpdate,
- _ => RawPointerEventType.Move
- };
-
- var coalescedEvents = new Lazy?>(() =>
- {
- var points = InputHelper.GetCoalescedEvents(args);
- s_intermediatePointsPooledList.Clear();
- s_intermediatePointsPooledList.Capacity = points.Length - 1;
-
- // Skip the last one, as it is already processed point.
- for (var i = 0; i < points.Length - 1; i++)
- {
- var point = points[i];
- s_intermediatePointsPooledList.Add(ExtractRawPointerFromJSArgs(point));
- }
-
- return s_intermediatePointsPooledList;
- });
-
- return _topLevelImpl.RawPointerEvent(type, pointerType!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"), coalescedEvents);
- }
-
- private bool OnPointerDown(JSObject args)
- {
- var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse";
- 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 => RawPointerEventType.XButton1Down, // should be 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 => RawPointerEventType.XButton1Up, // should be pen eraser button,
- _ => RawPointerEventType.Move
- }
- };
-
- var point = ExtractRawPointerFromJSArgs(args);
- return _topLevelImpl.RawPointerEvent(type, pointerType, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"));
- }
-
- private bool OnPointerCancel(JSObject args)
- {
- var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse";
- if (pointerType == "touch")
- {
- var point = ExtractRawPointerFromJSArgs(args);
- _topLevelImpl.RawPointerEvent(RawPointerEventType.TouchCancel, pointerType, point,
- GetModifiers(args), args.GetPropertyAsInt32("pointerId"));
- }
-
- return false;
- }
-
- private bool OnWheel(JSObject args)
- {
- return _topLevelImpl.RawMouseWheelEvent(new Point(args.GetPropertyAsDouble("offsetX"), args.GetPropertyAsDouble("offsetY")),
- 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;
- }
-
- public bool OnDragEvent(JSObject args)
- {
- var eventType = args?.GetPropertyAsString("type") switch
- {
- "dragenter" => RawDragEventType.DragEnter,
- "dragover" => RawDragEventType.DragOver,
- "dragleave" => RawDragEventType.DragLeave,
- "drop" => RawDragEventType.Drop,
- _ => (RawDragEventType)(int)-1
- };
- var dataObject = args?.GetPropertyAsJSObject("dataTransfer");
- if (args is null || eventType < 0 || dataObject is null)
- {
- return false;
- }
-
- // If file is dropped, we need storage js to be referenced.
- // TODO: restructure JS files, so it's not needed.
- _ = AvaloniaModule.ImportStorage();
-
- var position = new Point(args.GetPropertyAsDouble("offsetX"), args.GetPropertyAsDouble("offsetY"));
- var modifiers = GetModifiers(args);
-
- var effectAllowedStr = dataObject.GetPropertyAsString("effectAllowed") ?? "none";
- var effectAllowed = DragDropEffects.None;
- if (effectAllowedStr.Contains("copy", StringComparison.OrdinalIgnoreCase))
- {
- effectAllowed |= DragDropEffects.Copy;
- }
- if (effectAllowedStr.Contains("link", StringComparison.OrdinalIgnoreCase))
- {
- effectAllowed |= DragDropEffects.Link;
- }
- if (effectAllowedStr.Contains("move", StringComparison.OrdinalIgnoreCase))
- {
- effectAllowed |= DragDropEffects.Move;
- }
- if (effectAllowedStr.Equals("all", StringComparison.OrdinalIgnoreCase))
- {
- effectAllowed |= DragDropEffects.Move | DragDropEffects.Copy | DragDropEffects.Link;
- }
- if (effectAllowed == DragDropEffects.None)
- {
- return false;
- }
-
- var dropEffect = _topLevelImpl.RawDragEvent(eventType, position, modifiers, new BrowserDataObject(dataObject), effectAllowed);
- dataObject.SetProperty("dropEffect", dropEffect.ToString().ToLowerInvariant());
-
- return eventType is RawDragEventType.Drop or RawDragEventType.DragOver
- && dropEffect != DragDropEffects.None;
- }
-
- private bool OnKeyDown (string code, string key, int modifier)
- {
- var handled = _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)modifier);
-
- if (!handled && key.Length == 1)
- {
- handled = _topLevelImpl.RawTextEvent(key);
- }
-
- return handled;
- }
-
- private bool OnKeyUp(string code, string key, int modifier)
- {
- return _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, code, key, (RawInputModifiers)modifier);
- }
-
- private bool OnBeforeInput(JSObject arg, int start, int end)
- {
- var type = arg.GetPropertyAsString("inputType");
- if (type != "deleteByComposition")
- {
- if (type == "deleteContentBackward")
- {
- start = _inputElement.GetPropertyAsInt32("selectionStart");
- end = _inputElement.GetPropertyAsInt32("selectionEnd");
- }
- else
- {
- start = -1;
- end = -1;
- }
- }
-
- if(start != -1 && end != -1 && _client != null)
- {
- _client.Selection = new TextSelection(start, end);
- }
- return false;
- }
-
- 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);
-
- var text = args.GetPropertyAsString("data");
-
- if(text != null)
- {
- return _topLevelImpl.RawTextEvent(text);
- }
-
- return false;
- }
-
- private void OnRenderFrame()
- {
- if (_useGL && (_jsGlInfo == null))
- {
- return;
- }
- if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0)
- {
- return;
- }
-
- Dispatcher.UIThread.RunJobs(DispatcherPriority.UiThreadRender);
- ManualTriggerRenderTimer.Instance.RaiseTick();
- }
-
- 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)
- {
- MediaContext.Instance.ImmediateRenderRequested(dr.CompositionTarget, true);
- }
- }
-
- private void OnSizeOrDpiChanged(double displayWidth, double displayHeight, double dpi)
- {
- var newSize = new Size(displayWidth, displayHeight);
-
- if (_canvasSize != newSize || _dpi != dpi)
- {
- _dpi = dpi;
-
- _canvasSize = newSize;
-
- CanvasHelper.SetCanvasSize(_canvas, (int)_canvasSize.Width, (int)_canvasSize.Height);
-
- _topLevelImpl.SetClientSize(new(displayWidth / dpi, displayHeight / dpi), dpi);
-
- ForceBlit();
- }
- }
-
- private void HideIme()
- {
- InputHelper.HideElement(_inputElement);
- InputHelper.FocusElement(_containerElement);
- }
-
- void ITextInputMethodImpl.SetClient(TextInputMethodClient? 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 ?? "";
- var selection = _client.Selection;
-
- InputHelper.SetSurroundingText(_inputElement, surroundingText, selection.Start, selection.End);
- }
- else
- {
- HideIme();
- }
- }
-
- private void SurroundingTextChanged(object? sender, EventArgs e)
- {
- if (_client != null)
- {
- var surroundingText = _client.SurroundingText ?? "";
- var selection = _client.Selection;
-
- InputHelper.SetSurroundingText(_inputElement, surroundingText, selection.Start, selection.End);
- }
- }
-
- void ITextInputMethodImpl.SetCursorRect(Rect rect)
- {
- InputHelper.FocusElement(_inputElement);
- InputHelper.SetBounds(_inputElement, (int)rect.X, (int)rect.Y, (int)rect.Width, (int)rect.Height, _client?.Selection.End ?? 0);
- InputHelper.FocusElement(_inputElement);
- }
-
- void ITextInputMethodImpl.SetOptions(TextInputOptions options)
- {
- }
-
- void ITextInputMethodImpl.Reset()
- {
- InputHelper.ClearInputElement(_inputElement);
- InputHelper.SetSurroundingText(_inputElement, "", 0, 0);
- }
}
}
diff --git a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs
index 9c41fff43f..950d07f207 100644
--- a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs
+++ b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs
@@ -1,12 +1,30 @@
using System;
+using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Browser.Interop;
using Avalonia.Metadata;
namespace Avalonia.Browser;
-public class BrowserPlatformOptions
+public enum BrowserRenderingMode
+{
+ Software2D = 1,
+ WebGL1,
+ WebGL2
+}
+
+public record BrowserPlatformOptions
{
+ ///
+ /// Gets or sets Avalonia rendering modes with fallbacks.
+ /// The first element in the array has the highest priority.
+ ///
+ /// Thrown if no values were matched.
+ public IReadOnlyList RenderingMode { get; set; } = new[]
+ {
+ BrowserRenderingMode.WebGL2, BrowserRenderingMode.WebGL1, BrowserRenderingMode.Software2D
+ };
+
///
/// Defines paths where avalonia modules and service locator should be resolved.
/// If null, default path resolved depending on the backend (browser or blazor) is used.
@@ -26,7 +44,7 @@ public class BrowserPlatformOptions
/// By default, current domain root is used as a scope.
///
public string? AvaloniaServiceWorkerScope { get; set; }
-
+
///
/// Avalonia uses "native-file-system-adapter" polyfill for the file dialogs.
/// If native implementation is available, by default it is used.
@@ -44,13 +62,14 @@ public static class BrowserAppBuilder
/// Application builder.
/// ID of the html element where avalonia content should be rendered.
/// Browser backend specific options.
- public static async Task StartBrowserAppAsync(this AppBuilder builder, string mainDivId, BrowserPlatformOptions? options = null)
+ public static async Task StartBrowserAppAsync(this AppBuilder builder, string mainDivId,
+ BrowserPlatformOptions? options = null)
{
if (mainDivId is null)
{
throw new ArgumentNullException(nameof(mainDivId));
}
-
+
builder = await PreSetupBrowser(builder, options);
var lifetime = new BrowserSingleViewLifetime();
@@ -86,7 +105,7 @@ public static class BrowserAppBuilder
options.FrameworkAssetPathResolver ??= fileName => $"./{fileName}";
AvaloniaLocator.CurrentMutable.Bind().ToConstant(options);
-
+
await AvaloniaModule.ImportMain();
if (builder.WindowingSubsystemInitializer is null)
@@ -96,7 +115,7 @@ public static class BrowserAppBuilder
return builder;
}
-
+
public static AppBuilder UseBrowser(
this AppBuilder builder)
{
diff --git a/src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs b/src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs
index 6ee6c719a7..d7eaa81abf 100644
--- a/src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs
+++ b/src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs
@@ -44,7 +44,7 @@ internal class BrowserDispatcherImpl : IDispatcherImpl
// NOTE: by HTML5 spec minimal timeout is 4ms, but Chrome seems to work well with 1ms as well.
var interval = 1;
- CanvasHelper.SetTimeout(_signalCallback, interval);
+ TimerHelper.SetTimeout(_signalCallback, interval);
}
public void UpdateTimer(long? dueTimeInMs)
@@ -52,13 +52,13 @@ internal class BrowserDispatcherImpl : IDispatcherImpl
if (_timerId is { } timerId)
{
_timerId = null;
- CanvasHelper.ClearInterval(timerId);
+ TimerHelper.ClearInterval(timerId);
}
if (dueTimeInMs.HasValue)
{
var interval = Math.Max(1, dueTimeInMs.Value - _clock.ElapsedMilliseconds);
- _timerId = CanvasHelper.SetInterval(_timerCallback, (int)interval);
+ _timerId = TimerHelper.SetInterval(_timerCallback, (int)interval);
}
}
}
diff --git a/src/Browser/Avalonia.Browser/BrowserInputHandler.cs b/src/Browser/Avalonia.Browser/BrowserInputHandler.cs
new file mode 100644
index 0000000000..edd66b5a06
--- /dev/null
+++ b/src/Browser/Avalonia.Browser/BrowserInputHandler.cs
@@ -0,0 +1,383 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.InteropServices.JavaScript;
+using Avalonia.Browser.Interop;
+using Avalonia.Collections.Pooled;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+
+namespace Avalonia.Browser;
+
+internal class BrowserInputHandler
+{
+ private readonly BrowserTopLevelImpl _topLevelImpl;
+ private readonly JSObject _container;
+ private readonly Stopwatch _sw = Stopwatch.StartNew();
+ private readonly TouchDevice _touchDevice;
+ private readonly PenDevice _penDevice;
+ private readonly MouseDevice _wheelMouseDevice;
+ private readonly List _mouseDevices;
+ private IInputRoot? _inputRoot;
+
+ private static readonly PooledList s_intermediatePointsPooledList = new(ClearMode.Never);
+
+ public BrowserInputHandler(BrowserTopLevelImpl topLevelImpl, JSObject container)
+ {
+ _topLevelImpl = topLevelImpl;
+ _container = container ?? throw new ArgumentNullException(nameof(container));
+
+ _touchDevice = new TouchDevice();
+ _penDevice = new PenDevice();
+ _wheelMouseDevice = new MouseDevice();
+ _mouseDevices = new();
+
+ InputHelper.SubscribeKeyEvents(
+ container,
+ OnKeyDown,
+ OnKeyUp);
+ InputHelper.SubscribePointerEvents(container, OnPointerMove, OnPointerDown, OnPointerUp,
+ OnPointerCancel, OnWheel);
+ InputHelper.SubscribeDropEvents(container, OnDragEvent);
+ }
+
+ public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds;
+
+ internal void SetInputRoot(IInputRoot inputRoot)
+ {
+ _inputRoot = inputRoot;
+ }
+
+ 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 pointerType = args.GetPropertyAsString("pointerType");
+ var point = ExtractRawPointerFromJsArgs(args);
+ var type = pointerType switch
+ {
+ "touch" => RawPointerEventType.TouchUpdate,
+ _ => RawPointerEventType.Move
+ };
+
+ var coalescedEvents = new Lazy?>(() =>
+ {
+ var points = InputHelper.GetCoalescedEvents(args);
+ s_intermediatePointsPooledList.Clear();
+ s_intermediatePointsPooledList.Capacity = points.Length - 1;
+
+ // Skip the last one, as it is already processed point.
+ for (var i = 0; i < points.Length - 1; i++)
+ {
+ var point = points[i];
+ s_intermediatePointsPooledList.Add(ExtractRawPointerFromJsArgs(point));
+ }
+
+ return s_intermediatePointsPooledList;
+ });
+
+ return RawPointerEvent(type, pointerType!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"),
+ coalescedEvents);
+ }
+
+ private bool OnPointerDown(JSObject args)
+ {
+ var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse";
+ 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 => RawPointerEventType.XButton1Down, // should be pen eraser button,
+ _ => RawPointerEventType.Move
+ }
+ };
+
+ var point = ExtractRawPointerFromJsArgs(args);
+ return 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 => RawPointerEventType.XButton1Up, // should be pen eraser button,
+ _ => RawPointerEventType.Move
+ }
+ };
+
+ var point = ExtractRawPointerFromJsArgs(args);
+ return RawPointerEvent(type, pointerType, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"));
+ }
+
+ private bool OnPointerCancel(JSObject args)
+ {
+ var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse";
+ if (pointerType == "touch")
+ {
+ var point = ExtractRawPointerFromJsArgs(args);
+ RawPointerEvent(RawPointerEventType.TouchCancel, pointerType, point,
+ GetModifiers(args), args.GetPropertyAsInt32("pointerId"));
+ }
+
+ return false;
+ }
+
+ private bool OnWheel(JSObject args)
+ {
+ return RawMouseWheelEvent(new Point(args.GetPropertyAsDouble("offsetX"), args.GetPropertyAsDouble("offsetY")),
+ 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;
+ }
+
+ public bool OnDragEvent(JSObject args)
+ {
+ var eventType = args?.GetPropertyAsString("type") switch
+ {
+ "dragenter" => RawDragEventType.DragEnter,
+ "dragover" => RawDragEventType.DragOver,
+ "dragleave" => RawDragEventType.DragLeave,
+ "drop" => RawDragEventType.Drop,
+ _ => (RawDragEventType)(int)-1
+ };
+ var dataObject = args?.GetPropertyAsJSObject("dataTransfer");
+ if (args is null || eventType < 0 || dataObject is null)
+ {
+ return false;
+ }
+
+ // If file is dropped, we need storage js to be referenced.
+ // TODO: restructure JS files, so it's not needed.
+ _ = AvaloniaModule.ImportStorage();
+
+ var position = new Point(args.GetPropertyAsDouble("offsetX"), args.GetPropertyAsDouble("offsetY"));
+ var modifiers = GetModifiers(args);
+
+ var effectAllowedStr = dataObject.GetPropertyAsString("effectAllowed") ?? "none";
+ var effectAllowed = DragDropEffects.None;
+ if (effectAllowedStr.Contains("copy", StringComparison.OrdinalIgnoreCase))
+ {
+ effectAllowed |= DragDropEffects.Copy;
+ }
+
+ if (effectAllowedStr.Contains("link", StringComparison.OrdinalIgnoreCase))
+ {
+ effectAllowed |= DragDropEffects.Link;
+ }
+
+ if (effectAllowedStr.Contains("move", StringComparison.OrdinalIgnoreCase))
+ {
+ effectAllowed |= DragDropEffects.Move;
+ }
+
+ if (effectAllowedStr.Equals("all", StringComparison.OrdinalIgnoreCase))
+ {
+ effectAllowed |= DragDropEffects.Move | DragDropEffects.Copy | DragDropEffects.Link;
+ }
+
+ if (effectAllowed == DragDropEffects.None)
+ {
+ return false;
+ }
+
+ var dropEffect = RawDragEvent(eventType, position, modifiers, new BrowserDataObject(dataObject), effectAllowed);
+ dataObject.SetProperty("dropEffect", dropEffect.ToString().ToLowerInvariant());
+
+ return eventType is RawDragEventType.Drop or RawDragEventType.DragOver
+ && dropEffect != DragDropEffects.None;
+ }
+
+ private bool OnKeyDown(string code, string key, int modifier)
+ {
+ var handled = RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)modifier);
+
+ if (!handled && key.Length == 1)
+ {
+ handled = RawTextEvent(key);
+ }
+
+ return handled;
+ }
+
+ private bool OnKeyUp(string code, string key, int modifier)
+ {
+ return RawKeyboardEvent(RawKeyEventType.KeyUp, code, key, (RawInputModifiers)modifier);
+ }
+
+ private bool RawPointerEvent(
+ RawPointerEventType eventType, string pointerType,
+ RawPointerPoint p, RawInputModifiers modifiers, long touchPointId,
+ Lazy?>? intermediatePoints = null)
+ {
+ if (_inputRoot is { }
+ && _topLevelImpl.Input is { } input)
+ {
+ var device = GetPointerDevice(pointerType, touchPointId);
+ var args = device is TouchDevice ?
+ new RawTouchEventArgs(device, Timestamp, _inputRoot, eventType, p, modifiers, touchPointId)
+ {
+ IntermediatePoints = intermediatePoints
+ } :
+ new RawPointerEventArgs(device, Timestamp, _inputRoot, eventType, p, modifiers)
+ {
+ RawPointerId = touchPointId, IntermediatePoints = intermediatePoints
+ };
+
+ input.Invoke(args);
+
+ return args.Handled;
+ }
+
+ return false;
+ }
+
+ private IPointerDevice GetPointerDevice(string pointerType, long pointerId)
+ {
+ if (pointerType == "touch")
+ return _touchDevice;
+ else if (pointerType == "pen")
+ return _penDevice;
+
+ // TODO: refactor pointer devices, so we can reuse single instance here.
+ foreach (var mouseDevice in _mouseDevices)
+ {
+ if (mouseDevice.PointerId == pointerId)
+ return mouseDevice;
+ }
+
+ var newMouseDevice = new BrowserMouseDevice(pointerId, _container);
+ _mouseDevices.Add(newMouseDevice);
+ return newMouseDevice;
+ }
+
+ private bool RawMouseWheelEvent(Point p, Vector v, RawInputModifiers modifiers)
+ {
+ if (_inputRoot is { })
+ {
+ var args = new RawMouseWheelEventArgs(_wheelMouseDevice, Timestamp, _inputRoot, p, v, modifiers);
+
+ _topLevelImpl.Input?.Invoke(args);
+
+ return args.Handled;
+ }
+
+ return false;
+ }
+
+ private bool RawKeyboardEvent(RawKeyEventType type, string domCode, string domKey, RawInputModifiers modifiers)
+ {
+ if (_inputRoot is null)
+ return false;
+
+ var physicalKey = KeyInterop.PhysicalKeyFromDomCode(domCode);
+ var key = KeyInterop.KeyFromDomKey(domKey, physicalKey);
+ var keySymbol = KeyInterop.KeySymbolFromDomKey(domKey);
+
+ var args = new RawKeyEventArgs(
+ BrowserWindowingPlatform.Keyboard,
+ Timestamp,
+ _inputRoot,
+ type,
+ key,
+ modifiers,
+ physicalKey,
+ keySymbol
+ );
+
+ try
+ {
+ _topLevelImpl.Input?.Invoke(args);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex);
+ }
+
+ return args.Handled;
+ }
+
+ internal bool RawTextEvent(string text)
+ {
+ if (_inputRoot is { })
+ {
+ var args = new RawTextInputEventArgs(BrowserWindowingPlatform.Keyboard, Timestamp, _inputRoot, text);
+ _topLevelImpl.Input?.Invoke(args);
+
+ return args.Handled;
+ }
+
+ return false;
+ }
+
+ private DragDropEffects RawDragEvent(RawDragEventType eventType, Point position, RawInputModifiers modifiers,
+ BrowserDataObject dataObject, DragDropEffects dropEffect)
+ {
+ var device = AvaloniaLocator.Current.GetRequiredService();
+ var eventArgs = new RawDragEvent(device, eventType, _inputRoot!, position, dataObject, dropEffect, modifiers);
+ _topLevelImpl.Input?.Invoke(eventArgs);
+ return eventArgs.Effects;
+ }
+}
diff --git a/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs
index dac5a3f14a..2c8eb03c72 100644
--- a/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs
+++ b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs
@@ -1,17 +1,16 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Avalonia.Browser.Interop;
+using Avalonia.Browser.Interop;
using Avalonia.Controls.Platform;
using Avalonia.Media;
-using static Avalonia.Controls.Platform.IInsetsManager;
namespace Avalonia.Browser
{
internal class BrowserInsetsManager : InsetsManagerBase
{
+ public BrowserInsetsManager()
+ {
+ DomHelper.InitSafeAreaPadding();
+ }
+
public override bool? IsSystemBarVisible
{
get
diff --git a/src/Browser/Avalonia.Browser/BrowserMouseDevice.cs b/src/Browser/Avalonia.Browser/BrowserMouseDevice.cs
index c8c23b6000..9835e3a15b 100644
--- a/src/Browser/Avalonia.Browser/BrowserMouseDevice.cs
+++ b/src/Browser/Avalonia.Browser/BrowserMouseDevice.cs
@@ -1,6 +1,6 @@
using System.Runtime.InteropServices.JavaScript;
-using Avalonia.Input;
using Avalonia.Browser.Interop;
+using Avalonia.Input;
namespace Avalonia.Browser
{
diff --git a/src/Browser/Avalonia.Browser/BrowserNativeControlHost.cs b/src/Browser/Avalonia.Browser/BrowserNativeControlHost.cs
index c2e54c7ed7..75a5444ab2 100644
--- a/src/Browser/Avalonia.Browser/BrowserNativeControlHost.cs
+++ b/src/Browser/Avalonia.Browser/BrowserNativeControlHost.cs
@@ -11,9 +11,9 @@ namespace Avalonia.Browser
{
private readonly JSObject _hostElement;
- public BrowserNativeControlHost(JSObject element)
+ public BrowserNativeControlHost(JSObject nativeControlHost)
{
- _hostElement = element;
+ _hostElement = nativeControlHost ?? throw new ArgumentNullException(nameof(nativeControlHost));
}
public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent)
diff --git a/src/Browser/Avalonia.Browser/BrowserRuntimePlatform.cs b/src/Browser/Avalonia.Browser/BrowserRuntimePlatform.cs
index 8709fd79fd..ca3eb7bd94 100644
--- a/src/Browser/Avalonia.Browser/BrowserRuntimePlatform.cs
+++ b/src/Browser/Avalonia.Browser/BrowserRuntimePlatform.cs
@@ -1,8 +1,5 @@
using System;
using System.Reflection;
-using System.Runtime.InteropServices;
-using System.Runtime.InteropServices.JavaScript;
-using System.Text.RegularExpressions;
using Avalonia.Browser.Interop;
using Avalonia.Platform;
diff --git a/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs b/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs
index a28bb469f3..d352b92c0d 100644
--- a/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs
+++ b/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs
@@ -1,8 +1,8 @@
using System;
using System.Diagnostics.CodeAnalysis;
+using Avalonia.Browser;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
-using Avalonia.Browser;
namespace Avalonia;
@@ -29,7 +29,8 @@ internal class BrowserSingleViewLifetime : ISingleViewApplicationLifetime, ISing
{
if (View is null)
{
- throw new InvalidOperationException("Browser lifetime was not initialized. Make sure AppBuilder.StartBrowserApp was called.");
+ throw new InvalidOperationException(
+ "Browser lifetime was not initialized. Make sure AppBuilder.StartBrowserAppAsync was called.");
}
}
diff --git a/src/Browser/Avalonia.Browser/BrowserTextInputMethod.cs b/src/Browser/Avalonia.Browser/BrowserTextInputMethod.cs
new file mode 100644
index 0000000000..8cb4296c48
--- /dev/null
+++ b/src/Browser/Avalonia.Browser/BrowserTextInputMethod.cs
@@ -0,0 +1,162 @@
+using System;
+using System.Runtime.InteropServices.JavaScript;
+using Avalonia.Browser.Interop;
+using Avalonia.Input.TextInput;
+
+namespace Avalonia.Browser;
+
+internal class BrowserTextInputMethod : ITextInputMethodImpl
+{
+ private readonly JSObject _inputElement;
+ private readonly JSObject _containerElement;
+ private readonly BrowserInputHandler _inputHandler;
+ private TextInputMethodClient? _client;
+
+ public BrowserTextInputMethod(BrowserInputHandler inputHandler, JSObject containerElement, JSObject inputElement)
+ {
+ _inputHandler = inputHandler ?? throw new ArgumentNullException(nameof(inputHandler));
+ _containerElement = containerElement ?? throw new ArgumentNullException(nameof(containerElement));
+ _inputElement = inputElement ?? throw new ArgumentNullException(nameof(inputElement));
+
+ InputHelper.SubscribeTextEvents(
+ _inputElement,
+ OnBeforeInput,
+ OnCompositionStart,
+ OnCompositionUpdate,
+ OnCompositionEnd);
+ }
+
+ public bool IsComposing { get; private set; }
+
+ private void HideIme()
+ {
+ InputHelper.HideElement(_inputElement);
+ InputHelper.FocusElement(_containerElement);
+ }
+
+ public void SetClient(TextInputMethodClient? 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 ?? "";
+ var selection = _client.Selection;
+
+ InputHelper.SetSurroundingText(_inputElement, surroundingText, selection.Start, selection.End);
+ }
+ else
+ {
+ HideIme();
+ }
+ }
+
+ private void SurroundingTextChanged(object? sender, EventArgs e)
+ {
+ if (_client != null)
+ {
+ var surroundingText = _client.SurroundingText ?? "";
+ var selection = _client.Selection;
+
+ InputHelper.SetSurroundingText(_inputElement, surroundingText, selection.Start, selection.End);
+ }
+ }
+
+ public void SetCursorRect(Rect rect)
+ {
+ InputHelper.FocusElement(_inputElement);
+ InputHelper.SetBounds(_inputElement, (int)rect.X, (int)rect.Y, (int)rect.Width, (int)rect.Height,
+ _client?.Selection.End ?? 0);
+ InputHelper.FocusElement(_inputElement);
+ }
+
+ public void SetOptions(TextInputOptions options)
+ {
+ }
+
+ public void Reset()
+ {
+ InputHelper.ClearInputElement(_inputElement);
+ InputHelper.SetSurroundingText(_inputElement, "", 0, 0);
+ }
+
+ private bool OnBeforeInput(JSObject arg, int start, int end)
+ {
+ var type = arg.GetPropertyAsString("inputType");
+ if (type != "deleteByComposition")
+ {
+ if (type == "deleteContentBackward")
+ {
+ start = _inputElement.GetPropertyAsInt32("selectionStart");
+ end = _inputElement.GetPropertyAsInt32("selectionEnd");
+ }
+ else
+ {
+ start = -1;
+ end = -1;
+ }
+ }
+
+ if (start != -1 && end != -1 && _client != null)
+ {
+ _client.Selection = new TextSelection(start, end);
+ }
+
+ return false;
+ }
+
+ 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);
+
+ var text = args.GetPropertyAsString("data");
+
+ if (text != null)
+ {
+ return _inputHandler.RawTextEvent(text);
+ }
+
+ return false;
+ }
+}
diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs
index 629c42af9b..9e5742954b 100644
--- a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs
+++ b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs
@@ -1,9 +1,10 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;
+using Avalonia.Browser.Interop;
+using Avalonia.Browser.Rendering;
using Avalonia.Browser.Skia;
using Avalonia.Browser.Storage;
using Avalonia.Controls;
@@ -22,205 +23,86 @@ namespace Avalonia.Browser
{
internal class BrowserTopLevelImpl : ITopLevelImpl
{
- 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;
private readonly INativeControlHostImpl _nativeControlHost;
private readonly IStorageProvider _storageProvider;
private readonly ISystemNavigationManagerImpl _systemNavigationManager;
+ private readonly ITextInputMethodImpl _textInputMethodImpl;
private readonly ClipboardImpl _clipboard;
- private readonly IInsetsManager? _insetsManager;
+ private readonly IInsetsManager _insetsManager;
private readonly IInputPane _inputPane;
- private readonly List _mouseDevices;
private readonly JSObject _container;
+ private readonly BrowserInputHandler _inputHandler;
+ private string _currentCursor = CssCursor.Default;
+ private BrowserSurface? _surface;
+ static BrowserTopLevelImpl()
+ {
+ InputHelper.InitializeBackgroundHandlers();
+ }
- public BrowserTopLevelImpl(AvaloniaView avaloniaView, JSObject container)
+ public BrowserTopLevelImpl(JSObject container, JSObject nativeControlHost, JSObject inputElement)
{
Surfaces = Enumerable.Empty();
- _avaloniaView = avaloniaView;
AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1);
- _touchDevice = new TouchDevice();
- _penDevice = new PenDevice();
+ _inputHandler = new BrowserInputHandler(this, container);
+ _textInputMethodImpl = new BrowserTextInputMethod(_inputHandler, container, inputElement);
_insetsManager = new BrowserInsetsManager();
- _nativeControlHost = _avaloniaView.GetNativeControlHostImpl();
+ _nativeControlHost = new BrowserNativeControlHost(nativeControlHost);
_storageProvider = new BrowserStorageProvider();
_systemNavigationManager = new BrowserSystemNavigationManagerImpl();
_clipboard = new ClipboardImpl();
_inputPane = new BrowserInputPane(container);
- _mouseDevices = new();
_container = container;
- }
-
- 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);
- surface.DisplaySize = newSize;
- }
-
- Resized?.Invoke(newSize, WindowResizeReason.User);
-
- (_insetsManager as BrowserInsetsManager)?.NotifySafeAreaPaddingChanged();
- }
- }
-
- public bool RawPointerEvent(
- RawPointerEventType eventType, string pointerType,
- RawPointerPoint p, RawInputModifiers modifiers, long touchPointId,
- Lazy?>? intermediatePoints = null)
- {
- if (_inputRoot is { }
- && Input is { } input)
- {
- var device = GetPointerDevice(pointerType, touchPointId);
- var args = device is TouchDevice ?
- new RawTouchEventArgs(device, Timestamp, _inputRoot, eventType, p, modifiers, touchPointId)
- {
- IntermediatePoints = intermediatePoints
- } :
- new RawPointerEventArgs(device, Timestamp, _inputRoot, eventType, p, modifiers)
- {
- RawPointerId = touchPointId,
- IntermediatePoints = intermediatePoints
- };
-
- input.Invoke(args);
-
- return args.Handled;
- }
-
- return false;
- }
- private IPointerDevice GetPointerDevice(string pointerType, long pointerId)
- {
- if (pointerType == "touch")
- return _touchDevice;
- else if (pointerType == "pen")
- return _penDevice;
+ _surface = BrowserSurface.Create(container, PixelFormats.Rgba8888);
+ _surface.SizeChanged += OnSizeChanged;
+ _surface.ScalingChanged += OnScalingChanged;
- foreach (var mouseDevice in _mouseDevices)
- {
- if (mouseDevice.PointerId == pointerId)
- return mouseDevice;
- }
- var newMouseDevice = new BrowserMouseDevice(pointerId, _container);
- _mouseDevices.Add(newMouseDevice);
- return newMouseDevice;
+ Surfaces = new[] { _surface };
+ Compositor = _surface.IsWebGl ?
+ BrowserCompositor.WebGlUiCompositor :
+ BrowserCompositor.SoftwareUiCompositor;
}
- public bool RawMouseWheelEvent(Point p, Vector v, RawInputModifiers modifiers)
+ private void OnScalingChanged()
{
- if (_inputRoot is { })
+ if (_surface is not null)
{
- var args = new RawMouseWheelEventArgs(WheelMouseDevice, Timestamp, _inputRoot, p, v, modifiers);
-
- Input?.Invoke(args);
-
- return args.Handled;
+ ScalingChanged?.Invoke(_surface.Scaling);
}
-
- return false;
}
- public bool RawKeyboardEvent(RawKeyEventType type, string domCode, string domKey, RawInputModifiers modifiers)
+ private void OnSizeChanged()
{
- if (_inputRoot is null)
- return false;
-
- var physicalKey = KeyInterop.PhysicalKeyFromDomCode(domCode);
- var key = KeyInterop.KeyFromDomKey(domKey, physicalKey);
- var keySymbol = KeyInterop.KeySymbolFromDomKey(domKey);
-
- var args = new RawKeyEventArgs(
- KeyboardDevice,
- Timestamp,
- _inputRoot,
- type,
- key,
- modifiers,
- physicalKey,
- keySymbol
- );
-
- Input?.Invoke(args);
-
- return args.Handled;
- }
-
- public bool RawTextEvent(string text)
- {
- if (_inputRoot is { })
+ if (_surface is not null)
{
- var args = new RawTextInputEventArgs(KeyboardDevice, Timestamp, _inputRoot, text);
- Input?.Invoke(args);
-
- return args.Handled;
+ Resized?.Invoke(_surface.ClientSize, WindowResizeReason.User);
+ (_insetsManager as BrowserInsetsManager)?.NotifySafeAreaPaddingChanged();
}
-
- return false;
- }
-
- public DragDropEffects RawDragEvent(RawDragEventType eventType, Point position, RawInputModifiers modifiers, BrowserDataObject dataObject, DragDropEffects dropEffect)
- {
- var device = AvaloniaLocator.Current.GetRequiredService();
- var eventArgs = new RawDragEvent(device, eventType, _inputRoot!, position, dataObject, dropEffect, modifiers);
- Console.WriteLine($"{eventArgs.Location} {eventArgs.Effects} {eventArgs.Type} {eventArgs.KeyModifiers}");
- Input?.Invoke(eventArgs);
- return eventArgs.Effects;
}
public void Dispose()
{
-
+ _surface?.Dispose();
+ _surface = null;
}
- public Compositor Compositor { get; } = new(AvaloniaLocator.Current.GetRequiredService());
+ public Compositor Compositor { get; }
- public void Invalidate(Rect rect)
- {
- //Console.WriteLine("invalidate rect called");
- }
+ public void SetInputRoot(IInputRoot inputRoot) => _inputHandler.SetInputRoot(inputRoot);
- public void SetInputRoot(IInputRoot inputRoot)
- {
- _inputRoot = inputRoot;
- }
-
- public Point PointToClient(PixelPoint point) => new Point(point.X, point.Y);
+ public Point PointToClient(PixelPoint point) => new(point.X, point.Y);
- public PixelPoint PointToScreen(Point point) => new PixelPoint((int)point.X, (int)point.Y);
+ public PixelPoint PointToScreen(Point point) => new((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);
+ InputHelper.SetCursor(_container, val);
_currentCursor = val;
}
}
@@ -234,13 +116,12 @@ namespace Avalonia.Browser
{
}
- public Size ClientSize => _clientSize;
+ public Size ClientSize => _surface?.ClientSize ?? new Size(1, 1);
public Size? FrameSize => null;
- public double RenderScaling => (Surfaces.FirstOrDefault() as BrowserSkiaSurface)?.Scaling ?? 1;
+ public double RenderScaling => _surface?.Scaling ?? 1;
public IEnumerable Surfaces { get; set; }
- public Action? SetCssCursor { get; set; }
public Action? Input { get; set; }
public Action? Paint { get; set; }
public Action? Resized { get; set; }
@@ -248,10 +129,8 @@ namespace Avalonia.Browser
public Action? TransparencyLevelChanged { get; set; }
public Action? Closed { get; set; }
public Action? LostFocus { get; set; }
- public IMouseDevice WheelMouseDevice { get; } = new MouseDevice();
-
- public IKeyboardDevice KeyboardDevice { get; } = BrowserWindowingPlatform.Keyboard;
public WindowTransparencyLevel TransparencyLevel => WindowTransparencyLevel.None;
+
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant)
{
// not in the standard, but we potentially can use "apple-mobile-web-app-status-bar-style" for iOS and "theme-color" for android.
@@ -268,7 +147,7 @@ namespace Avalonia.Browser
if (featureType == typeof(ITextInputMethodImpl))
{
- return _avaloniaView;
+ return _textInputMethodImpl;
}
if (featureType == typeof(ISystemNavigationManagerImpl))
@@ -290,7 +169,7 @@ namespace Avalonia.Browser
{
return _clipboard;
}
-
+
if (featureType == typeof(IInputPane))
{
return _inputPane;
diff --git a/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs b/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs
index cdcda6a320..e725c2997a 100644
--- a/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs
+++ b/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs
@@ -1,4 +1,7 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.JavaScript;
@@ -8,41 +11,42 @@ internal record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int
internal static partial class CanvasHelper
{
- public static GLInfo InitialiseGL(JSObject canvas, Action renderFrameCallback)
+ public static (JSObject CanvasView, GLInfo? GLInfo) CreateSurface(
+ JSObject container, BrowserRenderingMode renderingMode)
{
- 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;
+ var isGlMode = renderingMode is BrowserRenderingMode.WebGL1 or BrowserRenderingMode.WebGL2;
+
+ var canvasView = Create(container, (int)renderingMode);
+
+ GLInfo? glInfo = null;
+ if (isGlMode)
+ {
+ glInfo = new GLInfo(
+ canvasView.GetPropertyAsInt32("contextHandle")!,
+ (uint)canvasView.GetPropertyAsInt32("fboId"),
+ canvasView.GetPropertyAsInt32("stencil"),
+ canvasView.GetPropertyAsInt32("sample"),
+ canvasView.GetPropertyAsInt32("depth"));
+ }
+
+ return (canvasView, glInfo);
}
- [JSImport("Canvas.requestAnimationFrame", AvaloniaModule.MainModuleName)]
- public static partial void RequestAnimationFrame(JSObject canvas, bool renderLoop);
-
- [JSImport("Canvas.setCanvasSize", AvaloniaModule.MainModuleName)]
- public static partial void SetCanvasSize(JSObject canvas, int width, int height);
-
- [JSImport("Canvas.initGL", AvaloniaModule.MainModuleName)]
- private static partial JSObject InitGL(
- JSObject canvas,
- string canvasId,
- [JSMarshalAs] Action renderFrameCallback);
+ [JSImport("CanvasFactory.onSizeChanged", AvaloniaModule.MainModuleName)]
+ public static partial void OnSizeChanged(
+ JSObject canvasSurface,
+ [JSMarshalAs>]
+ Action onSizeChanged);
- [JSImport("globalThis.setTimeout")]
- public static partial int SetTimeout([JSMarshalAs] Action callback, int intervalMs);
+ [JSImport("CanvasFactory.create", AvaloniaModule.MainModuleName)]
+ private static partial JSObject Create(JSObject canvasSurface, int mode);
- [JSImport("globalThis.clearTimeout")]
- public static partial int ClearTimeout(int id);
+ [JSImport("CanvasFactory.destroy", AvaloniaModule.MainModuleName)]
+ public static partial void Destroy(JSObject canvasSurface);
- [JSImport("globalThis.setInterval")]
- public static partial int SetInterval([JSMarshalAs] Action callback, int intervalMs);
+ [JSImport("CanvasFactory.ensureSize", AvaloniaModule.MainModuleName)]
+ public static partial void EnsureSize(JSObject canvasSurface);
- [JSImport("globalThis.clearInterval")]
- public static partial int ClearInterval(int id);
+ [JSImport("CanvasFactory.putPixelData", AvaloniaModule.MainModuleName)]
+ public static partial void PutPixelData(JSObject canvasSurface, [JSMarshalAs] ArraySegment data, int width, int height);
}
diff --git a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs
index 9cd7261994..a567973131 100644
--- a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs
+++ b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs
@@ -8,6 +8,9 @@ internal static partial class DomHelper
[JSImport("globalThis.document.getElementById")]
internal static partial JSObject? GetElementById(string id);
+ [JSImport("AvaloniaDOM.getFirstElementByClassName", AvaloniaModule.MainModuleName)]
+ internal static partial JSObject? GetElementsByClassName(string className, JSObject? parent);
+
[JSImport("AvaloniaDOM.createAvaloniaHost", AvaloniaModule.MainModuleName)]
public static partial JSObject CreateAvaloniaHost(JSObject element);
@@ -18,17 +21,14 @@ internal static partial class DomHelper
public static partial JSObject SetFullscreen(bool isFullscreen);
[JSImport("AvaloniaDOM.getSafeAreaPadding", AvaloniaModule.MainModuleName)]
- public static partial byte[] GetSafeAreaPadding();
+ public static partial double[] GetSafeAreaPadding();
+
+ [JSImport("AvaloniaDOM.initSafeAreaPadding", AvaloniaModule.MainModuleName)]
+ public static partial void InitSafeAreaPadding();
[JSImport("AvaloniaDOM.addClass", AvaloniaModule.MainModuleName)]
public static partial void AddCssClass(JSObject element, string className);
- [JSImport("ResizeHandler.observeSize", AvaloniaModule.MainModuleName)]
- public static partial void ObserveSize(
- JSObject canvas,
- [JSMarshalAs>]
- Action onSizeOrDpiChanged);
-
[JSImport("AvaloniaDOM.observeDarkMode", AvaloniaModule.MainModuleName)]
public static partial JSObject ObserveDarkMode(
[JSMarshalAs>]
diff --git a/src/Browser/Avalonia.Browser/Interop/TimerHelper.cs b/src/Browser/Avalonia.Browser/Interop/TimerHelper.cs
new file mode 100644
index 0000000000..32c2f66565
--- /dev/null
+++ b/src/Browser/Avalonia.Browser/Interop/TimerHelper.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Runtime.InteropServices.JavaScript;
+
+namespace Avalonia.Browser.Interop;
+
+internal static partial class TimerHelper
+{
+ [JSImport("TimerHelper.runAnimationFrames", AvaloniaModule.MainModuleName)]
+ public static partial void RunAnimationFrames(
+ [JSMarshalAs>] Func renderFrameCallback);
+
+ [JSImport("globalThis.setTimeout")]
+ public static partial int SetTimeout([JSMarshalAs] Action callback, int intervalMs);
+
+ [JSImport("globalThis.clearTimeout")]
+ public static partial int ClearTimeout(int id);
+
+ [JSImport("globalThis.setInterval")]
+ public static partial int SetInterval([JSMarshalAs] Action callback, int intervalMs);
+
+ [JSImport("globalThis.clearInterval")]
+ public static partial int ClearInterval(int id);
+}
diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserCompositor.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserCompositor.cs
new file mode 100644
index 0000000000..a16b659a6e
--- /dev/null
+++ b/src/Browser/Avalonia.Browser/Rendering/BrowserCompositor.cs
@@ -0,0 +1,24 @@
+using Avalonia.Platform;
+using Avalonia.Rendering;
+using Avalonia.Rendering.Composition;
+
+namespace Avalonia.Browser.Rendering;
+
+///
+/// We want to reuse timer/compositor instances per each AvaloniaView.
+/// But at the same time, we want to keep possiblity of having different rendering modes (both software and webgl) at the same time.
+/// For example, WebGL contexts number might exceed maximum allowed, or we might want to keep popups in software renderer.
+///
+internal static class BrowserCompositor
+{
+ private static BrowserRenderTimer? s_browserUiRenderTimer;
+ private static BrowserRenderTimer BrowserUiRenderTimer => s_browserUiRenderTimer ??= new BrowserRenderTimer(false);
+
+ private static Compositor? s_webGlUiCompositor, s_softwareUiCompositor;
+
+ internal static Compositor WebGlUiCompositor => s_webGlUiCompositor ??= new Compositor(
+ new RenderLoop(BrowserUiRenderTimer), AvaloniaLocator.Current.GetRequiredService());
+
+ internal static Compositor SoftwareUiCompositor => s_softwareUiCompositor ??= new Compositor(
+ new RenderLoop(BrowserUiRenderTimer), null);
+}
diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserGlSurface.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserGlSurface.cs
new file mode 100644
index 0000000000..5cb65bafd8
--- /dev/null
+++ b/src/Browser/Avalonia.Browser/Rendering/BrowserGlSurface.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Runtime.InteropServices.JavaScript;
+using Avalonia.Browser.Interop;
+using Avalonia.Browser.Skia;
+using Avalonia.Platform;
+using SkiaSharp;
+
+namespace Avalonia.Browser.Rendering;
+
+internal sealed class BrowserGlSurface : BrowserSurface
+{
+ private readonly GRGlInterface _glInterface;
+
+ public BrowserGlSurface(JSObject canvasSurface, GLInfo glInfo, PixelFormat pixelFormat,
+ BrowserRenderingMode renderingMode)
+ : base(canvasSurface, renderingMode)
+ {
+ var skiaOptions = AvaloniaLocator.Current.GetService();
+ _glInterface = GRGlInterface.Create() ?? throw new InvalidOperationException("Unable to create GRGlInterface.");
+ Context = GRContext.CreateGl(_glInterface) ??
+ throw new InvalidOperationException("Unable to create GRContext.");
+ if (skiaOptions?.MaxGpuResourceSizeBytes is { } resourceSizeBytes)
+ {
+ Context.SetResourceCacheLimit(resourceSizeBytes);
+ }
+
+ GlInfo = glInfo ?? throw new ArgumentNullException(nameof(glInfo));
+ PixelFormat = pixelFormat;
+ }
+
+ public PixelFormat PixelFormat { get; }
+
+ public GRContext Context { get; private set; }
+
+ public GLInfo GlInfo { get; }
+
+ public override void Dispose()
+ {
+ base.Dispose();
+
+ Context.Dispose();
+ Context = null!;
+
+ _glInterface.Dispose();
+ }
+
+ public void EnsureResize()
+ {
+ CanvasHelper.EnsureSize(JsSurface);
+ }
+}
diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserRasterSurface.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserRasterSurface.cs
new file mode 100644
index 0000000000..725d6241ac
--- /dev/null
+++ b/src/Browser/Avalonia.Browser/Rendering/BrowserRasterSurface.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Buffers;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.JavaScript;
+using Avalonia.Browser.Interop;
+using Avalonia.Controls.Platform.Surfaces;
+using Avalonia.Platform;
+
+namespace Avalonia.Browser.Skia;
+
+internal sealed class BrowserRasterSurface : BrowserSurface, IFramebufferPlatformSurface
+{
+ public PixelFormat PixelFormat { get; set; }
+
+ private FramebufferData? _fbData;
+ private readonly Action _onDisposeAction;
+ private readonly int _bytesPerPixel;
+
+ public BrowserRasterSurface(JSObject canvasSurface, PixelFormat pixelFormat, BrowserRenderingMode renderingMode)
+ : base(canvasSurface, renderingMode)
+ {
+ PixelFormat = pixelFormat;
+ _onDisposeAction = Blit;
+ _bytesPerPixel = pixelFormat.BitsPerPixel / 8;
+ }
+
+ public override void Dispose()
+ {
+ _fbData?.Dispose();
+ _fbData = null;
+
+ base.Dispose();
+ }
+
+ public ILockedFramebuffer Lock()
+ {
+ var bytesPerPixel = _bytesPerPixel;
+ var dpi = Scaling * 96.0;
+ var size = RenderSize;
+
+ if (_fbData is null || _fbData?.Size != size)
+ {
+ _fbData?.Dispose();
+ _fbData = new FramebufferData(size.Width, size.Height, bytesPerPixel);
+ }
+
+ var data = _fbData;
+ return new LockedFramebuffer(
+ data.Address, data.Size, data.RowBytes,
+ new Vector(dpi, dpi), PixelFormat, _onDisposeAction);
+ }
+
+ private void Blit()
+ {
+ if (_fbData is { } data)
+ {
+ CanvasHelper.PutPixelData(JsSurface, data.AsSegment, data.Size.Width, data.Size.Height);
+ }
+ }
+
+ private class FramebufferData
+ {
+ private static ArrayPool s_pool = ArrayPool.Create();
+
+ private readonly byte[] _array;
+ private GCHandle _handle;
+
+ public FramebufferData(int width, int height, int bytesPerPixel)
+ {
+ Size = new PixelSize(width, height);
+ RowBytes = width * bytesPerPixel;
+
+ var length = width * height * bytesPerPixel;
+ _array = s_pool.Rent(length);
+
+ _handle = GCHandle.Alloc(_array, GCHandleType.Pinned);
+ Address = _handle.AddrOfPinnedObject();
+
+ AsSegment = new ArraySegment(_array, 0, length);
+ }
+
+ public PixelSize Size { get; }
+
+ public int RowBytes { get; }
+
+ public IntPtr Address { get; }
+
+ public ArraySegment AsSegment { get; }
+
+ public void Dispose()
+ {
+ _handle.Free();
+ s_pool.Return(_array);
+ }
+ }
+
+ public IFramebufferRenderTarget CreateFramebufferRenderTarget() => new FuncFramebufferRenderTarget(Lock);
+}
diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs
new file mode 100644
index 0000000000..15f74e6585
--- /dev/null
+++ b/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Diagnostics;
+using Avalonia.Browser.Interop;
+using Avalonia.Rendering;
+
+namespace Avalonia.Browser.Rendering;
+
+internal class BrowserRenderTimer : IRenderTimer
+{
+ private Action? _tick;
+
+ public BrowserRenderTimer(bool isBackground)
+ {
+ RunsInBackground = isBackground;
+ }
+
+ public bool RunsInBackground { get; }
+
+ public event Action? Tick
+ {
+ add
+ {
+ if (_tick is null)
+ {
+ TimerHelper.RunAnimationFrames(RenderFrameCallback);
+ }
+
+ _tick += value;
+ }
+ remove
+ {
+ _tick -= value;
+ }
+ }
+
+ private bool RenderFrameCallback(double timestamp)
+ {
+ if (_tick is { } tick)
+ {
+ tick.Invoke(TimeSpan.FromMilliseconds(timestamp));
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserSurface.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserSurface.cs
new file mode 100644
index 0000000000..aa2b4cf014
--- /dev/null
+++ b/src/Browser/Avalonia.Browser/Rendering/BrowserSurface.cs
@@ -0,0 +1,102 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.JavaScript;
+using Avalonia.Browser.Interop;
+using Avalonia.Browser.Rendering;
+using Avalonia.Logging;
+using Avalonia.Platform;
+using Avalonia.Rendering;
+using Avalonia.Threading;
+
+namespace Avalonia.Browser.Skia;
+
+internal abstract class BrowserSurface : IDisposable
+{
+ private readonly BrowserRenderingMode _renderingMode;
+
+ protected BrowserSurface(JSObject jsSurface, BrowserRenderingMode renderingMode)
+ {
+ _renderingMode = renderingMode;
+ JsSurface = jsSurface;
+
+ Scaling = 1;
+ ClientSize = new Size(1, 1);
+ RenderSize = new PixelSize(1, 1);
+ }
+
+ public bool IsWebGl => _renderingMode is BrowserRenderingMode.WebGL1 or BrowserRenderingMode.WebGL2;
+
+ public JSObject JsSurface { get; private set; }
+ public double Scaling { get; private set; }
+ public Size ClientSize { get; private set; }
+ public PixelSize RenderSize { get; private set; }
+
+ public bool IsValid => RenderSize.Width > 0 && RenderSize.Height > 0 && Scaling > 0;
+
+ public event Action? SizeChanged;
+ public event Action? ScalingChanged;
+
+ public static BrowserSurface Create(JSObject container, PixelFormat pixelFormat)
+ {
+ var opts = AvaloniaLocator.Current.GetService() ?? new BrowserPlatformOptions();
+ if (opts.RenderingMode is null || !opts.RenderingMode.Any())
+ {
+ throw new InvalidOperationException(
+ $"{nameof(BrowserPlatformOptions)}.{nameof(BrowserPlatformOptions.RenderingMode)} must not be empty or null");
+ }
+
+ BrowserSurface? surface = null;
+ foreach (var mode in opts.RenderingMode)
+ {
+ try
+ {
+ var (jsSurface, jsGlInfo) = CanvasHelper.CreateSurface(container, mode);
+ surface = jsGlInfo != null
+ ? new BrowserGlSurface(jsSurface, jsGlInfo, pixelFormat, mode)
+ : new BrowserRasterSurface(jsSurface, pixelFormat, mode);
+ break;
+ }
+ catch (Exception ex)
+ {
+ Logger.TryGet(LogEventLevel.Error, LogArea.BrowserPlatform)?
+ .Log(null,
+ "Creation of BrowserSurface with mode {Mode} failed with an error:\r\n{Exception}",
+ mode, ex);
+ }
+ }
+
+ if (surface is null)
+ {
+ throw new InvalidOperationException(
+ $"{nameof(BrowserPlatformOptions)}.{nameof(BrowserPlatformOptions.RenderingMode)} has a value of \"{string.Join(", ", opts.RenderingMode)}\", but no options were applied.");
+ }
+
+ CanvasHelper.OnSizeChanged(surface.JsSurface, surface.OnSizeChanged);
+ return surface;
+ }
+
+ public virtual void Dispose()
+ {
+ CanvasHelper.Destroy(JsSurface);
+ JsSurface.Dispose();
+ JsSurface = null!;
+ RenderSize = default;
+ ClientSize = default;
+ }
+
+ private void OnSizeChanged(int pixelWidth, int pixelHeight, double dpr)
+ {
+ var oldScaling = Scaling;
+ var oldClientSize = ClientSize;
+ RenderSize = new PixelSize(pixelWidth, pixelHeight);
+ ClientSize = RenderSize.ToSize(dpr);
+ Scaling = dpr;
+ if (oldClientSize != ClientSize)
+ SizeChanged?.Invoke();
+ if (Math.Abs(oldScaling - dpr) > 0.0001)
+ ScalingChanged?.Invoke();
+ }
+}
diff --git a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs
index a169966188..cb8a912b8c 100644
--- a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs
+++ b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using Avalonia.Browser.Rendering;
using Avalonia.Platform;
using Avalonia.Skia;
using Avalonia.Reactive;
@@ -12,7 +13,7 @@ namespace Avalonia.Browser.Skia
{
foreach (var surface in surfaces)
{
- if (surface is BrowserSkiaSurface browserSkiaSurface)
+ if (surface is BrowserGlSurface browserSkiaSurface)
{
return new BrowserSkiaGpuRenderTarget(browserSkiaSurface);
}
@@ -28,20 +29,19 @@ namespace Avalonia.Browser.Skia
public void Dispose()
{
-
}
- public object? TryGetFeature(Type t) => null;
-
+ public object? TryGetFeature(Type t) => null;
+
public bool IsLost => false;
-
+
public IDisposable EnsureCurrent()
{
return Disposable.Empty;
}
}
- class BrowserSkiaGraphics : IPlatformGraphics
+ internal class BrowserSkiaGraphics : IPlatformGraphics
{
private BrowserSkiaGpu _skia = new();
public bool UsesSharedContext => true;
diff --git a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderSession.cs b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderSession.cs
index 3e4cf31dda..b8fa21342c 100644
--- a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderSession.cs
+++ b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderSession.cs
@@ -1,3 +1,5 @@
+using System;
+using Avalonia.Browser.Rendering;
using Avalonia.Skia;
using SkiaSharp;
@@ -7,16 +9,17 @@ namespace Avalonia.Browser.Skia
{
private readonly SKSurface _surface;
- public BrowserSkiaGpuRenderSession(BrowserSkiaSurface browserSkiaSurface, GRBackendRenderTarget renderTarget)
+ public BrowserSkiaGpuRenderSession(BrowserGlSurface browserGlSurface, GRBackendRenderTarget renderTarget)
{
- _surface = SKSurface.Create(browserSkiaSurface.Context, renderTarget, browserSkiaSurface.Origin,
- browserSkiaSurface.ColorType, new SKSurfaceProperties(SKPixelGeometry.RgbHorizontal));
+ _surface = SKSurface.Create(browserGlSurface.Context, renderTarget, GRSurfaceOrigin.BottomLeft,
+ browserGlSurface.PixelFormat.ToSkColorType(), new SKSurfaceProperties(SKPixelGeometry.RgbHorizontal))
+ ?? throw new InvalidOperationException("Unable to create SKSurface.");
- GrContext = browserSkiaSurface.Context;
+ GrContext = browserGlSurface.Context;
+ ScaleFactor = browserGlSurface.Scaling;
+ SurfaceOrigin = GRSurfaceOrigin.BottomLeft;
- ScaleFactor = browserSkiaSurface.Scaling;
-
- SurfaceOrigin = browserSkiaSurface.Origin;
+ browserGlSurface.EnsureResize();
}
public void Dispose()
diff --git a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderTarget.cs b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderTarget.cs
index 9ede45a3eb..db4c5f2f8b 100644
--- a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderTarget.cs
+++ b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderTarget.cs
@@ -1,4 +1,5 @@
using System;
+using Avalonia.Browser.Rendering;
using Avalonia.Skia;
using SkiaSharp;
@@ -7,20 +8,20 @@ namespace Avalonia.Browser.Skia
internal class BrowserSkiaGpuRenderTarget : ISkiaGpuRenderTarget
{
private readonly GRBackendRenderTarget _renderTarget;
- private readonly BrowserSkiaSurface _browserSkiaSurface;
+ private readonly BrowserGlSurface _browserGlSurface;
private readonly PixelSize _size;
- public BrowserSkiaGpuRenderTarget(BrowserSkiaSurface browserSkiaSurface)
+ public BrowserSkiaGpuRenderTarget(BrowserGlSurface browserGlSurface)
{
- _size = browserSkiaSurface.Size;
+ _size = browserGlSurface.RenderSize;
- var glFbInfo = new GRGlFramebufferInfo(browserSkiaSurface.GlInfo.FboId, browserSkiaSurface.ColorType.ToGlSizedFormat());
- _browserSkiaSurface = browserSkiaSurface;
+ var glFbInfo = new GRGlFramebufferInfo(browserGlSurface.GlInfo.FboId, browserGlSurface.PixelFormat.ToSkColorType().ToGlSizedFormat());
+ _browserGlSurface = browserGlSurface;
_renderTarget = new GRBackendRenderTarget(
- (int)Math.Round(browserSkiaSurface.DisplaySize.Width * browserSkiaSurface.Scaling),
- (int)Math.Round(browserSkiaSurface.DisplaySize.Height * browserSkiaSurface.Scaling),
- browserSkiaSurface.GlInfo.Samples,
- browserSkiaSurface.GlInfo.Stencils, glFbInfo);
+ _size.Width,
+ _size.Height,
+ browserGlSurface.GlInfo.Samples,
+ browserGlSurface.GlInfo.Stencils, glFbInfo);
}
public void Dispose()
@@ -30,9 +31,9 @@ namespace Avalonia.Browser.Skia
public ISkiaGpuRenderSession BeginRenderingSession()
{
- return new BrowserSkiaGpuRenderSession(_browserSkiaSurface, _renderTarget);
+ return new BrowserSkiaGpuRenderSession(_browserGlSurface, _renderTarget);
}
- public bool IsCorrupted => _browserSkiaSurface.Size != _size;
+ public bool IsCorrupted => _browserGlSurface.RenderSize != _size;
}
}
diff --git a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaRasterSurface.cs b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaRasterSurface.cs
deleted file mode 100644
index 777d44ea66..0000000000
--- a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaRasterSurface.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-using System;
-using System.Runtime.InteropServices;
-using Avalonia.Controls.Platform.Surfaces;
-using Avalonia.Platform;
-using Avalonia.Skia;
-using SkiaSharp;
-
-namespace Avalonia.Browser.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 _blitCallback;
- private readonly Action _onDisposeAction;
-
- public BrowserSkiaRasterSurface(
- SKColorType colorType, PixelSize size, double scaling, Action 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);
- }
- }
-
- public IFramebufferRenderTarget CreateFramebufferRenderTarget() => new FuncFramebufferRenderTarget(Lock);
- }
-}
diff --git a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaSurface.cs b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaSurface.cs
deleted file mode 100644
index 9170aab782..0000000000
--- a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaSurface.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using Avalonia.Browser.Interop;
-using SkiaSharp;
-
-namespace Avalonia.Browser.Skia
-{
- internal class BrowserSkiaSurface : IBrowserSkiaSurface
- {
- public BrowserSkiaSurface(GRContext context, GLInfo glInfo, SKColorType colorType, PixelSize size, Size displaySize, double scaling, GRSurfaceOrigin origin)
- {
- Context = context;
- GlInfo = glInfo;
- ColorType = colorType;
- Size = size;
- DisplaySize = displaySize;
- Scaling = scaling;
- Origin = origin;
- }
-
- public SKColorType ColorType { get; set; }
-
- public PixelSize Size { get; set; }
-
- public Size DisplaySize { get; set; }
-
- public GRContext Context { get; set; }
-
- public GRSurfaceOrigin Origin { get; set; }
-
- public double Scaling { get; set; }
-
- public GLInfo GlInfo { get; set; }
- }
-}
diff --git a/src/Browser/Avalonia.Browser/Skia/IBrowserSkiaSurface.cs b/src/Browser/Avalonia.Browser/Skia/IBrowserSkiaSurface.cs
deleted file mode 100644
index 1585d7f283..0000000000
--- a/src/Browser/Avalonia.Browser/Skia/IBrowserSkiaSurface.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Avalonia.Browser.Skia
-{
- internal interface IBrowserSkiaSurface
- {
- public PixelSize Size { get; set; }
-
- public double Scaling { get; set; }
- }
-}
diff --git a/src/Browser/Avalonia.Browser/webapp/.eslintrc.json b/src/Browser/Avalonia.Browser/webapp/.eslintrc.json
index f4fb8e37bf..b8b5d87e76 100644
--- a/src/Browser/Avalonia.Browser/webapp/.eslintrc.json
+++ b/src/Browser/Avalonia.Browser/webapp/.eslintrc.json
@@ -23,6 +23,7 @@
],
"quotes": ["warn", "double"],
"semi": ["error", "always"],
+ "@typescript-eslint/no-this-alias": "off",
"@typescript-eslint/quotes": ["warn", "double"],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-extraneous-class": "off",
diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts
index 212d67211c..7d7b64656c 100644
--- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts
+++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts
@@ -1,4 +1,3 @@
-import { ResizeHandler, Canvas } from "./avalonia/canvas";
import { InputHelper } from "./avalonia/input";
import { AvaloniaDOM } from "./avalonia/dom";
import { Caniuse } from "./avalonia/caniuse";
@@ -6,6 +5,8 @@ import { StreamHelper } from "./avalonia/stream";
import { NativeControlHost } from "./avalonia/nativeControlHost";
import { NavigationHelper } from "./avalonia/navigationHelper";
import { GeneralHelpers } from "./avalonia/generalHelpers";
+import { TimerHelper } from "./avalonia/timer";
+import { CanvasFactory } from "./avalonia/surfaces/surfaceFactory";
async function registerServiceWorker(path: string, scope: string | undefined) {
if ("serviceWorker" in navigator) {
@@ -15,13 +16,13 @@ async function registerServiceWorker(path: string, scope: string | undefined) {
export {
Caniuse,
- Canvas,
+ CanvasFactory,
InputHelper,
- ResizeHandler,
AvaloniaDOM,
StreamHelper,
NativeControlHost,
NavigationHelper,
GeneralHelpers,
+ TimerHelper,
registerServiceWorker
};
diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/canvas.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/canvas.ts
deleted file mode 100644
index a653c5518a..0000000000
--- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/canvas.ts
+++ /dev/null
@@ -1,263 +0,0 @@
-interface SKGLViewInfo {
- context: WebGLRenderingContext | WebGL2RenderingContext | undefined;
- fboId: number;
- stencil: number;
- sample: number;
- depth: number;
-}
-
-type CanvasElement = {
- Canvas: Canvas | undefined;
-} & HTMLCanvasElement;
-
-function getGL(): any {
- const self = globalThis as any;
- const module = self.Module ?? self.getDotnetRuntime(0)?.Module;
- return module?.GL ?? self.AvaloniaGL ?? self.SkiaSharpGL;
-}
-
-export class Canvas {
- static elements: Map;
-
- 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();
- }
- 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 = getGL();
-
- // 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.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 {
- if (this.renderLoopRequest !== 0) {
- window.cancelAnimationFrame(this.renderLoopRequest);
- this.renderLoopRequest = 0;
- }
-
- 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;
- }
-
- this.requestAnimationFrame();
- }
-
- 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 = getGL();
-
- 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 ResizeHandlerCallback = (displayWidth: number, displayHeight: number, dpi: number) => void;
-
-type ResizeObserverWithCallbacks = {
- callbacks: Map;
-} & ResizeObserver;
-
-export class ResizeHandler {
- private static resizeObserver?: ResizeObserverWithCallbacks;
-
- public static observeSize(element: HTMLElement, callback: ResizeHandlerCallback): (() => void) {
- if (!this.resizeObserver) {
- this.resizeObserver = new ResizeObserver(this.onResize) as ResizeObserverWithCallbacks;
- this.resizeObserver.callbacks = new Map();
- }
-
- this.resizeObserver.callbacks.set(element, callback);
- this.resizeObserver.observe(element, { box: "content-box" });
-
- return () => {
- this.resizeObserver?.callbacks.delete(element);
- this.resizeObserver?.unobserve(element);
- };
- }
-
- private static onResize(entries: ResizeObserverEntry[], observer: ResizeObserver) {
- for (const entry of entries) {
- const callback = (observer as ResizeObserverWithCallbacks).callbacks.get(entry.target);
- if (!callback) {
- continue;
- }
-
- const trueDpr = window.devicePixelRatio;
- let width;
- let height;
- let dpr = trueDpr;
- if (entry.devicePixelContentBoxSize) {
- // NOTE: Only this path gives the correct answer
- // The other paths are imperfect fallbacks
- // for browsers that don't provide anyway to do this
- width = entry.devicePixelContentBoxSize[0].inlineSize;
- height = entry.devicePixelContentBoxSize[0].blockSize;
- dpr = 1; // it's already in width and height
- } else if (entry.contentBoxSize) {
- if (entry.contentBoxSize[0]) {
- width = entry.contentBoxSize[0].inlineSize;
- height = entry.contentBoxSize[0].blockSize;
- } else {
- width = (entry.contentBoxSize as any).inlineSize;
- height = (entry.contentBoxSize as any).blockSize;
- }
- } else {
- width = entry.contentRect.width;
- height = entry.contentRect.height;
- }
- const displayWidth = Math.round(width * dpr);
- const displayHeight = Math.round(height * dpr);
-
- callback(displayWidth, displayHeight, trueDpr);
- }
- }
-}
diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts
index 36119545eb..d51ee4b184 100644
--- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts
+++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts
@@ -1,3 +1,4 @@
+
export class AvaloniaDOM {
public static addClass(element: HTMLElement, className: string): void {
element.classList.add(className);
@@ -24,27 +25,48 @@ export class AvaloniaDOM {
};
}
+ static getFirstElementByClassName(className: string, parent?: HTMLElement): Element | null {
+ const elements = (parent ?? globalThis.document).getElementsByClassName(className);
+ return elements ? elements[0] : null;
+ }
+
+ static createAvaloniaCanvas(host: HTMLElement): HTMLCanvasElement {
+ const containerId = host.getAttribute("data-containerId") ?? "0000";
+
+ const canvas = document.createElement("canvas");
+ canvas.id = `canvas${containerId}`;
+ canvas.classList.add("avalonia-canvas");
+ canvas.style.width = "100%";
+ canvas.style.height = "100%";
+ canvas.style.position = "absolute";
+
+ return canvas;
+ }
+
+ static attachCanvas(host: HTMLElement, canvas: HTMLCanvasElement): void {
+ host.prepend(canvas);
+ }
+
+ static detachCanvas(host: HTMLElement, canvas: HTMLCanvasElement): void {
+ host.removeChild(canvas);
+ }
+
static createAvaloniaHost(host: HTMLElement) {
- const randomIdPart = Math.random().toString(36).replace(/[^a-z]+/g, "").substr(2, 10);
+ const containerId = Math.random().toString(36).replace(/[^a-z]+/g, "").substr(2, 10);
// Root element
host.classList.add("avalonia-container");
host.tabIndex = 0;
+ host.setAttribute("data-containerId", containerId);
host.oncontextmenu = function () { return false; };
host.style.overflow = "hidden";
host.style.touchAction = "none";
- // Rendering target canvas
- const canvas = document.createElement("canvas");
- canvas.id = `canvas${randomIdPart}`;
- canvas.classList.add("avalonia-canvas");
- canvas.style.width = "100%";
- canvas.style.height = "100%";
- canvas.style.position = "absolute";
+ // Canvas is lazily created depending on the rendering mode. See createAvaloniaCanvas usage.
// Native controls host
const nativeHost = document.createElement("div");
- nativeHost.id = `nativeHost${randomIdPart}`;
+ nativeHost.id = `nativeHost${containerId}`;
nativeHost.classList.add("avalonia-native-host");
nativeHost.style.left = "0px";
nativeHost.style.top = "0px";
@@ -54,13 +76,14 @@ export class AvaloniaDOM {
// IME
const inputElement = document.createElement("input");
- inputElement.id = `inputElement${randomIdPart}`;
+ inputElement.id = `inputElement${containerId}`;
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.borderWidth = "0";
inputElement.style.position = "absolute";
inputElement.style.overflow = "hidden";
inputElement.style.borderStyle = "hidden";
@@ -76,11 +99,9 @@ export class AvaloniaDOM {
host.prepend(inputElement);
host.prepend(nativeHost);
- host.prepend(canvas);
return {
host,
- canvas,
nativeHost,
inputElement
};
@@ -99,11 +120,18 @@ export class AvaloniaDOM {
}
}
+ public static initSafeAreaPadding(): void {
+ document.documentElement.style.setProperty("--av-sat", "env(safe-area-inset-top)");
+ document.documentElement.style.setProperty("--av-sar", "env(safe-area-inset-right)");
+ document.documentElement.style.setProperty("--av-sab", "env(safe-area-inset-bottom)");
+ document.documentElement.style.setProperty("--av-sal", "env(safe-area-inset-left)");
+ }
+
public static getSafeAreaPadding(): number[] {
- const top = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sat"));
- const bottom = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sab"));
- const left = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sal"));
- const right = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sar"));
+ const top = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--av-sat"));
+ const bottom = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--av-sab"));
+ const left = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--av-sal"));
+ const right = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--av-sar"));
return [left, top, bottom, right];
}
diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/htmlSurfaceBase.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/htmlSurfaceBase.ts
new file mode 100644
index 0000000000..30a6bbac03
--- /dev/null
+++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/htmlSurfaceBase.ts
@@ -0,0 +1,40 @@
+import { ResizeHandler } from "./resizeHandler";
+import { CanvasSurface, AvaloniaRenderingContext, BrowserRenderingMode } from "./surfaceBase";
+
+export abstract class HtmlCanvasSurfaceBase extends CanvasSurface {
+ private sizeParams?: [number, number, number];
+ private sizeChangedCallback?: (width: number, height: number, dpr: number) => void;
+
+ constructor(
+ public canvas: HTMLCanvasElement,
+ public context: AvaloniaRenderingContext,
+ public mode: BrowserRenderingMode) {
+ super(context, mode);
+
+ // No need to ubsubsribe, canvas never leaves JS world, it should be GC'ed with all callbacks.
+ ResizeHandler.observeSize(canvas, (width, height, dpr) => {
+ this.sizeParams = [width, height, dpr];
+
+ if (this.sizeChangedCallback) {
+ this.sizeChangedCallback(width, height, dpr);
+ }
+ });
+ }
+
+ public destroy(): void {
+ delete this.sizeChangedCallback;
+ }
+
+ public onSizeChanged(sizeChangedCallback: (width: number, height: number, dpr: number) => void) {
+ if (this.sizeChangedCallback) { throw new Error("For simplicity, we don't support multiple size changed callbacks per surface, not needed yet."); }
+ this.sizeChangedCallback = sizeChangedCallback;
+ }
+
+ public ensureSize() {
+ if (this.sizeParams) {
+ this.canvas.width = this.sizeParams[0];
+ this.canvas.height = this.sizeParams[1];
+ delete this.sizeParams;
+ }
+ }
+}
diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/resizeHandler.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/resizeHandler.ts
new file mode 100644
index 0000000000..03f1f23b96
--- /dev/null
+++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/resizeHandler.ts
@@ -0,0 +1,58 @@
+type ResizeObserverWithCallbacks = {
+ callbacks: Map void)>;
+} & ResizeObserver;
+
+export class ResizeHandler {
+ private static resizeObserver?: ResizeObserverWithCallbacks;
+
+ public static observeSize(element: HTMLElement, callback: (width: number, height: number, dpr: number) => void) : (() => void) {
+ if (!this.resizeObserver) {
+ this.resizeObserver = new ResizeObserver(this.onResize) as ResizeObserverWithCallbacks;
+ this.resizeObserver.callbacks = new Map void)>();
+ }
+
+ this.resizeObserver.callbacks.set(element, callback);
+ this.resizeObserver.observe(element, { box: "content-box" });
+
+ return () => {
+ this.resizeObserver?.callbacks.delete(element);
+ this.resizeObserver?.unobserve(element);
+ };
+ }
+
+ private static onResize(entries: ResizeObserverEntry[], observer: ResizeObserver) {
+ for (const entry of entries) {
+ const callback = (observer as ResizeObserverWithCallbacks).callbacks.get(entry.target);
+ if (!callback) {
+ continue;
+ }
+
+ const trueDpr = window.devicePixelRatio;
+ let width;
+ let height;
+ let dpr = trueDpr;
+ if (entry.devicePixelContentBoxSize) {
+ // NOTE: Only this path gives the correct answer
+ // The other paths are imperfect fallbacks
+ // for browsers that don't provide anyway to do this
+ width = entry.devicePixelContentBoxSize[0].inlineSize;
+ height = entry.devicePixelContentBoxSize[0].blockSize;
+ dpr = 1; // it's already in width and height
+ } else if (entry.contentBoxSize) {
+ if (entry.contentBoxSize[0]) {
+ width = entry.contentBoxSize[0].inlineSize;
+ height = entry.contentBoxSize[0].blockSize;
+ } else {
+ width = (entry.contentBoxSize as any).inlineSize;
+ height = (entry.contentBoxSize as any).blockSize;
+ }
+ } else {
+ width = entry.contentRect.width;
+ height = entry.contentRect.height;
+ }
+ const displayWidth = Math.round(width * dpr);
+ const displayHeight = Math.round(height * dpr);
+ callback(displayWidth, displayHeight, trueDpr);
+ }
+ }
+}
diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/softwareSurface.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/softwareSurface.ts
new file mode 100644
index 0000000000..df32711a37
--- /dev/null
+++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/softwareSurface.ts
@@ -0,0 +1,48 @@
+import { BrowserRenderingMode } from "./surfaceBase";
+import { HtmlCanvasSurfaceBase } from "./htmlSurfaceBase";
+import { RuntimeAPI } from "../../../types/dotnet";
+
+const sharedArrayBufferDefined = typeof SharedArrayBuffer !== "undefined";
+function isSharedArrayBuffer(buffer: any): buffer is SharedArrayBuffer {
+ // BEWARE: In some cases, `instanceof SharedArrayBuffer` returns false even though buffer is an SAB.
+ // Patch adapted from https://github.com/emscripten-core/emscripten/pull/16994
+ // See also https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag
+ return sharedArrayBufferDefined && buffer[Symbol.toStringTag] === "SharedArrayBuffer";
+}
+
+export class SoftwareSurface extends HtmlCanvasSurfaceBase {
+ private readonly runtime: RuntimeAPI | undefined;
+
+ constructor(public canvas: HTMLCanvasElement) {
+ const context = canvas.getContext("2d", {
+ alpha: true
+ });
+ if (!context) {
+ throw new Error("HTMLCanvasElement.getContext(2d) returned null.");
+ }
+ super(canvas, context, BrowserRenderingMode.Software2D);
+
+ this.runtime = globalThis.getDotnetRuntime(0);
+ }
+
+ public putPixelData(span: any /* IMemoryView */, width: number, height: number): void {
+ this.ensureSize();
+
+ const heap8 = this.runtime?.localHeapViewU8();
+
+ let clampedBuffer: Uint8ClampedArray;
+ if (span._pointer > 0 && span._length > 0 && heap8 && !isSharedArrayBuffer(heap8.buffer)) {
+ // Attempt to use undocumented access to the HEAP8 directly
+ // Note, SharedArrayBuffer cannot be used with ImageData (when WasmEnableThreads = true).
+ clampedBuffer = new Uint8ClampedArray(heap8.buffer, span._pointer, span._length);
+ } else {
+ // Or fallback to the normal API that does multiple array copies.
+ const copy = new Uint8Array(span.byteLength);
+ span.copyTo(copy);
+ clampedBuffer = new Uint8ClampedArray(copy.buffer);
+ }
+
+ const imageData = new ImageData(clampedBuffer, width, height);
+ (this.context as CanvasRenderingContext2D).putImageData(imageData, 0, 0);
+ }
+}
diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/surfaceBase.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/surfaceBase.ts
new file mode 100644
index 0000000000..88cde86f37
--- /dev/null
+++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/surfaceBase.ts
@@ -0,0 +1,18 @@
+export type AvaloniaRenderingContext = RenderingContext;
+
+export enum BrowserRenderingMode {
+ Software2D = 1,
+ WebGL1,
+ WebGL2
+}
+
+export abstract class CanvasSurface {
+ constructor(
+ public context: AvaloniaRenderingContext,
+ public mode: BrowserRenderingMode) {
+ }
+
+ abstract destroy(): void;
+ abstract ensureSize(): void;
+ abstract onSizeChanged(sizeChangedCallback: (width: number, height: number, dpr: number) => void): void;
+}
diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/surfaceFactory.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/surfaceFactory.ts
new file mode 100644
index 0000000000..cb5810f5a4
--- /dev/null
+++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/surfaceFactory.ts
@@ -0,0 +1,44 @@
+import { AvaloniaDOM } from "../dom";
+import { SoftwareSurface } from "./softwareSurface";
+import { BrowserRenderingMode, CanvasSurface } from "./surfaceBase";
+import { WebGlSurface } from "./webGlSurface";
+
+export class CanvasFactory {
+ public static create(container: HTMLElement, mode: BrowserRenderingMode): CanvasSurface {
+ if (!container) {
+ throw new Error("No html container was provided.");
+ }
+
+ const canvas = AvaloniaDOM.createAvaloniaCanvas(container);
+ AvaloniaDOM.attachCanvas(container, canvas);
+
+ try {
+ if (mode === BrowserRenderingMode.Software2D) {
+ return new SoftwareSurface(canvas);
+ } else if (mode === BrowserRenderingMode.WebGL1 || mode === BrowserRenderingMode.WebGL2) {
+ return new WebGlSurface(canvas, mode);
+ } else {
+ throw new Error(`Unsupported rendering mode: ${BrowserRenderingMode[mode]}`);
+ }
+ } catch (ex) {
+ AvaloniaDOM.detachCanvas(container, canvas);
+ throw ex;
+ }
+ }
+
+ public static destroy(surface: CanvasSurface) {
+ surface.destroy();
+ }
+
+ public static onSizeChanged(surface: CanvasSurface, sizeChangedCallback: (width: number, height: number, dpr: number) => void) {
+ surface.onSizeChanged(sizeChangedCallback);
+ }
+
+ public static ensureSize(surface: CanvasSurface): void {
+ surface.ensureSize();
+ }
+
+ public static putPixelData(surface: SoftwareSurface, span: any /* IMemoryView */, width: number, height: number): void {
+ surface.putPixelData(span, width, height);
+ }
+}
diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/webGlSurface.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/webGlSurface.ts
new file mode 100644
index 0000000000..9da7f5bf8a
--- /dev/null
+++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/webGlSurface.ts
@@ -0,0 +1,58 @@
+import { BrowserRenderingMode } from "./surfaceBase";
+import { HtmlCanvasSurfaceBase } from "./htmlSurfaceBase";
+
+function getGL(): any {
+ const self = globalThis as any;
+ const module = self.Module ?? self.getDotnetRuntime(0)?.Module;
+ return module?.GL ?? self.AvaloniaGL ?? self.SkiaSharpGL;
+}
+
+export class WebGlSurface extends HtmlCanvasSurfaceBase {
+ public contextHandle?: number;
+ public fboId?: number;
+ public stencil?: number;
+ public sample?: number;
+ public depth?: number;
+
+ constructor(public canvas: HTMLCanvasElement, mode: BrowserRenderingMode.WebGL1 | BrowserRenderingMode.WebGL2) {
+ // Skia only understands WebGL context wrapped in Emscripten.
+ const gl = getGL();
+ if (!gl) {
+ throw new Error("Module.GL object wasn't initialized, WebGL can't be used.");
+ }
+
+ const modeStr = mode === BrowserRenderingMode.WebGL1 ? "webgl" : "webgl2";
+ const attrs: WebGLContextAttributes | any =
+ {
+ alpha: true,
+ depth: true,
+ stencil: true,
+ antialias: false,
+ premultipliedAlpha: true,
+ preserveDrawingBuffer: false,
+ // only supported on older browsers, which is perfect as we want to fallback to 2d there.
+ failIfMajorPerformanceCaveat: true,
+ // attrs used by Emscripten:
+ majorVersion: mode === BrowserRenderingMode.WebGL1 ? 1 : 2,
+ minorVersion: 0,
+ enableExtensionsByDefault: 1,
+ explicitSwapControl: 0
+ };
+ const context = canvas.getContext(modeStr, attrs) as WebGLRenderingContext;
+ if (!context) {
+ throw new Error(`HTMLCanvasElement.getContext(${modeStr}) returned null.`);
+ }
+
+ const handle = gl.registerContext(context, attrs);
+ gl.makeContextCurrent(handle);
+ (context as any).gl_handle = handle;
+
+ super(canvas, context, BrowserRenderingMode.Software2D);
+
+ this.contextHandle = handle;
+ this.fboId = context.getParameter(context.FRAMEBUFFER_BINDING)?.id ?? 0;
+ this.stencil = context.getParameter(context.STENCIL_BITS);
+ this.sample = context.getParameter(context.SAMPLES);
+ this.depth = context.getParameter(context.DEPTH_BITS);
+ }
+}
diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts
new file mode 100644
index 0000000000..7e52092056
--- /dev/null
+++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts
@@ -0,0 +1,12 @@
+export class TimerHelper {
+ public static runAnimationFrames(renderFrameCallback: (timestamp: number) => boolean): void {
+ function render(time: number) {
+ const next = renderFrameCallback(time);
+ if (next) {
+ window.requestAnimationFrame(render);
+ }
+ }
+
+ window.requestAnimationFrame(render);
+ }
+}
diff --git a/src/Browser/Avalonia.Browser/webapp/types/dotnet.d.ts b/src/Browser/Avalonia.Browser/webapp/types/dotnet.d.ts
index 0067ee3e0e..81eb0bd748 100644
--- a/src/Browser/Avalonia.Browser/webapp/types/dotnet.d.ts
+++ b/src/Browser/Avalonia.Browser/webapp/types/dotnet.d.ts
@@ -1,4 +1,4 @@
-// See https://raw.githubusercontent.com/dotnet/runtime/main/src/mono/wasm/runtime/dotnet.d.ts
+// See https://github.com/dotnet/runtime/blob/v8.0.3/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.
@@ -7,23 +7,6 @@
//! 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;
- run(): Promise;
-}
-
declare interface NativePointer {
__brandNativePointer: "NativePointer";
}
@@ -37,18 +20,28 @@ declare interface Int32Ptr extends NativePointer {
__brand: "Int32Ptr";
}
declare interface EmscriptenModule {
+ /** @deprecated Please use growableHeapI8() instead.*/
HEAP8: Int8Array;
+ /** @deprecated Please use growableHeapI16() instead.*/
HEAP16: Int16Array;
+ /** @deprecated Please use growableHeapI32() instead. */
HEAP32: Int32Array;
+ /** @deprecated Please use growableHeapI64() instead. */
+ HEAP64: BigInt64Array;
+ /** @deprecated Please use growableHeapU8() instead. */
HEAPU8: Uint8Array;
+ /** @deprecated Please use growableHeapU16() instead. */
HEAPU16: Uint16Array;
+ /** @deprecated Please use growableHeapU32() instead */
HEAPU32: Uint32Array;
+ /** @deprecated Please use growableHeapF32() instead */
HEAPF32: Float32Array;
+ /** @deprecated Please use growableHeapF64() instead. */
HEAPF64: Float64Array;
_malloc(size: number): VoidPtr;
_free(ptr: VoidPtr): void;
- print(message: string): void;
- printErr(message: string): void;
+ out(message: string): void;
+ err(message: string): void;
ccall(ident: string, returnType?: string | null, argTypes?: string[], args?: any[], opts?: any): T;
cwrap(ident: string, returnType: string, argTypes?: string[], opts?: any): T;
cwrap(ident: string, ...args: any[]): T;
@@ -57,15 +50,13 @@ declare interface EmscriptenModule {
getValue(ptr: number, type: string, noSafe?: number | boolean): number;
UTF8ToString(ptr: CharPtr, maxBytesToRead?: number): string;
UTF8ArrayToString(u8Array: Uint8Array, idx?: number, maxBytesToRead?: number): string;
+ stringToUTF8Array(str: string, heap: Uint8Array, outIdx: number, maxBytesToWrite: number): void;
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;
+ addFunction(fn: Function, signature: string): number;
stackSave(): VoidPtr;
stackRestore(stack: VoidPtr): void;
stackAlloc(size: number): VoidPtr;
- ready: Promise;
instantiateWasm?: InstantiateWasmCallBack;
preInit?: (() => any)[] | (() => any);
preRun?: (() => any)[] | (() => any);
@@ -74,20 +65,38 @@ declare interface EmscriptenModule {
onAbort?: {
(error: any): void;
};
+ onExit?: {
+ (code: number): void;
+ };
}
-declare type InstantiateWasmSuccessCallback = (instance: WebAssembly.Instance, module: WebAssembly.Module) => void;
-declare type InstantiateWasmCallBack = (imports: WebAssembly.Imports, successCallback: InstantiateWasmSuccessCallback) => any;
+type InstantiateWasmSuccessCallback = (instance: WebAssembly.Instance, module: WebAssembly.Module | undefined) => void;
+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;
+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;
+ withApplicationEnvironment(applicationEnvironment?: string): DotnetHostBuilder;
+ withApplicationCulture(applicationCulture?: string): DotnetHostBuilder;
/**
- * A list of assets to load along with the runtime.
+ * Overrides the built-in boot resource loading mechanism so that boot resources can be fetched
+ * from a custom source, such as an external CDN.
*/
- assets?: AssetEntry[];
+ withResourceLoader(loadBootResource?: LoadBootResourceCallback): DotnetHostBuilder;
+ create(): Promise;
+ run(): Promise;
+}
+type MonoConfig = {
/**
* Additional search locations for assets.
*/
@@ -100,6 +109,10 @@ declare type MonoConfig = {
* We are throttling parallel downloads in order to avoid net::ERR_INSUFFICIENT_RESOURCES on chrome. The default value is 16.
*/
maxParallelDownloads?: number;
+ /**
+ * We are making up to 2 more delayed attempts to download same asset. Default true.
+ */
+ enableDownloadRetry?: boolean;
/**
* Name of the assembly with main entrypoint
*/
@@ -111,12 +124,28 @@ declare type MonoConfig = {
/**
* 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 < 0 enables debugging and disables debug logging.
*/
debugLevel?: number;
/**
- * Enables diagnostic log messages during startup
- */
+ * Gets a value that determines whether to enable caching of the 'resources' inside a CacheStorage instance within the browser.
+ */
+ cacheBootResources?: boolean;
+ /**
+ * Delay of the purge of the cached resources in milliseconds. Default is 10000 (10 seconds).
+ */
+ cachedResourcesPurgeDelay?: number;
+ /**
+ * Configures use of the `integrity` directive for fetching assets
+ */
+ disableIntegrityCheck?: boolean;
+ /**
+ * Configures use of the `no-cache` directive for fetching assets
+ */
+ disableNoCacheFetch?: boolean;
+ /**
+ * Enables diagnostic log messages during startup
+ */
diagnosticTracing?: boolean;
/**
* Dictionary-style Object containing environment variables
@@ -128,19 +157,97 @@ declare type MonoConfig = {
* initial number of workers to add to the emscripten pthread pool
*/
pthreadPoolSize?: number;
+ /**
+ * If true, the snapshot of runtime's memory will be stored in the browser and used for faster startup next time. Default is false.
+ */
+ startupMemoryCache?: boolean;
+ /**
+ * application environment
+ */
+ applicationEnvironment?: string;
+ /**
+ * Gets the application culture. This is a name specified in the BCP 47 format. See https://tools.ietf.org/html/bcp47
+ */
+ applicationCulture?: string;
+ /**
+ * definition of assets to load along with the runtime.
+ */
+ resources?: ResourceGroups;
+ /**
+ * appsettings files to load to VFS
+ */
+ appsettings?: string[];
+ /**
+ * config extensions declared in MSBuild items @(WasmBootConfigExtension)
+ */
+ extensions?: {
+ [name: string]: any;
+ };
};
-interface ResourceRequest {
- name: string;
- behavior: AssetBehaviours;
- resolvedUrl?: string;
+type ResourceExtensions = {
+ [extensionName: string]: ResourceList;
+};
+interface ResourceGroups {
hash?: string;
+ assembly?: ResourceList;
+ lazyAssembly?: ResourceList;
+ pdb?: ResourceList;
+ jsModuleWorker?: ResourceList;
+ jsModuleNative: ResourceList;
+ jsModuleRuntime: ResourceList;
+ wasmSymbols?: ResourceList;
+ wasmNative: ResourceList;
+ icu?: ResourceList;
+ satelliteResources?: {
+ [cultureName: string]: ResourceList;
+ };
+ modulesAfterConfigLoaded?: ResourceList;
+ modulesAfterRuntimeReady?: ResourceList;
+ extensions?: ResourceExtensions;
+ vfs?: {
+ [virtualPath: string]: ResourceList;
+ };
}
+/**
+ * A "key" is name of the file, a "value" is optional hash for integrity check.
+ */
+type ResourceList = {
+ [name: string]: string | null | "";
+};
+/**
+ * Overrides the built-in boot resource loading mechanism so that boot resources can be fetched
+ * from a custom source, such as an external CDN.
+ * @param type The type of the resource to be loaded.
+ * @param name The name of the resource to be loaded.
+ * @param defaultUri The URI from which the framework would fetch the resource by default. The URI may be relative or absolute.
+ * @param integrity The integrity string representing the expected content in the response.
+ * @param behavior The detailed behavior/type of the resource to be loaded.
+ * @returns A URI string or a Response promise to override the loading process, or null/undefined to allow the default loading behavior.
+ * When returned string is not qualified with `./` or absolute URL, it will be resolved against the application base URI.
+ */
+type LoadBootResourceCallback = (type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string, behavior: AssetBehaviors) => string | Promise | null | undefined;
interface LoadingResource {
name: string;
url: string;
response: Promise;
}
-interface AssetEntry extends ResourceRequest {
+interface AssetEntry {
+ /**
+ * the name of the asset, including extension.
+ */
+ name: string;
+ /**
+ * determines how the asset will be handled once loaded
+ */
+ behavior: AssetBehaviors;
+ /**
+ * this should be absolute url to the asset
+ */
+ resolvedUrl?: string;
+ /**
+ * the integrity hash of the asset (if any)
+ */
+ hash?: string | null | "";
/**
* If specified, overrides the path of the asset in the virtual filesystem and similar data structures once downloaded.
*/
@@ -161,34 +268,116 @@ interface AssetEntry extends ResourceRequest {
* 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;
+ buffer?: ArrayBuffer | Promise;
+ /**
+ * If provided, runtime doesn't have to import it's JavaScript modules.
+ * This will not work for multi-threaded runtime.
+ */
+ moduleExports?: any | Promise;
/**
* It's metadata + fetch-like Promise
* 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 = {
+type SingleAssetBehaviors =
+/**
+ * The binary of the dotnet runtime.
+ */
+ "dotnetwasm"
+ /**
+ * The javascript module for loader.
+ */
+ | "js-module-dotnet"
+ /**
+ * The javascript module for threads.
+ */
+ | "js-module-threads"
+ /**
+ * The javascript module for runtime.
+ */
+ | "js-module-runtime"
+ /**
+ * The javascript module for emscripten.
+ */
+ | "js-module-native"
+ /**
+ * Typically blazor.boot.json
+ */
+ | "manifest";
+type AssetBehaviors = SingleAssetBehaviors |
+ /**
+ * Load asset as a managed resource assembly.
+ */
+ "resource"
+ /**
+ * Load asset as a managed assembly.
+ */
+ | "assembly"
+ /**
+ * Load asset as a managed debugging information.
+ */
+ | "pdb"
+ /**
+ * Store asset into the native heap.
+ */
+ | "heap"
+ /**
+ * Load asset as an ICU data archive.
+ */
+ | "icu"
+ /**
+ * Load asset into the virtual filesystem (for fopen, File.Open, etc).
+ */
+ | "vfs"
+ /**
+ * The javascript module that came from nuget package .
+ */
+ | "js-module-library-initializer"
+ /**
+ * The javascript module for threads.
+ */
+ | "symbols";
+declare const enum GlobalizationMode {
+ /**
+ * Load sharded ICU data.
+ */
+ Sharded = "sharded",
+ /**
+ * Load all ICU data.
+ */
+ All = "all",
+ /**
+ * Operate in invariant globalization mode.
+ */
+ Invariant = "invariant",
+ /**
+ * Use user defined icu file.
+ */
+ Custom = "custom",
+ /**
+ * Operate in hybrid globalization mode with small ICU files, using native platform functions.
+ */
+ Hybrid = "hybrid"
+}
+type DotnetModuleConfig = {
disableDotnet6Compatibility?: boolean;
config?: MonoConfig;
configSrc?: string;
onConfigLoaded?: (config: MonoConfig) => void | Promise;
onDotnetReady?: () => void | Promise;
+ onDownloadResourceProgress?: (resourcesLoaded: number, totalResources: number) => void;
imports?: any;
exports?: string[];
- downloadResource?: (request: ResourceRequest) => LoadingResource | undefined;
} & Partial;
-declare type APIType = {
+type APIType = {
runMain: (mainAssemblyName: string, args: string[]) => Promise;
runMainAndExit: (mainAssemblyName: string, args: string[]) => Promise;
setEnvironmentVariable: (name: string, value: string) => void;
getAssemblyExports(assemblyName: string): Promise;
setModuleImports(moduleName: string, moduleImports: any): void;
getConfig: () => MonoConfig;
+ invokeLibraryInitializers: (functionName: string, args: any[]) => Promise;
setHeapB32: (offset: NativePointer, value: number | boolean) => void;
setHeapU8: (offset: NativePointer, value: number) => void;
setHeapU16: (offset: NativePointer, value: number) => void;
@@ -213,8 +402,17 @@ declare type APIType = {
getHeapI64Big: (offset: NativePointer) => bigint;
getHeapF32: (offset: NativePointer) => number;
getHeapF64: (offset: NativePointer) => number;
+ localHeapViewI8: () => Int8Array;
+ localHeapViewI16: () => Int16Array;
+ localHeapViewI32: () => Int32Array;
+ localHeapViewI64Big: () => BigInt64Array;
+ localHeapViewU8: () => Uint8Array;
+ localHeapViewU16: () => Uint16Array;
+ localHeapViewU32: () => Uint32Array;
+ localHeapViewF32: () => Float32Array;
+ localHeapViewF64: () => Float64Array;
};
-declare type RuntimeAPI = {
+type RuntimeAPI = {
/**
* @deprecated Please use API object instead. See also MONOType in dotnet-legacy.d.ts
*/
@@ -232,23 +430,18 @@ declare type RuntimeAPI = {
buildConfiguration: string;
};
} & APIType;
-declare type ModuleAPI = {
+type ModuleAPI = {
dotnet: DotnetHostBuilder;
exit: (code: number, reason?: any) => void;
};
-declare function createDotnetRuntime(moduleFactory: DotnetModuleConfig | ((api: RuntimeAPI) => DotnetModuleConfig)): Promise;
-declare type CreateDotnetRuntimeType = typeof createDotnetRuntime;
+type CreateDotnetRuntimeType = (moduleFactory: DotnetModuleConfig | ((api: RuntimeAPI) => DotnetModuleConfig)) => Promise;
+type WebAssemblyBootResourceType = "assembly" | "pdb" | "dotnetjs" | "dotnetwasm" | "globalization" | "manifest" | "configuration";
-declare global {
- function getDotnetRuntime(runtimeId: number): RuntimeAPI | undefined;
+interface IDisposable {
+ dispose(): void;
+ get isDisposed(): boolean;
}
-
-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 {
+interface IMemoryView extends IDisposable {
/**
* copies elements from provided source to the wasm memory.
* target has to have the elements of the same type as the underlying C# array.
@@ -264,7 +457,18 @@ export interface IMemoryView {
* same as TypedArray.slice()
*/
slice(start?: number, end?: number): TypedArray;
-
get length(): number;
get byteLength(): number;
}
+
+declare function mono_exit(exit_code: number, reason?: any): void;
+
+declare const dotnet: DotnetHostBuilder;
+declare const exit: typeof mono_exit;
+
+declare global {
+ function getDotnetRuntime(runtimeId: number): RuntimeAPI | undefined;
+}
+declare const createDotnetRuntime: CreateDotnetRuntimeType;
+
+export { AssetBehaviors, AssetEntry, CreateDotnetRuntimeType, DotnetHostBuilder, DotnetModuleConfig, EmscriptenModule, GlobalizationMode, IMemoryView, ModuleAPI, MonoConfig, RuntimeAPI, createDotnetRuntime as default, dotnet, exit };