Browse Source

Make Avalonia.Browser work on .NET 8 (#13312)

* Update SkiaSharp/HarfBuzzSharp and use proper native bits, make it configurable

* Run UiThreadRender jobs on each animation request in WASM

* Update ControlCatalog to match our current templates

* Add TaskContinuationOptions.ExecuteSynchronously in compositing engine where it's missed

* Log invalid rendering configuration

* Use setInterval isntead of Timer on WASM

* Minor fixes

* Implement BrowserDispatcherImpl and avoid possible memory leak
release/11.0.6
Max Katz 2 years ago
parent
commit
f3cee957dc
  1. 0
      samples/ControlCatalog.Browser/AppBundle/Logo.svg
  2. 74
      samples/ControlCatalog.Browser/AppBundle/app.css
  3. 0
      samples/ControlCatalog.Browser/AppBundle/embed.js
  4. 0
      samples/ControlCatalog.Browser/AppBundle/favicon.ico
  5. 28
      samples/ControlCatalog.Browser/AppBundle/index.html
  6. 5
      samples/ControlCatalog.Browser/AppBundle/main.js
  7. 28
      samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj
  8. 3
      samples/ControlCatalog.Browser/EmbedSample.Browser.cs
  9. 6
      samples/ControlCatalog.Browser/Program.cs
  10. 7
      samples/ControlCatalog.Browser/Roots.xml
  11. 56
      samples/ControlCatalog.Browser/app.css
  12. 31
      samples/ControlCatalog.Browser/index.html
  13. 5
      src/Avalonia.Base/Logging/LogArea.cs
  14. 7
      src/Avalonia.Base/Media/MediaContext.Compositor.cs
  15. 2
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  16. 3
      src/Avalonia.Base/Rendering/Composition/Compositor.cs
  17. 4
      src/Browser/Avalonia.Browser/Avalonia.Browser.props
  18. 54
      src/Browser/Avalonia.Browser/Avalonia.Browser.targets
  19. 9
      src/Browser/Avalonia.Browser/AvaloniaView.cs
  20. 64
      src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs
  21. 12
      src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs
  22. 112
      src/Browser/Avalonia.Browser/WindowingPlatform.cs

0
samples/ControlCatalog.Browser/Logo.svg → samples/ControlCatalog.Browser/AppBundle/Logo.svg

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

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

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

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

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

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 172 KiB

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

@ -0,0 +1,28 @@
<!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/main.js → samples/ControlCatalog.Browser/AppBundle/main.js

@ -1,7 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
import { dotnet } from './dotnet.js'
import { dotnet } from './dotnet.js' // NET 7
//import { dotnet } from './_framework/dotnet.js' // NET 8+
const is_browser = typeof window != "undefined";
if (!is_browser) throw new Error(`Expected to be running in a browser`);
@ -13,4 +14,4 @@ const dotnetRuntime = await dotnet
const config = dotnetRuntime.getConfig();
await dotnetRuntime.runMainAndExit(config.mainAssemblyName, ["dotnet", "is", "great!"]);
await dotnetRuntime.runMainAndExit(config.mainAssemblyName, [globalThis.location.href]);

28
samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj

@ -1,29 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\src\Browser\Avalonia.Browser\Avalonia.Browser.props" />
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<WasmMainJSPath>main.js</WasmMainJSPath>
<WasmMainJSPath>AppBundle\main.js</WasmMainJSPath>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver>
<WasmBuildNative>true</WasmBuildNative>
<EmccFlags>-sVERBOSE -sERROR_ON_UNDEFINED_SYMBOLS=0</EmccFlags>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<RunAOTCompilation>true</RunAOTCompilation>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>
<WasmBuildNative>true</WasmBuildNative>
<InvariantGlobalization>true</InvariantGlobalization>
<EmccCompileOptimizationFlag>-O2</EmccCompileOptimizationFlag>
<EmccLinkOptimizationFlag>-O2</EmccLinkOptimizationFlag>
</PropertyGroup>
<ItemGroup>
<TrimmerRootDescriptor Include="Roots.xml" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
<ProjectReference Include="..\..\src\Browser\Avalonia.Browser\Avalonia.Browser.csproj" />
@ -31,14 +17,8 @@
</ItemGroup>
<ItemGroup>
<WasmExtraFilesToDeploy Include="index.html" />
<WasmExtraFilesToDeploy Include="main.js" />
<WasmExtraFilesToDeploy Include="embed.js" />
<WasmExtraFilesToDeploy Include="favicon.ico" />
<WasmExtraFilesToDeploy Include="Logo.svg" />
<WasmExtraFilesToDeploy Include="app.css" />
<WasmExtraFilesToDeploy Include="AppBundle\**" />
</ItemGroup>
<Import Project="..\..\src\Browser\Avalonia.Browser\Avalonia.Browser.props" />
<Import Project="..\..\src\Browser\Avalonia.Browser\Avalonia.Browser.targets" />
</Project>

3
samples/ControlCatalog.Browser/EmbedSample.Browser.cs

@ -4,6 +4,7 @@ using Avalonia.Platform;
using Avalonia.Browser;
using ControlCatalog.Pages;
using System.Threading.Tasks;
namespace ControlCatalog.Browser;
@ -25,7 +26,7 @@ public class EmbedSampleWeb : INativeDemoControl
_ = JSHost.ImportAsync("embed.js", "./embed.js").ContinueWith(_ =>
{
EmbedInterop.AddAppButton(defaultHandle.Object);
});
}, TaskScheduler.FromCurrentSynchronizationContext());
return defaultHandle;
}

