diff --git a/src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs b/src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs index d7eaa81abf..2d3d031c9b 100644 --- a/src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs +++ b/src/Browser/Avalonia.Browser/BrowserDispatcherImpl.cs @@ -14,16 +14,17 @@ internal class BrowserDispatcherImpl : IDispatcherImpl 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 = () => + TimerHelper.Interval += () => + { + Timer?.Invoke(); + }; + + TimerHelper.Timeout = () => { _signaled = false; Signaled?.Invoke(); @@ -44,7 +45,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; - TimerHelper.SetTimeout(_signalCallback, interval); + TimerHelper.SetTimeout(interval); } public void UpdateTimer(long? dueTimeInMs) @@ -58,7 +59,7 @@ internal class BrowserDispatcherImpl : IDispatcherImpl if (dueTimeInMs.HasValue) { var interval = Math.Max(1, dueTimeInMs.Value - _clock.ElapsedMilliseconds); - _timerId = TimerHelper.SetInterval(_timerCallback, (int)interval); + _timerId = TimerHelper.SetInterval((int)interval); } } } diff --git a/src/Browser/Avalonia.Browser/Interop/TimerHelper.cs b/src/Browser/Avalonia.Browser/Interop/TimerHelper.cs index 32c2f66565..aff472e232 100644 --- a/src/Browser/Avalonia.Browser/Interop/TimerHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/TimerHelper.cs @@ -6,17 +6,37 @@ namespace Avalonia.Browser.Interop; internal static partial class TimerHelper { [JSImport("TimerHelper.runAnimationFrames", AvaloniaModule.MainModuleName)] - public static partial void RunAnimationFrames( - [JSMarshalAs>] Func renderFrameCallback); + public static partial void RunAnimationFrames(); - [JSImport("globalThis.setTimeout")] - public static partial int SetTimeout([JSMarshalAs] Action callback, int intervalMs); + public static Action? AnimationFrame; + [JSExport] + public static void JsExportOnAnimationFrame(double d) + { + AnimationFrame?.Invoke(d); + } + + public static Action? Timeout; + [JSExport] + public static void JsExportOnTimeout() + { + Timeout?.Invoke(); + } + + [JSImport("TimerHelper.setTimeout", AvaloniaModule.MainModuleName)] + public static partial int SetTimeout(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); + public static Action? Interval; + [JSExport] + public static void JsExportOnInterval() + { + Interval?.Invoke(); + } + + [JSImport("TimerHelper.setInterval", AvaloniaModule.MainModuleName)] + public static partial int SetInterval( int intervalMs); [JSImport("globalThis.clearInterval")] public static partial int ClearInterval(int id); diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs index 55f42d0df7..fea94d0248 100644 --- a/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs +++ b/src/Browser/Avalonia.Browser/Rendering/BrowserRenderTimer.cs @@ -38,18 +38,16 @@ internal class BrowserRenderTimer : IRenderTimer if (!_started) { _started = true; - TimerHelper.RunAnimationFrames(RenderFrameCallback); + TimerHelper.AnimationFrame += RenderFrameCallback; + TimerHelper.RunAnimationFrames(); } } - private bool RenderFrameCallback(double timestamp) + private void RenderFrameCallback(double timestamp) { if (_tick is { } tick) { tick.Invoke(TimeSpan.FromMilliseconds(timestamp)); - return true; } - - return false; } } diff --git a/src/Browser/Avalonia.Browser/Rendering/RenderTargetBrowserSurface.cs b/src/Browser/Avalonia.Browser/Rendering/RenderTargetBrowserSurface.cs index bbd113b75a..bfbf7cdce0 100644 --- a/src/Browser/Avalonia.Browser/Rendering/RenderTargetBrowserSurface.cs +++ b/src/Browser/Avalonia.Browser/Rendering/RenderTargetBrowserSurface.cs @@ -62,7 +62,7 @@ internal class RenderTargetBrowserSurface : BrowserSurface public BrowserRenderTarget? Target => _target ??= BrowserRenderTarget.GetRenderTarget(_targetId, () => CanvasSize); - public bool IsReady => Target != null; + public bool IsReady => Target != null && CanvasSize.Size != default; public bool UsesContexts => Target!.PlatformGraphicsContext != null; public bool UsesSharedContext => UsesContexts; public (PixelSize Size, double Scaling) CanvasSize { get; set; } @@ -95,8 +95,7 @@ internal class RenderTargetBrowserSurface : BrowserSurface public static RenderTargetBrowserSurface Create(JSObject container, IReadOnlyList modes) { - // TODO: Get thread id from JSWebWorker - var js = CanvasHelper.CreateRenderTargetSurface(container, modes.Select(m => (int)m).ToArray(), 0); + var js = CanvasHelper.CreateRenderTargetSurface(container, modes.Select(m => (int)m).ToArray(), RenderWorker.WorkerThreadId); return new RenderTargetBrowserSurface(js); } } \ No newline at end of file diff --git a/src/Browser/Avalonia.Browser/WindowingPlatform.cs b/src/Browser/Avalonia.Browser/WindowingPlatform.cs index 5ecaa1bb84..af78d616f4 100644 --- a/src/Browser/Avalonia.Browser/WindowingPlatform.cs +++ b/src/Browser/Avalonia.Browser/WindowingPlatform.cs @@ -4,6 +4,7 @@ using System.Threading; using Avalonia.Browser.Interop; using Avalonia.Browser.Skia; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Platform; @@ -64,12 +65,12 @@ internal class BrowserWindowingPlatform : IWindowingPlatform .Bind().ToSingleton() .Bind().ToConstant(s_keyboard) .Bind().ToSingleton() - .Bind().ToSingleton() .Bind().ToConstant(instance) .Bind().ToConstant(new BrowserSkiaGraphics()) .Bind().ToSingleton() .Bind().ToSingleton() .Bind().ToSingleton(); + AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); if (AvaloniaLocator.Current.GetService() is { } options && options.RegisterAvaloniaServiceWorker) diff --git a/src/Browser/Avalonia.Browser/build/Avalonia.Browser.targets b/src/Browser/Avalonia.Browser/build/Avalonia.Browser.targets index 53ad40168a..fe66c02dd8 100644 --- a/src/Browser/Avalonia.Browser/build/Avalonia.Browser.targets +++ b/src/Browser/Avalonia.Browser/build/Avalonia.Browser.targets @@ -6,6 +6,7 @@ + diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/jsExports.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/jsExports.ts new file mode 100644 index 0000000000..18fde7449a --- /dev/null +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/jsExports.ts @@ -0,0 +1,12 @@ +export class JsExports { + public static resolvedExports?: any; + public static exportsPromise: Promise; +} +async function resolveExports (): Promise { + const runtimeApi = await globalThis.getDotnetRuntime(0); + if (runtimeApi == null) { return; } + JsExports.resolvedExports = await runtimeApi.getAssemblyExports("Avalonia.Browser.dll"); + return JsExports.resolvedExports; +} + +JsExports.exportsPromise = resolveExports(); diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/htmlSurfaceBase.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/htmlSurfaceBase.ts index fcb71828e4..50290add9a 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/htmlSurfaceBase.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/htmlSurfaceBase.ts @@ -26,6 +26,7 @@ export abstract class HtmlCanvasSurfaceBase extends CanvasSurface { 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; + if (this.sizeParams) { this.sizeChangedCallback(this.sizeParams[0], this.sizeParams[1], this.sizeParams[2]); } } public ensureSize() { diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/webRenderTargetRegistry.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/webRenderTargetRegistry.ts index 600e832fde..f8d7ab326f 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/webRenderTargetRegistry.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/surfaces/webRenderTargetRegistry.ts @@ -21,9 +21,13 @@ export class WebRenderTargetRegistry { } else { const self = globalThis as any; const module = self.Module ?? self.getDotnetRuntime(0)?.Module; - const pthread = module?.PThread; - if (pthread == null) { throw new Error("Unable to access emscripten PThread api"); } - const worker = pthread.pthreads[pthreadId]?.worker as Worker; + const pthreads = module?.PThread; + if (pthreads == null) { throw new Error("Unable to access emscripten PThread api"); } + const pthread = pthreads.pthreads[pthreadId]; + if (pthread == null) { throw new Error(`Unable get pthread with id ${pthreadId}`); } + let worker: Worker | undefined; + if (pthread.postMessage != null) { worker = pthread as Worker; } else { worker = pthread.worker; } + if (worker == null) { throw new Error(`Unable get Worker for pthread ${pthreadId}`); } const offscreen = canvas.transferControlToOffscreen(); worker.postMessage({ @@ -31,7 +35,7 @@ export class WebRenderTargetRegistry { canvas: offscreen, modes: preferredModes, id - }); + }, [offscreen]); WebRenderTargetRegistry.registry[id] = { canvas, worker @@ -41,18 +45,18 @@ export class WebRenderTargetRegistry { } static initializeWorker() { - self.addEventListener("onmessage", ev => { - const msg = ev as MessageEvent; + const oldHandler = self.onmessage; + self.onmessage = ev => { + const msg = ev; if (msg.data.avaloniaCmd === "registerCanvas") { WebRenderTargetRegistry.targets[msg.data.id] = WebRenderTargetRegistry.createRenderTarget(msg.data.canvas, msg.data.modes); - } - if (msg.data.avaloniaCmd === "unregisterCanvas") { + } else if (msg.data.avaloniaCmd === "unregisterCanvas") { /* eslint-disable */ // Our keys are _always_ numbers and are safe to delete delete WebRenderTargetRegistry.targets[msg.data.id]; /* eslint-enable */ - } - }); + } else if (oldHandler != null) { oldHandler.call(self, ev); } + }; } static getRenderTarget(id: number): WebRenderTarget | undefined { diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts index 51a5590d25..3ccd669293 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts @@ -1,12 +1,33 @@ +import { JsExports } from "./jsExports"; + export class TimerHelper { - public static runAnimationFrames(renderFrameCallback: (timestamp: number) => boolean): void { + public static runAnimationFrames(): void { function render(time: number) { - const next = renderFrameCallback(time); - if (next) { - self.requestAnimationFrame(render); + if (JsExports.resolvedExports != null) { + JsExports.resolvedExports.Avalonia.Browser.Interop.TimerHelper.JsExportOnAnimationFrame(time); } + self.requestAnimationFrame(render); } - self.requestAnimationFrame(render); } + + static onTimeout() { + if (JsExports.resolvedExports != null) { + JsExports.resolvedExports.Avalonia.Browser.Interop.TimerHelper.JsExportOnTimeout(); + } else { console.error("TimerHelper.onTimeout call while uninitialized"); } + } + + static onInterval() { + if (JsExports.resolvedExports != null) { + JsExports.resolvedExports.Avalonia.Browser.Interop.TimerHelper.JsExportOnInterval(); + } else { console.error("TimerHelper.onInterval call while uninitialized"); } + } + + public static setTimeout(interval: number): number { + return setTimeout(TimerHelper.onTimeout, interval); + } + + public static setInterval(interval: number): number { + return setInterval(TimerHelper.onInterval, interval); + } }