diff --git a/samples/ControlCatalog.Browser/Logo.svg b/samples/ControlCatalog.Browser/AppBundle/Logo.svg
similarity index 100%
rename from samples/ControlCatalog.Browser/Logo.svg
rename to samples/ControlCatalog.Browser/AppBundle/Logo.svg
diff --git a/samples/ControlCatalog.Browser/AppBundle/app.css b/samples/ControlCatalog.Browser/AppBundle/app.css
new file mode 100644
index 0000000000..e14dfe4487
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/samples/ControlCatalog.Browser/embed.js b/samples/ControlCatalog.Browser/AppBundle/embed.js
similarity index 100%
rename from samples/ControlCatalog.Browser/embed.js
rename to samples/ControlCatalog.Browser/AppBundle/embed.js
diff --git a/samples/ControlCatalog.Browser/favicon.ico b/samples/ControlCatalog.Browser/AppBundle/favicon.ico
similarity index 100%
rename from samples/ControlCatalog.Browser/favicon.ico
rename to samples/ControlCatalog.Browser/AppBundle/favicon.ico
diff --git a/samples/ControlCatalog.Browser/AppBundle/index.html b/samples/ControlCatalog.Browser/AppBundle/index.html
new file mode 100644
index 0000000000..b35acaed5c
--- /dev/null
+++ b/samples/ControlCatalog.Browser/AppBundle/index.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+ AvaloniaUI - ControlCatalog
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+
diff --git a/samples/ControlCatalog.Browser/main.js b/samples/ControlCatalog.Browser/AppBundle/main.js
similarity index 68%
rename from samples/ControlCatalog.Browser/main.js
rename to samples/ControlCatalog.Browser/AppBundle/main.js
index 9d90db8bd2..9eae9fd740 100644
--- a/samples/ControlCatalog.Browser/main.js
+++ b/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]);
diff --git a/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj b/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj
index c4278459f3..6a406714c4 100644
--- a/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj
+++ b/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj
@@ -1,29 +1,15 @@
+
+
net7.0
browser-wasm
- main.js
+ AppBundle\main.js
Exe
true
true
- true
- -sVERBOSE -sERROR_ON_UNDEFINED_SYMBOLS=0
-
-
-
- true
- true
- full
- true
- true
- -O2
- -O2
-
-
-
-
@@ -31,14 +17,8 @@
-
-
-
-
-
-
+
-
diff --git a/samples/ControlCatalog.Browser/EmbedSample.Browser.cs b/samples/ControlCatalog.Browser/EmbedSample.Browser.cs
index c367230ddf..1bd226d578 100644
--- a/samples/ControlCatalog.Browser/EmbedSample.Browser.cs
+++ b/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;
}
diff --git a/samples/ControlCatalog.Browser/Program.cs b/samples/ControlCatalog.Browser/Program.cs
index e1a4500173..919df5103c 100644
--- a/samples/ControlCatalog.Browser/Program.cs
+++ b/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();
diff --git a/samples/ControlCatalog.Browser/Roots.xml b/samples/ControlCatalog.Browser/Roots.xml
deleted file mode 100644
index b07fd86fa2..0000000000
--- a/samples/ControlCatalog.Browser/Roots.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
diff --git a/samples/ControlCatalog.Browser/app.css b/samples/ControlCatalog.Browser/app.css
deleted file mode 100644
index 0e6ab12461..0000000000
--- a/samples/ControlCatalog.Browser/app.css
+++ /dev/null
@@ -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;
- }
-}
diff --git a/samples/ControlCatalog.Browser/index.html b/samples/ControlCatalog.Browser/index.html
deleted file mode 100644
index 226ae70695..0000000000
--- a/samples/ControlCatalog.Browser/index.html
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-
- AvaloniaUI - ControlCatalog
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Avalonia.Base/Logging/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs
index f15e87da1b..18bd21b664 100644
--- a/src/Avalonia.Base/Logging/LogArea.cs
+++ b/src/Avalonia.Base/Logging/LogArea.cs
@@ -74,5 +74,10 @@ namespace Avalonia.Logging
/// The log event comes from macOS Platform
///
public const string macOSPlatform = nameof(macOSPlatform);
+
+ ///
+ /// The log event comes from Browser Platform
+ ///
+ public static string BrowserPlatform => nameof(BrowserPlatform);
}
}
diff --git a/src/Avalonia.Base/Media/MediaContext.Compositor.cs b/src/Avalonia.Base/Media/MediaContext.Compositor.cs
index feb6fee8d6..41585ddc51 100644
--- a/src/Avalonia.Base/Media/MediaContext.Compositor.cs
+++ b/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() == null)
return;
-
+
if (compositor is
{
UseUiThreadForSynchronousCommits: false,
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
index ca00b94eaf..52892b379b 100644
--- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
+++ b/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);
}
}
diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs
index ca4dfe5ed3..24817d7865 100644
--- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs
@@ -108,7 +108,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();
}
diff --git a/src/Browser/Avalonia.Browser/Avalonia.Browser.props b/src/Browser/Avalonia.Browser/Avalonia.Browser.props
index 668dd20789..1cff1c8be5 100644
--- a/src/Browser/Avalonia.Browser/Avalonia.Browser.props
+++ b/src/Browser/Avalonia.Browser/Avalonia.Browser.props
@@ -1,5 +1,7 @@
- 16384000
+ True
+ True
+ True
diff --git a/src/Browser/Avalonia.Browser/Avalonia.Browser.targets b/src/Browser/Avalonia.Browser/Avalonia.Browser.targets
index 22363b33d8..7c396743e7 100644
--- a/src/Browser/Avalonia.Browser/Avalonia.Browser.targets
+++ b/src/Browser/Avalonia.Browser/Avalonia.Browser.targets
@@ -1,37 +1,33 @@
-
-
-
-
-
-
- True
$(EmccExtraLDFlags) --js-library="$(MSBuildThisFileDirectory)\interop.js"
$(EmccExtraLDFlags) -sERROR_ON_UNDEFINED_SYMBOLS=0
- true
-
- true
- full
- true
- -Oz
- -Oz
- false
- false
- 0
- false
- true
- false
- false
- false
- false
- false
- false
- true
- true
- en
- false
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Browser/Avalonia.Browser/AvaloniaView.cs b/src/Browser/Avalonia.Browser/AvaloniaView.cs
index dde4746394..86323afbbd 100644
--- a/src/Browser/Avalonia.Browser/AvaloniaView.cs
+++ b/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();
}
diff --git a/src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs b/src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs
new file mode 100644
index 0000000000..6ee6c719a7
--- /dev/null
+++ b/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);
+ }
+ }
+}
diff --git a/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs b/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs
index 27a2b1dcb7..9e182eaa09 100644
--- a/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs
+++ b/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs
@@ -39,4 +39,16 @@ internal static partial class CanvasHelper
JSObject canvas,
string canvasId,
[JSMarshalAs] Action renderFrameCallback);
+
+ [JSImport("globalThis.setTimeout")]
+ public static partial int SetTimeout([JSMarshalAs] Action callback, int intervalMs);
+
+ [JSImport("globalThis.clearTimeout")]
+ public static partial int ClearTimeout(int id);
+
+ [JSImport("globalThis.setInterval")]
+ public static partial int SetInterval([JSMarshalAs] Action callback, int intervalMs);
+
+ [JSImport("globalThis.clearInterval")]
+ public static partial int ClearInterval(int id);
}
diff --git a/src/Browser/Avalonia.Browser/WindowingPlatform.cs b/src/Browser/Avalonia.Browser/WindowingPlatform.cs
index a23cd01910..2db0e2aec3 100644
--- a/src/Browser/Avalonia.Browser/WindowingPlatform.cs
+++ b/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().ToSingleton()
- .Bind().ToSingleton()
- .Bind().ToConstant(s_keyboard)
- .Bind().ToSingleton()
- .Bind().ToConstant(instance)
- .Bind().ToConstant(ManualTriggerRenderTimer.Instance)
- .Bind().ToConstant(instance)
- .Bind().ToConstant(new BrowserSkiaGraphics())
- .Bind().ToSingleton()
- .Bind().ToSingleton();
+namespace Avalonia.Browser;
- if (AvaloniaLocator.Current.GetService() 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().ToSingleton()
+ .Bind().ToSingleton()
+ .Bind().ToConstant(s_keyboard)
+ .Bind().ToSingleton()
+ .Bind().ToSingleton()
+ .Bind().ToConstant(ManualTriggerRenderTimer.Instance)
+ .Bind().ToConstant(instance)
+ .Bind().ToConstant(new BrowserSkiaGraphics())
+ .Bind().ToSingleton()
+ .Bind().ToSingleton();
+
+ if (AvaloniaLocator.Current.GetService() is { } options
+ && options.RegisterAvaloniaServiceWorker)
{
- get
- {
- return true; // Browser is single threaded.
- }
+ var swPath = AvaloniaModule.ResolveServiceWorkerPath();
+ AvaloniaModule.RegisterServiceWorker(swPath, options.AvaloniaServiceWorkerScope);
}
-
- public event Action? Signaled;
}
}