6
samples/ControlCatalog.Browser/Program.cs

@ -1,8 +1,9 @@
using System.Diagnostics;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Browser;
using Avalonia.Controls;
using Avalonia.Logging;
using ControlCatalog;
using ControlCatalog.Browser;
@ -12,7 +13,10 @@ internal partial class Program
{
public static async Task Main(string[] args)
{
Trace.Listeners.Add(new ConsoleTraceListener());
await BuildAvaloniaApp()
.LogToTrace(LogEventLevel.Warning)
.AfterSetup(_ =>
{
ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb();

7
samples/ControlCatalog.Browser/Roots.xml

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

56
samples/ControlCatalog.Browser/app.css

@ -1,56 +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);
}
#out {
height: 100vh;
width: 100vw
}
#avalonia-splash {
position: relative;
height: 100%;
width: 100%;
color: whitesmoke;
background: #171C2C;
font-family: 'Nunito', sans-serif;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
}
#avalonia-splash a{
color: whitesmoke;
text-decoration: none;
}
.center {
display: flex;
justify-content: center;
height: 250px;
}
.splash-close {
animation: slide 0.5s linear 1s forwards;
}
@keyframes slide {
0% {
top: 0%;
}
50% {
opacity: 80%;
}
100% {
top: 100%;
overflow: hidden;
opacity: 0;
display: none;
visibility: collapse;
}
}

31
samples/ControlCatalog.Browser/index.html

@ -1,31 +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="modulepreload" href="./main.js" />
<link rel="modulepreload" href="./dotnet.js" />
<link rel="modulepreload" href="./avalonia.js" />
<link rel="stylesheet" href="./app.css" />
</head>
<body style="margin: 0px">
<div id="out">
<div id="avalonia-splash">
<div class="center">
<h2>Powered by</h2>
<a class="navbar-brand" href="https://www.avaloniaui.net/" target="_blank">
<img src="Logo.svg" alt="Avalonia Logo" width="30" height="24" />
Avalonia
</a>
</div>
</div>
</div>
<script type='module' src="./main.js"></script>
</body>
</html>

5
src/Avalonia.Base/Logging/LogArea.cs

@ -74,5 +74,10 @@ namespace Avalonia.Logging
/// The log event comes from macOS Platform
/// </summary>
public const string macOSPlatform = nameof(macOSPlatform);
/// <summary>
/// The log event comes from Browser Platform
/// </summary>
public static string BrowserPlatform => nameof(BrowserPlatform);
}
}

7
src/Avalonia.Base/Media/MediaContext.Compositor.cs

@ -1,4 +1,6 @@
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Platform;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Transport;
@ -20,7 +22,8 @@ partial class MediaContext
_requestedCommits.Remove(compositor);
_pendingCompositionBatches[compositor] = commit;
commit.Processed.ContinueWith(_ =>
_dispatcher.Post(() => CompositionBatchFinished(compositor, commit), DispatcherPriority.Send));
_dispatcher.Post(() => CompositionBatchFinished(compositor, commit), DispatcherPriority.Send),
TaskContinuationOptions.ExecuteSynchronously);
return commit;
}
@ -93,7 +96,7 @@ partial class MediaContext
// Unit tests are assuming that they can call any API without setting up platforms
if (AvaloniaLocator.Current.GetService<IPlatformRenderInterface>() == null)
return;
if (compositor is
{
UseUiThreadForSynchronousCommits: false,

2
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@ -183,7 +183,7 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester
{
_queuedSceneInvalidation = false;
SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize)));
}, DispatcherPriority.Input));
}, DispatcherPriority.Input), TaskContinuationOptions.ExecuteSynchronously);
}
}

