Browse Source

Browser software renderer and some refactoring allowing multiple AvaloniaView instances (#15207)

* Update .NET runtime TypeScript definitions

* Get splash screen by classname instead of ID

* Minimize css usage

* Move insets css properties to JS file

* Refactor browser surface handling, re-enable software renderer, allow fallback render modes

* Extract render timer from the surface, try to reuse compositor instance between avalonia views

* ControlCatalog: Make it possible to set some browser properties (like render mode) from URI arguments

* Rename AppBundle folder to wwwroot

* Extract ITextInputMethodImpl into a BrowserTopLevelImpl

* Extract input into BrowserInputHandler

* Make default surface size 1,1 to match other backends

* Reformat code that I touched (for the most part)

* Why this method even was in public API
pull/15258/head
Max Katz 2 years ago
committed by GitHub
parent
commit
7816f11858
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 22
      api/Avalonia.Browser.nupkg.xml
  2. 5
      samples/ControlCatalog.Browser/AppBundle/Logo.svg
  3. 74
      samples/ControlCatalog.Browser/AppBundle/app.css
  4. 28
      samples/ControlCatalog.Browser/AppBundle/index.html
  5. 5
      samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj
  6. 84
      samples/ControlCatalog.Browser/Program.cs
  7. 5
      samples/ControlCatalog.Browser/wwwroot/Logo.svg
  8. 38
      samples/ControlCatalog.Browser/wwwroot/app.css
  9. 0
      samples/ControlCatalog.Browser/wwwroot/embed.js
  10. 0
      samples/ControlCatalog.Browser/wwwroot/favicon.ico
  11. 46
      samples/ControlCatalog.Browser/wwwroot/index.html
  12. 6
      samples/ControlCatalog.Browser/wwwroot/main.js
  13. 8
      samples/MobileSandbox/Platforms/Browser/wwwroot/Logo.svg
  14. 84
      samples/MobileSandbox/Platforms/Browser/wwwroot/app.css
  15. 14
      samples/MobileSandbox/Platforms/Browser/wwwroot/index.html
  16. 1
      src/Avalonia.Base/Data/Core/TargetTypeConverter.cs
  17. 536
      src/Browser/Avalonia.Browser/AvaloniaView.cs
  18. 31
      src/Browser/Avalonia.Browser/BrowserAppBuilder.cs
  19. 6
      src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs
  20. 383
      src/Browser/Avalonia.Browser/BrowserInputHandler.cs
  21. 13
      src/Browser/Avalonia.Browser/BrowserInsetsManager.cs
  22. 2
      src/Browser/Avalonia.Browser/BrowserMouseDevice.cs
  23. 4
      src/Browser/Avalonia.Browser/BrowserNativeControlHost.cs
  24. 3
      src/Browser/Avalonia.Browser/BrowserRuntimePlatform.cs
  25. 5
      src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs
  26. 162
      src/Browser/Avalonia.Browser/BrowserTextInputMethod.cs
  27. 203
      src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs
  28. 64
      src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs
  29. 14
      src/Browser/Avalonia.Browser/Interop/DomHelper.cs
  30. 23
      src/Browser/Avalonia.Browser/Interop/TimerHelper.cs
  31. 24
      src/Browser/Avalonia.Browser/Rendering/BrowserCompositor.cs
  32. 51
      src/Browser/Avalonia.Browser/Rendering/BrowserGlSurface.cs
  33. 98
      src/Browser/Avalonia.Browser/Rendering/BrowserRasterSurface.cs
  34. 46
      src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs
  35. 102
      src/Browser/Avalonia.Browser/Rendering/BrowserSurface.cs
  36. 12
      src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs
  37. 17
      src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderSession.cs
  38. 23
      src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderTarget.cs
  39. 90
      src/Browser/Avalonia.Browser/Skia/BrowserSkiaRasterSurface.cs
  40. 33
      src/Browser/Avalonia.Browser/Skia/BrowserSkiaSurface.cs
  41. 9
      src/Browser/Avalonia.Browser/Skia/IBrowserSkiaSurface.cs
  42. 1
      src/Browser/Avalonia.Browser/webapp/.eslintrc.json
  43. 7
      src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts
  44. 263
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/canvas.ts
  45. 60
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts
  46. 40
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/htmlSurfaceBase.ts
  47. 58
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/resizeHandler.ts
  48. 48
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/softwareSurface.ts
  49. 18
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/surfaceBase.ts
  50. 44
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/surfaceFactory.ts
  51. 58
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/webGlSurface.ts
  52. 12
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts
  53. 330
      src/Browser/Avalonia.Browser/webapp/types/dotnet.d.ts

22
api/Avalonia.Browser.nupkg.xml

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Browser.AvaloniaView.get_IsComposing</Target>
<Left>baseline/net7.0/Avalonia.Browser.dll</Left>
<Right>target/net8.0-browser1.0/Avalonia.Browser.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Browser.AvaloniaView.OnDragEvent(System.Runtime.InteropServices.JavaScript.JSObject)</Target>
<Left>baseline/net7.0/Avalonia.Browser.dll</Left>
<Right>target/net8.0-browser1.0/Avalonia.Browser.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0008</DiagnosticId>
<Target>T:Avalonia.Browser.AvaloniaView</Target>
<Left>baseline/net7.0/Avalonia.Browser.dll</Left>
<Right>target/net8.0-browser1.0/Avalonia.Browser.dll</Right>
</Suppression>
</Suppressions>

5
samples/ControlCatalog.Browser/AppBundle/Logo.svg

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

Before

Width:  |  Height:  |  Size: 1.2 KiB

74
samples/ControlCatalog.Browser/AppBundle/app.css

@ -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;
}
}

