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 + + + + + + +
+
+
+

+ Powered by + Avalonia UI +

+
+ Avalonia Logo +
+
+ + + + 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 - - - - - - - - - -
-
-
-

Powered by

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