3
src/Avalonia.Base/Rendering/Composition/Compositor.cs

@ -107,7 +107,8 @@ namespace Avalonia.Rendering.Composition
var pending = _pendingBatch;
if (pending != null)
pending.Processed.ContinueWith(
_ => Dispatcher.Post(_triggerCommitRequested, DispatcherPriority.Send));
_ => Dispatcher.Post(_triggerCommitRequested, DispatcherPriority.Send),
TaskContinuationOptions.ExecuteSynchronously);
else
_triggerCommitRequested();
}

4
src/Browser/Avalonia.Browser/Avalonia.Browser.props

@ -1,5 +1,7 @@
<Project>
<PropertyGroup>
<EmccInitialHeapSize>16384000</EmccInitialHeapSize> <!-- must be a multiple of 64KiB, 1024000 * num MB, number grows -->
<ShouldIncludeAvaloniaJavaScript Condition=" '$(ShouldIncludeAvaloniaJavaScript)' == '' ">True</ShouldIncludeAvaloniaJavaScript>
<ShouldIncludeNativeSkiaSharp Condition=" '$(ShouldIncludeNativeSkiaSharp)' == '' ">True</ShouldIncludeNativeSkiaSharp>
<ShouldIncludeNativeHarfBuzzSharp Condition=" '$(ShouldIncludeNativeHarfBuzzSharp)' == '' ">True</ShouldIncludeNativeHarfBuzzSharp>
</PropertyGroup>
</Project>

54
src/Browser/Avalonia.Browser/Avalonia.Browser.targets

@ -1,37 +1,33 @@
<Project>
<ItemGroup>
<WasmExtraFilesToDeploy Include="$(MSBuildThisFileDirectory)/wwwroot/**/*.*" />
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\3.1.7\libHarfBuzzSharp.a" />
<NativeFileReference Include="$(SkiaSharpStaticLibraryPath)\3.1.7\libSkiaSharp.a" />
</ItemGroup>
<PropertyGroup>
<UseAvaloniaWasmDefaultOptimizations Condition="'$(UseAvaloniaWasmDefaultOptimizations)'==''">True</UseAvaloniaWasmDefaultOptimizations>
<EmccExtraLDFlags>$(EmccExtraLDFlags) --js-library="$(MSBuildThisFileDirectory)\interop.js"</EmccExtraLDFlags>
<EmccFlags>$(EmccExtraLDFlags) -sERROR_ON_UNDEFINED_SYMBOLS=0</EmccFlags>
<WasmBuildNative>true</WasmBuildNative>
</PropertyGroup>
<PropertyGroup Condition="'$(UseAvaloniaWasmDefaultOptimizations)'=='True' And '$(Configuration)' == 'Release'">
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>
<InvariantGlobalization>true</InvariantGlobalization>
<EmccCompileOptimizationFlag>-Oz</EmccCompileOptimizationFlag>
<EmccLinkOptimizationFlag>-Oz</EmccLinkOptimizationFlag>
<WasmEmitSymbolMap>false</WasmEmitSymbolMap>
<WasmNativeDebugSymbols>false</WasmNativeDebugSymbols>
<WasmDebugLevel>0</WasmDebugLevel>
<WasmEnableExceptionHandling>false</WasmEnableExceptionHandling>
<TrimmerRemoveSymbols>true</TrimmerRemoveSymbols>
<DebuggerSupport>false</DebuggerSupport>
<EnableUnsafeBinaryFormatterSerialization>false</EnableUnsafeBinaryFormatterSerialization>
<EnableUnsafeUTF7Encoding>false</EnableUnsafeUTF7Encoding>
<EventSourceSupport>false</EventSourceSupport>
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
<MetadataUpdaterSupport>false</MetadataUpdaterSupport>
<UseNativeHttpHandler>true</UseNativeHttpHandler>
<UseSystemResourceKeys>true</UseSystemResourceKeys>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<IncludeSatelliteDllsProjectOutputGroup>false</IncludeSatelliteDllsProjectOutputGroup>
<ItemGroup>
<WasmExtraFilesToDeploy Condition="'$(WasmRuntimeAssetsLocation)' == ''" Include="$(MSBuildThisFileDirectory)\wwwroot\**\*.*" />
<WasmExtraFilesToDeploy Condition="'$(WasmRuntimeAssetsLocation)' != ''" Include="$(MSBuildThisFileDirectory)\wwwroot\**\*.*" TargetPath="$(WasmRuntimeAssetsLocation)\%(FileName)%(Extension)" />
</ItemGroup>
<PropertyGroup Condition="'$(ShouldIncludeNativeSkiaSharp)' == 'True' or '$(ShouldIncludeNativeHarfBuzzSharp)' == 'True'">
<WasmBuildNative Condition="'$(WasmBuildNative)' == ''">true</WasmBuildNative>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFrameworkVersion)' != '' and '$(ShouldIncludeNativeSkiaSharp)' == 'True'">
<!-- net7.0 -->
<NativeFileReference Include="$(SkiaSharpStaticLibraryPath)\3.1.12\mt\*.a" Condition="!$([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '8.0')) and '$(WasmEnableThreads)' == 'True'" />
<NativeFileReference Include="$(SkiaSharpStaticLibraryPath)\3.1.12\st\*.a" Condition="!$([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '8.0')) and '$(WasmEnableThreads)' != 'True'" />
<!-- net8.0 -->
<NativeFileReference Include="$(SkiaSharpStaticLibraryPath)\3.1.34\mt\*.a" Condition="$([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '8.0')) and '$(WasmEnableThreads)' == 'True'" />
<NativeFileReference Include="$(SkiaSharpStaticLibraryPath)\3.1.34\st\*.a" Condition="$([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '8.0')) and '$(WasmEnableThreads)' != 'True'" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFrameworkVersion)' != '' and '$(ShouldIncludeNativeHarfBuzzSharp)' == 'True'">
<!-- net7.0 -->
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\3.1.12\st\*.a" Condition="!$([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '8.0')) and '$(WasmEnableThreads)' != 'True'" />
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\3.1.12\mt\*.a" Condition="!$([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '8.0')) and '$(WasmEnableThreads)' == 'True'" />
<!-- net8.0 -->
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\3.1.34\st\*.a" Condition="$([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '8.0')) and '$(WasmEnableThreads)' != 'True'" />
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\3.1.34\mt\*.a" Condition="$([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '8.0')) and '$(WasmEnableThreads)' == 'True'" />
</ItemGroup>
</Project>