28
samples/ControlCatalog.Browser/AppBundle/index.html

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

5
samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj

@ -2,10 +2,9 @@
<PropertyGroup>
<TargetFramework>$(AvsCurrentBrowserTargetFramework)</TargetFramework>
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<WasmMainJSPath>AppBundle/main.js</WasmMainJSPath>
<WasmMainJSPath>wwwroot/main.js</WasmMainJSPath>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<WasmRuntimeAssetsLocation>./</WasmRuntimeAssetsLocation>
</PropertyGroup>
<ItemGroup>
@ -15,7 +14,7 @@
</ItemGroup>
<ItemGroup>
<WasmExtraFilesToDeploy Include="AppBundle/**" />
<WasmExtraFilesToDeploy Include="wwwroot/**" />
</ItemGroup>
<Import Project="../../src/Browser/Avalonia.Browser/build/Avalonia.Browser.props" />

84
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<App>();
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<BrowserRenderingMode>(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;
}
}
}

5
samples/ControlCatalog.Browser/wwwroot/Logo.svg

@ -0,0 +1,5 @@
<svg viewBox="0 0 35 35" xmlns="http://www.w3.org/2000/svg">
<path d="M30.466 34.928h.21a4.256 4.256 0 0 0 4.23-3.753l.03-14.18C34.686 7.567 26.965 0 17.476 0 7.832 0 .014 7.82.014 17.463c0 9.542 7.652 17.3 17.155 17.464h13.297z" fill="#fff"/>
<path d="M17.524 5.948c-5.497 0-10.094 3.853-11.238 9.006a2.603 2.603 0 0 1 0 5.01c1.144 5.154 5.74 9.007 11.238 9.007 2.001 0 3.883-.51 5.522-1.409v1.33h5.99V17.938c.005-.146 0-.33 0-.477 0-6.358-5.154-11.511-11.512-11.511zM12.01 17.459a5.514 5.514 0 1 1 11.028 0 5.514 5.514 0 0 1-11.028 0z" clip-rule="evenodd" fill="#8B44AC" fill-rule="evenodd"/>
<path d="M7.368 17.452a1.842 1.842 0 1 1-3.684 0 1.842 1.842 0 0 1 3.684 0z" fill="#8B44AC"/>
</svg>

After

Width:  |  Height:  |  Size: 701 B

38
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;
}

0
samples/ControlCatalog.Browser/AppBundle/embed.js → samples/ControlCatalog.Browser/wwwroot/embed.js

0
samples/ControlCatalog.Browser/AppBundle/favicon.ico → samples/ControlCatalog.Browser/wwwroot/favicon.ico

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 172 KiB

46
samples/ControlCatalog.Browser/wwwroot/index.html

@ -0,0 +1,46 @@
<!DOCTYPE html>
<!-- Licensed to the .NET Foundation under one or more agreements. -->
<!-- The .NET Foundation licenses this file to you under the MIT license. -->
<html>
<head>
<title>AvaloniaUI - ControlCatalog</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./app.css" />
</head>
<body style="margin: 0; overflow: hidden">
<div id="out">
<div class="avalonia-splash">
<h2>
Powered by
<a href="https://www.avaloniaui.net/" target="_blank">Avalonia UI</a>
</h2>
<img src="Logo.svg" alt="Avalonia Logo" />
</div>
</div>
<!--<div id="common-root" style="display: grid; grid-template-columns: 50% 50%; position: absolute; width:100%; height: 100%">-->
<!-- <div id="out1" style="position:relative">-->
<!-- <div class="avalonia-splash">-->
<!-- <h2>-->
<!-- Powered by-->
<!-- <a href="https://www.avaloniaui.net/" target="_blank">Avalonia UI</a>-->
<!-- </h2>-->
<!-- <img src="Logo.svg" alt="Avalonia Logo" />-->
<!-- </div>-->
<!-- </div>-->
<!-- <div id="out2" style="position:relative">-->
<!-- <div class="avalonia-splash">-->
<!-- <h2>-->
<!-- Powered by-->
<!-- <a href="https://www.avaloniaui.net/" target="_blank">Avalonia UI</a>-->
<!-- </h2>-->
<!-- <img src="Logo.svg" alt="Avalonia Logo" />-->
<!-- </div>-->
<!-- </div>-->
<!--</div>-->
<script type='module' src="./main.js"></script>
</body>
</html>

6
samples/ControlCatalog.Browser/AppBundle/main.js → 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]);

8
samples/MobileSandbox/Platforms/Browser/wwwroot/Logo.svg

@ -1,5 +1,5 @@
<svg width="35" height="35" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M30.4661 34.928C30.5364 34.928 30.6052 34.928 30.6754 34.928C32.8596 34.928 34.654 33.2918 34.9053 31.1752L34.9356 16.9955C34.6872 7.56697 26.9662 0 17.4777 0C7.83263 0 0.0137329 7.8189 0.0137329 17.464C0.0137329 27.0059 7.66618 34.7631 17.1687 34.928H30.4661Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.5239 5.948C12.0268 5.948 7.42967 9.80117 6.286 14.954C7.38092 15.2609 8.18385 16.2664 8.18385 17.4593C8.18385 18.6523 7.38092 19.6577 6.286 19.9647C7.42966 25.1175 12.0268 28.9706 17.5239 28.9706C19.525 28.9706 21.4068 28.4601 23.0462 27.562V28.8927H29.0352V17.9365C29.0407 17.7908 29.0352 17.6063 29.0352 17.4593C29.0352 11.1018 23.8814 5.948 17.5239 5.948ZM12.0098 17.4593C12.0098 14.414 14.4786 11.9452 17.5239 11.9452C20.5693 11.9452 23.038 14.414 23.038 17.4593C23.038 20.5047 20.5693 22.9734 17.5239 22.9734C14.4786 22.9734 12.0098 20.5047 12.0098 17.4593Z" fill="#8B44AC"/>
<path d="M7.36841 17.4517C7.36841 18.4691 6.54368 19.2938 5.52631 19.2938C4.50894 19.2938 3.6842 18.4691 3.6842 17.4517C3.6842 16.4343 4.50894 15.6096 5.52631 15.6096C6.54368 15.6096 7.36841 16.4343 7.36841 17.4517Z" fill="#8B44AC"/>
<svg viewBox="0 0 35 35" xmlns="http://www.w3.org/2000/svg">
<path d="M30.466 34.928h.21a4.256 4.256 0 0 0 4.23-3.753l.03-14.18C34.686 7.567 26.965 0 17.476 0 7.832 0 .014 7.82.014 17.463c0 9.542 7.652 17.3 17.155 17.464h13.297z" fill="#fff"/>
<path d="M17.524 5.948c-5.497 0-10.094 3.853-11.238 9.006a2.603 2.603 0 0 1 0 5.01c1.144 5.154 5.74 9.007 11.238 9.007 2.001 0 3.883-.51 5.522-1.409v1.33h5.99V17.938c.005-.146 0-.33 0-.477 0-6.358-5.154-11.511-11.512-11.511zM12.01 17.459a5.514 5.514 0 1 1 11.028 0 5.514 5.514 0 0 1-11.028 0z" clip-rule="evenodd" fill="#8B44AC" fill-rule="evenodd"/>
<path d="M7.368 17.452a1.842 1.842 0 1 1-3.684 0 1.842 1.842 0 0 1 3.684 0z" fill="#8B44AC"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 708 B

84
samples/MobileSandbox/Platforms/Browser/wwwroot/app.css