9
src/Browser/Avalonia.Browser/AvaloniaView.cs

@ -12,6 +12,7 @@ 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;
@ -138,11 +139,8 @@ namespace Avalonia.Browser
}
else
{
//var rasterInitialized = _interop.InitRaster();
//Console.WriteLine("raster initialized: {0}", rasterInitialized);
//_topLevelImpl.SetSurface(ColorType,
// new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData);
Logger.TryGet(LogEventLevel.Error, LogArea.BrowserPlatform)?
.Log(this, "[Avalonia]: Unable to initialize Canvas surface.");
}
CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
@ -442,6 +440,7 @@ namespace Avalonia.Browser
return;
}
Dispatcher.UIThread.RunJobs(DispatcherPriority.UiThreadRender);
ManualTriggerRenderTimer.Instance.RaiseTick();
}

64
src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs

@ -0,0 +1,64 @@
using System;
using System.Diagnostics;
using System.Threading;
using Avalonia.Browser.Interop;
using Avalonia.Threading;
namespace Avalonia.Browser;
internal class BrowserDispatcherImpl : IDispatcherImpl
{
private readonly Thread _thread;
private readonly Stopwatch _clock;
private bool _signaled;
private int? _timerId;
private readonly Action _timerCallback;
private readonly Action _signalCallback;
public BrowserDispatcherImpl()
{
_thread = Thread.CurrentThread;
_clock = Stopwatch.StartNew();
_timerCallback = () => Timer?.Invoke();
_signalCallback = () =>
{
_signaled = false;
Signaled?.Invoke();
};
}
public bool CurrentThreadIsLoopThread => Thread.CurrentThread == _thread;
public long Now => _clock.ElapsedMilliseconds;
public event Action? Signaled;
public event Action? Timer;
public void Signal()
{
if (_signaled)
return;
// 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);
}
public void UpdateTimer(long? dueTimeInMs)
{
if (_timerId is { } timerId)
{
_timerId = null;
CanvasHelper.ClearInterval(timerId);
}
if (dueTimeInMs.HasValue)
{
var interval = Math.Max(1, dueTimeInMs.Value - _clock.ElapsedMilliseconds);
_timerId = CanvasHelper.SetInterval(_timerCallback, (int)interval);
}
}
}

12
src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs

@ -39,4 +39,16 @@ internal static partial class CanvasHelper
JSObject canvas,
string canvasId,
[JSMarshalAs<JSType.Function>] Action 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);
}

112
src/Browser/Avalonia.Browser/WindowingPlatform.cs