@ -1,74 +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);
/* 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;
}
/* HTML styles for the splash screen */
.avalonia-splash h2 {
font-size: 1.5rem;
color: #8b44ac;
}
.highlight {
.avalonia-splash a {
color: white;
text-decoration: none;
font-size: 2.5rem;
display: block;
}
.purple {
color: #8b44ac;
}
.icon {
.avalonia-splash img {
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;
right: 3%;
bottom: 3%;
}
#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;
}
.avalonia-splash.splash-close {
transition: opacity 200ms, display 200ms;
display: none;
opacity: 0;
}

14
samples/MobileSandbox/Platforms/Browser/wwwroot/index.html

@ -12,15 +12,13 @@
<link rel="stylesheet" href="./app.css" />
</head>
<body style="margin: 0px; overflow: hidden">
<body style="margin: 0; overflow: hidden">
<div id="out">
<div id="avalonia-splash">
<div class="center">
<h2 class="purple">
Powered by
<a class="highlight" href="https://www.avaloniaui.net/" target="_blank">Avalonia UI</a>
</h2>
</div>
<div class="avalonia-splash">
<h2>
Powered by
<a href="https://www.avaloniaui.net/" target="_blank">Avalonia UI</a>
</h2>
<img class="icon" src="Logo.svg" alt="Avalonia Logo" />
</div>
</div>

1
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);

536
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<RawPointerPoint> 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;
/// <param name="divId">ID of the html element where avalonia content should be rendered.</param>
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."))
{
}
/// <param name="host">JSObject holding a div element where avalonia content should be rendered.</param>
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<SkiaOptions>();
_useGL = AvaloniaLocator.Current.GetService<IPlatformGraphics>() != 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<IReadOnlyList<RawPointerPoint>?>(() =>
{
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);
}
}
}

31
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
{
/// <summary>
/// Gets or sets Avalonia rendering modes with fallbacks.
/// The first element in the array has the highest priority.
/// </summary>
/// <exception cref="System.InvalidOperationException">Thrown if no values were matched.</exception>
public IReadOnlyList<BrowserRenderingMode> RenderingMode { get; set; } = new[]
{
BrowserRenderingMode.WebGL2, BrowserRenderingMode.WebGL1, BrowserRenderingMode.Software2D
};
/// <summary>
/// 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.
/// </summary>
public string? AvaloniaServiceWorkerScope { get; set; }
/// <summary>
/// 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
/// <param name="builder">Application builder.</param>
/// <param name="mainDivId">ID of the html element where avalonia content should be rendered.</param>
/// <param name="options">Browser backend specific options.</param>
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<BrowserPlatformOptions>().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)
{

6
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);
}
}
}

383
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<BrowserMouseDevice> _mouseDevices;
private IInputRoot? _inputRoot;
private static readonly PooledList<RawPointerPoint> 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<IReadOnlyList<RawPointerPoint>?>(() =>
{
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<IReadOnlyList<RawPointerPoint>?>? 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<IDragDropDevice>();
var eventArgs = new RawDragEvent(device, eventType, _inputRoot!, position, dataObject, dropEffect, modifiers);
_topLevelImpl.Input?.Invoke(eventArgs);
return eventArgs.Effects;
}
}

13
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

2
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
{

4
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)

3
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;

5
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.");
}
}

162
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;
}
}

203
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<BrowserMouseDevice> _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<object>();
_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<IReadOnlyList<RawPointerPoint>?>? 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<IDragDropDevice>();
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<IPlatformGraphics>());
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<object> Surfaces { get; set; }
public Action<string>? SetCssCursor { get; set; }
public Action<RawInputEventArgs>? Input { get; set; }
public Action<Rect>? Paint { get; set; }
public Action<Size, WindowResizeReason>? Resized { get; set; }
@ -248,10 +129,8 @@ namespace Avalonia.Browser
public Action<WindowTransparencyLevel>? 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;

64
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<JSType.Function>] Action renderFrameCallback);
[JSImport("CanvasFactory.onSizeChanged", AvaloniaModule.MainModuleName)]
public static partial void OnSizeChanged(
JSObject canvasSurface,
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Number, JSType.Number>>]
Action<int, int, double> onSizeChanged);
[JSImport("globalThis.setTimeout")]
public static partial int SetTimeout([JSMarshalAs<JSType.Function>] 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<JSType.Function>] 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<JSType.MemoryView>] ArraySegment<byte> data, int width, int height);
}