@ -1,5 +1,4 @@
using System;
using System.Threading;
using Avalonia.Browser.Interop;
using Avalonia.Browser.Skia;
using Avalonia.Input;
@ -8,88 +7,49 @@ using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Threading;
namespace Avalonia.Browser
{
internal class BrowserWindowingPlatform : IWindowingPlatform, IPlatformThreadingInterface
{
private bool _signaled;
private static KeyboardDevice? s_keyboard;
public IWindowImpl CreateWindow() => throw new NotSupportedException("Browser doesn't support windowing platform. In order to display a single-view content, set ISingleViewApplicationLifetime.MainView.");
IWindowImpl IWindowingPlatform.CreateEmbeddableWindow()
{
throw new NotImplementedException("Browser doesn't support embeddable windowing platform.");
}
public ITrayIconImpl? CreateTrayIcon()
{
return null;
}
public static KeyboardDevice Keyboard => s_keyboard ??
throw new InvalidOperationException("BrowserWindowingPlatform not registered.");
public static void Register()
{
var instance = new BrowserWindowingPlatform();
s_keyboard = new KeyboardDevice();
AvaloniaLocator.CurrentMutable
.Bind<IRuntimePlatform>().ToSingleton<BrowserRuntimePlatform>()
.Bind<ICursorFactory>().ToSingleton<CssCursorFactory>()
.Bind<IKeyboardDevice>().ToConstant(s_keyboard)
.Bind<IPlatformSettings>().ToSingleton<BrowserPlatformSettings>()
.Bind<IPlatformThreadingInterface>().ToConstant(instance)
.Bind<IRenderTimer>().ToConstant(ManualTriggerRenderTimer.Instance)
.Bind<IWindowingPlatform>().ToConstant(instance)
.Bind<IPlatformGraphics>().ToConstant(new BrowserSkiaGraphics())
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();
namespace Avalonia.Browser;
if (AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() is { } options
&& options.RegisterAvaloniaServiceWorker)
{
var swPath = AvaloniaModule.ResolveServiceWorkerPath();
AvaloniaModule.RegisterServiceWorker(swPath, options.AvaloniaServiceWorkerScope);
}
}
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
{
return new Timer(_ =>
{
Dispatcher.UIThread.RunJobs(priority);
tick();
}, null, interval, interval);
}
internal class BrowserWindowingPlatform : IWindowingPlatform
{
private static KeyboardDevice? s_keyboard;
public void Signal(DispatcherPriority priority)
{
if (_signaled)
return;
public IWindowImpl CreateWindow() => throw new NotSupportedException("Browser doesn't support windowing platform. In order to display a single-view content, set ISingleViewApplicationLifetime.MainView.");
_signaled = true;
var interval = TimeSpan.FromMilliseconds(1);
IWindowImpl IWindowingPlatform.CreateEmbeddableWindow()
{
throw new NotImplementedException("Browser doesn't support embeddable windowing platform.");
}
IDisposable? disp = null;
disp = new Timer(_ =>
{
_signaled = false;
disp?.Dispose();
public ITrayIconImpl? CreateTrayIcon()
{
return null;
}
Signaled?.Invoke(null);
}, null, interval, interval);
}
public static KeyboardDevice Keyboard => s_keyboard ??
throw new InvalidOperationException("BrowserWindowingPlatform not registered.");
public bool CurrentThreadIsLoopThread
public static void Register()
{
var instance = new BrowserWindowingPlatform();
s_keyboard = new KeyboardDevice();
AvaloniaLocator.CurrentMutable
.Bind<IRuntimePlatform>().ToSingleton<BrowserRuntimePlatform>()
.Bind<ICursorFactory>().ToSingleton<CssCursorFactory>()
.Bind<IKeyboardDevice>().ToConstant(s_keyboard)
.Bind<IPlatformSettings>().ToSingleton<BrowserPlatformSettings>()
.Bind<IDispatcherImpl>().ToSingleton<BrowserDispatcherImpl>()
.Bind<IRenderTimer>().ToConstant(ManualTriggerRenderTimer.Instance)
.Bind<IWindowingPlatform>().ToConstant(instance)
.Bind<IPlatformGraphics>().ToConstant(new BrowserSkiaGraphics())
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();
if (AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() is { } options
&& options.RegisterAvaloniaServiceWorker)
{
get
{
return true; // Browser is single threaded.
}
var swPath = AvaloniaModule.ResolveServiceWorkerPath();
AvaloniaModule.RegisterServiceWorker(swPath, options.AvaloniaServiceWorkerScope);
}
public event Action<DispatcherPriority?>? Signaled;
}
}

Loading…
Cancel
Save