14
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<JSType.Function<JSType.Number, JSType.Number, JSType.Number>>]
Action<double, double, double> onSizeOrDpiChanged);
[JSImport("AvaloniaDOM.observeDarkMode", AvaloniaModule.MainModuleName)]
public static partial JSObject ObserveDarkMode(
[JSMarshalAs<JSType.Function<JSType.Boolean, JSType.Boolean>>]

23
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<JSType.Function<JSType.Number, JSType.Boolean>>] Func<double, bool> renderFrameCallback);
[JSImport("globalThis.setTimeout")]
public static partial int SetTimeout([JSMarshalAs<JSType.Function>] Action callback, int intervalMs);
[JSImport("globalThis.clearTimeout")]
public static partial int ClearTimeout(int id);
[JSImport("globalThis.setInterval")]
public static partial int SetInterval([JSMarshalAs<JSType.Function>] Action callback, int intervalMs);
[JSImport("globalThis.clearInterval")]
public static partial int ClearInterval(int id);
}

24
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;
/// <summary>
/// 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.
/// </summary>
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<IPlatformGraphics>());
internal static Compositor SoftwareUiCompositor => s_softwareUiCompositor ??= new Compositor(
new RenderLoop(BrowserUiRenderTimer), null);
}

51
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<SkiaOptions>();
_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);
}
}

98
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<byte> s_pool = ArrayPool<byte>.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<byte>(_array, 0, length);
}
public PixelSize Size { get; }
public int RowBytes { get; }
public IntPtr Address { get; }
public ArraySegment<byte> AsSegment { get; }
public void Dispose()
{
_handle.Free();
s_pool.Return(_array);
}
}
public IFramebufferRenderTarget CreateFramebufferRenderTarget() => new FuncFramebufferRenderTarget(Lock);
}

46
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<TimeSpan>? _tick;
public BrowserRenderTimer(bool isBackground)
{
RunsInBackground = isBackground;
}
public bool RunsInBackground { get; }
public event Action<TimeSpan>? 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;
}
}

102
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<BrowserPlatformOptions>() ?? 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();
}
}

12
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;

17
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()

23
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;
}
}

90
src/Browser/Avalonia.Browser/Skia/BrowserSkiaRasterSurface.cs

@ -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<IntPtr, SKSizeI> _blitCallback;
private readonly Action _onDisposeAction;
public BrowserSkiaRasterSurface(
SKColorType colorType, PixelSize size, double scaling, Action<IntPtr, SKSizeI> blitCallback)
{
ColorType = colorType;
Size = size;
Scaling = scaling;
_blitCallback = blitCallback;
_onDisposeAction = Blit;
}
public void Dispose()
{
_fbData?.Dispose();
_fbData = null;
}
public ILockedFramebuffer Lock()
{
var bytesPerPixel = 4; // TODO: derive from ColorType
var dpi = Scaling * 96.0;
var width = (int)(Size.Width * Scaling);
var height = (int)(Size.Height * Scaling);
if (_fbData is null || _fbData?.Size.Width != width || _fbData?.Size.Height != height)
{
_fbData?.Dispose();
_fbData = new FramebufferData(width, height, bytesPerPixel);
}
var pixelFormat = ColorType.ToPixelFormat();
var data = _fbData.Value;
return new LockedFramebuffer(
data.Address, data.Size, data.RowBytes,
new Vector(dpi, dpi), pixelFormat, _onDisposeAction);
}
private void Blit()
{
if (_fbData != null)
{
var data = _fbData.Value;
_blitCallback(data.Address, new SKSizeI(data.Size.Width, data.Size.Height));
}
}
private readonly struct FramebufferData
{
public PixelSize Size { get; }
public int RowBytes { get; }
public IntPtr Address { get; }
public FramebufferData(int width, int height, int bytesPerPixel)
{
Size = new PixelSize(width, height);
RowBytes = width * bytesPerPixel;
Address = Marshal.AllocHGlobal(width * height * bytesPerPixel);
}
public void Dispose()
{
Marshal.FreeHGlobal(Address);
}
}
public IFramebufferRenderTarget CreateFramebufferRenderTarget() => new FuncFramebufferRenderTarget(Lock);
}
}

33
src/Browser/Avalonia.Browser/Skia/BrowserSkiaSurface.cs

@ -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; }
}
}

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

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

1
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",

7
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
};

263
src/Browser/Avalonia.Browser/webapp/modules/avalonia/canvas.ts

@ -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<string, HTMLCanvasElement>;
htmlCanvas: HTMLCanvasElement;
glInfo?: SKGLViewInfo;
renderFrameCallback: () => void;
renderLoopEnabled: boolean = false;
renderLoopRequest: number = 0;
newWidth?: number;
newHeight?: number;
public static initGL(element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): SKGLViewInfo | null {
const view = Canvas.init(true, element, elementId, renderFrameCallback);
if (!view || !view.glInfo) {
return null;
}
return view.glInfo;
}
static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): Canvas | null {
const htmlCanvas = element as CanvasElement;
if (!htmlCanvas) {
console.error("No canvas element was provided.");
return null;
}
if (!Canvas.elements) {
Canvas.elements = new Map<string, HTMLCanvasElement>();
}
Canvas.elements.set(elementId, element);
const view = new Canvas(useGL, element, renderFrameCallback);
htmlCanvas.Canvas = view;
return view;
}
public constructor(useGL: boolean, element: HTMLCanvasElement, renderFrameCallback: () => void) {
this.htmlCanvas = element;
this.renderFrameCallback = renderFrameCallback;
if (useGL) {
const ctx = Canvas.createWebGLContext(element);
if (!ctx) {
console.error("Failed to create WebGL context");
return;
}
const GL = 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<Element, ResizeHandlerCallback>;
} & 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<Element, ResizeHandlerCallback>();
}
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);
}
}
}

60
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];
}

40
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;
}
}
}

58
src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/resizeHandler.ts

@ -0,0 +1,58 @@
type ResizeObserverWithCallbacks = {
callbacks: Map<Element, ((width: number, height: number, dpr: number) => 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<Element, ((width: number, height: number, dpr: number) => 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);
}
}
}

48
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);
}
}

18
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;
}

44
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);
}
}

58
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);
}
}

12
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);
}
}

330
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<RuntimeAPI>;
run(): Promise<number>;
}
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<T>(ident: string, returnType?: string | null, argTypes?: string[], args?: any[], opts?: any): T;
cwrap<T extends Function>(ident: string, returnType: string, argTypes?: string[], opts?: any): T;
cwrap<T extends Function>(ident: string, ...args: any[]): T;
@ -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<unknown>;
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<RuntimeAPI>;
run(): Promise<number>;
}
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<Response> | null | undefined;
interface LoadingResource {
name: string;
url: string;
response: Promise<Response>;
}
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<ArrayBuffer>;
/**
* If provided, runtime doesn't have to import it's JavaScript modules.
* This will not work for multi-threaded runtime.
*/
moduleExports?: any | Promise<any>;
/**
* It's metadata + fetch-like Promise<Response>
* If provided, the runtime doesn't have to initiate the download. It would just await the response.
*/
pendingDownload?: LoadingResource;
}
declare type AssetBehaviours = "resource" | "assembly" | "pdb" | "heap" | "icu" | "vfs" | "dotnetwasm" | "js-module-threads";
declare type GlobalizationMode = "icu" | // load ICU globalization data from any runtime assets with behavior "icu".
"invariant" | // operate in invariant globalization mode.
"auto";
declare type DotnetModuleConfig = {
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<void>;
onDotnetReady?: () => void | Promise<void>;
onDownloadResourceProgress?: (resourcesLoaded: number, totalResources: number) => void;
imports?: any;
exports?: string[];
downloadResource?: (request: ResourceRequest) => LoadingResource | undefined;
} & Partial<EmscriptenModule>;
declare type APIType = {
type APIType = {
runMain: (mainAssemblyName: string, args: string[]) => Promise<number>;
runMainAndExit: (mainAssemblyName: string, args: string[]) => Promise<number>;
setEnvironmentVariable: (name: string, value: string) => void;
getAssemblyExports(assemblyName: string): Promise<any>;
setModuleImports(moduleName: string, moduleImports: any): void;
getConfig: () => MonoConfig;
invokeLibraryInitializers: (functionName: string, args: any[]) => Promise<void>;
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<RuntimeAPI>;
declare type CreateDotnetRuntimeType = typeof createDotnetRuntime;
type CreateDotnetRuntimeType = (moduleFactory: DotnetModuleConfig | ((api: RuntimeAPI) => DotnetModuleConfig)) => Promise<RuntimeAPI>;
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 };

Loading…
Cancel
Save