Forráskód Böngészése

Refactor WASM input and dom-callbacks to work with multithreading (#15849)

* Make resizing work again

* Fix various DOM events on multithreading

* Refactor WASM input to work with multithreading

* Minor improvements for drag n drop

* Use Microsoft.NET.Sdk.WebAssembly in control catalog browser

* Shortcut resolved exports

* Fix DomHelper.GetCurrentDocumentVisibility not working

* Fix embed sample

* Remove ManualTriggerRenderTimer

* Use pre-saved globalThis instance to make sure that JSImport interop works on a correct threading context

* Implement managed dispatcher for browser with event grouping

* Fix InputHelper.GetCoalescedEvents usage

* Nits after review
Max Katz 1 éve
szülő
commit
f140033e42
30 módosított fájl, 619 hozzáadás és 539 törlés
  1. 2 8
      samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj
  2. 1 1
      samples/ControlCatalog.Browser/EmbedSample.Browser.cs
  3. 6 3
      samples/ControlCatalog.Browser/Program.cs
  4. 6 0
      src/Browser/Avalonia.Browser/Avalonia.Browser.csproj
  5. 2 2
      src/Browser/Avalonia.Browser/AvaloniaView.cs
  6. 8 24
      src/Browser/Avalonia.Browser/BrowserActivatableLifetime.cs
  7. 44 5
      src/Browser/Avalonia.Browser/BrowserAppBuilder.cs
  8. 108 119
      src/Browser/Avalonia.Browser/BrowserInputHandler.cs
  9. 2 11
      src/Browser/Avalonia.Browser/BrowserInputPane.cs
  10. 3 8
      src/Browser/Avalonia.Browser/BrowserInsetsManager.cs
  11. 12 8
      src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs
  12. 9 14
      src/Browser/Avalonia.Browser/BrowserSystemNavigationManager.cs
  13. 21 42
      src/Browser/Avalonia.Browser/BrowserTextInputMethod.cs
  14. 24 13
      src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs
  15. 2 2
      src/Browser/Avalonia.Browser/ClipboardImpl.cs
  16. 20 13
      src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs
  17. 31 14
      src/Browser/Avalonia.Browser/Interop/DomHelper.cs
  18. 80 62
      src/Browser/Avalonia.Browser/Interop/InputHelper.cs
  19. 9 0
      src/Browser/Avalonia.Browser/Interop/NavigationHelper.cs
  20. 0 18
      src/Browser/Avalonia.Browser/ManualTriggerRenderTimer.cs
  21. 5 8
      src/Browser/Avalonia.Browser/Rendering/BrowserSurface.cs
  22. 5 5
      src/Browser/Avalonia.Browser/Rendering/RenderTargetBrowserSurface.cs
  23. 28 5
      src/Browser/Avalonia.Browser/WindowingPlatform.cs
  24. 1 1
      src/Browser/Avalonia.Browser/webapp/build.js
  25. 67 35
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts
  26. 96 88
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts
  27. 16 0
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/jsExports.ts
  28. 5 19
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/canvasSurface.ts
  29. 3 9
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts
  30. 3 2
      src/Browser/Avalonia.Browser/webapp/tsconfig.json

+ 2 - 8
samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj

@@ -1,9 +1,7 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk.WebAssembly">
   <PropertyGroup>
     <TargetFramework>$(AvsCurrentBrowserTargetFramework)</TargetFramework>
-    <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
-    <WasmMainJSPath>wwwroot/main.js</WasmMainJSPath>
-    <OutputType>Exe</OutputType>
+    <WasmEnableThreads>false</WasmEnableThreads>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
     <DebuggerSupport>true</DebuggerSupport>
     <WasmDebugLevel>5</WasmDebugLevel>
@@ -22,10 +20,6 @@
     <AdditionalUpToDateCheckInput Include="../../src/Browser/Avalonia.Browser/**/*" Visible="false"/>
   </ItemGroup>
 
-  <ItemGroup>
-    <WasmExtraFilesToDeploy Include="wwwroot/**" />
-  </ItemGroup>
-
   <Import Project="../../src/Browser/Avalonia.Browser/build/Avalonia.Browser.props" />
   <Import Project="../../src/Browser/Avalonia.Browser/build/Avalonia.Browser.targets" />
 </Project>

+ 1 - 1
samples/ControlCatalog.Browser/EmbedSample.Browser.cs

@@ -29,7 +29,7 @@ public class EmbedSampleWeb : INativeDemoControl
 
             static async void AddButton(JSObject parent)
             {
-                await JSHost.ImportAsync("embed.js", "./embed.js");
+                await JSHost.ImportAsync("embed.js", "../embed.js");
                 EmbedInterop.AddAppButton(parent);
             } 
         }

+ 6 - 3
samples/ControlCatalog.Browser/Program.cs

@@ -33,10 +33,13 @@ internal partial class Program
             })
             .StartBrowserAppAsync("out", options);
 
-        if (Application.Current!.ApplicationLifetime is ISingleTopLevelApplicationLifetime lifetime)
+        Dispatcher.UIThread.Invoke(() =>
         {
-            lifetime.TopLevel!.RendererDiagnostics.DebugOverlays = RendererDebugOverlays.Fps;
-        }
+            if (Application.Current!.ApplicationLifetime is ISingleTopLevelApplicationLifetime lifetime)
+            {
+                lifetime.TopLevel!.RendererDiagnostics.DebugOverlays = RendererDebugOverlays.Fps;
+            }
+        });
     }
 
     // Test with multiple AvaloniaView at once. 

+ 6 - 0
src/Browser/Avalonia.Browser/Avalonia.Browser.csproj

@@ -54,4 +54,10 @@
     <InternalsVisibleTo Include="Avalonia.Browser.Blazor, PublicKey=$(AvaloniaPublicKey)" />
   </ItemGroup>
 
+  <ItemGroup>
+    <Compile Include="..\..\Shared\RawEventGrouping.cs">
+      <Link>RawEventGrouping.cs</Link>
+    </Compile>
+  </ItemGroup>
+
 </Project>

+ 2 - 2
src/Browser/Avalonia.Browser/AvaloniaView.cs

@@ -12,7 +12,7 @@ namespace Avalonia.Browser
 
         /// <param name="divId">ID of the html element where avalonia content should be rendered.</param>
         public AvaloniaView(string divId)
-            : this(DomHelper.GetElementById(divId) ??
+            : this(DomHelper.GetElementById(divId, BrowserWindowingPlatform.GlobalThis) ??
                    throw new Exception($"Element with id '{divId}' was not found in the html document."))
         {
         }
@@ -47,7 +47,7 @@ namespace Avalonia.Browser
                 // 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");
+                             ?? DomHelper.GetElementById("avalonia-splash", BrowserWindowingPlatform.GlobalThis);
                 if (splash is not null)
                 {
                     DomHelper.AddCssClass(splash, "splash-close");

+ 8 - 24
src/Browser/Avalonia.Browser/BrowserActivatableLifetime.cs

@@ -1,36 +1,20 @@
-using System;
 using Avalonia.Browser.Interop;
 using Avalonia.Controls.ApplicationLifetimes;
-using Avalonia.Threading;
 
 namespace Avalonia.Browser;
 
-internal class BrowserActivatableLifetime : IActivatableLifetime
+internal class BrowserActivatableLifetime : ActivatableLifetimeBase
 {
-    public BrowserActivatableLifetime()
+    public void OnVisibilityStateChanged(string visibilityState)
     {
-        bool? initiallyVisible = InputHelper.SubscribeVisibilityChange(visible =>
+        var visible = visibilityState == "visible";
+        if (visible)
         {
-            initiallyVisible = null;
-            (visible ? Activated : Deactivated)?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background));
-        });
-    
-        // Trigger Activated as an initial state, if web page is visible, and wasn't hidden during initialization.
-        if (initiallyVisible == true)
+            OnActivated(ActivationKind.Background);
+        }
+        else
         {
-            _ = Dispatcher.UIThread.InvokeAsync(() =>
-            {
-                if (initiallyVisible == true)
-                {
-                    Activated?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background));
-                }
-            }, DispatcherPriority.Background);
+            OnDeactivated(ActivationKind.Background);
         }
     }
-    
-    public event EventHandler<ActivatedEventArgs>? Activated;
-    public event EventHandler<ActivatedEventArgs>? Deactivated;
-
-    public bool TryLeaveBackground() => false;
-    public bool TryEnterBackground() => false;
 }

+ 44 - 5
src/Browser/Avalonia.Browser/BrowserAppBuilder.cs

@@ -1,8 +1,10 @@
 using System;
 using System.Collections.Generic;
+using System.Threading;
 using System.Threading.Tasks;
 using Avalonia.Browser.Interop;
 using Avalonia.Browser.Rendering;
+using Avalonia.Controls;
 using Avalonia.Metadata;
 
 namespace Avalonia.Browser;
@@ -53,6 +55,12 @@ public record BrowserPlatformOptions
     /// For more details, see https://github.com/jimmywarting/native-file-system-adapter#a-note-when-downloading-with-the-polyfilled-version.
     /// </summary>
     public bool PreferFileDialogPolyfill { get; set; }
+
+    /// <summary>
+    /// Defines if Avalonia should create a controlled dispatcher loop on the web worker thread.
+    /// If used only when WasmEnableThreads is set to true. Default value is true.
+    /// </summary>
+    public bool? PreferManagedThreadDispatcher { get; set; } = true;
 }
 
 public static class BrowserAppBuilder
@@ -63,8 +71,9 @@ 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)
         {
@@ -78,8 +87,35 @@ public static class BrowserAppBuilder
             .AfterApplicationSetup(_ =>
             {
                 lifetime.View = new AvaloniaView(mainDivId);
-            })
-            .SetupWithLifetime(lifetime);
+            });
+
+        if (BrowserWindowingPlatform.IsManagedDispatcherEnabled)
+        {
+            var tcs = new TaskCompletionSource();
+            var thread = new Thread(() =>
+            {
+                try
+                {
+                    builder
+                        .SetupWithLifetime(lifetime);
+                    tcs.TrySetResult();
+                    builder.Instance!.Run(CancellationToken.None);
+                }
+                catch (Exception ex)
+                {
+                    tcs.TrySetException(ex);
+                }
+            });
+#pragma warning disable CA1416
+            thread.Start();
+#pragma warning restore CA1416
+            await tcs.Task;
+        }
+        else
+        {
+            builder
+                .SetupWithLifetime(lifetime);
+        }
     }
 
     /// <summary>
@@ -102,12 +138,15 @@ public static class BrowserAppBuilder
 
     internal static async Task<AppBuilder> PreSetupBrowser(AppBuilder builder, BrowserPlatformOptions? options)
     {
-        options ??= new BrowserPlatformOptions();
+        options ??= AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions();
         options.FrameworkAssetPathResolver ??= fileName => $"./{fileName}";
 
         AvaloniaLocator.CurrentMutable.Bind<BrowserPlatformOptions>().ToConstant(options);
 
         await AvaloniaModule.ImportMain();
+
+        BrowserWindowingPlatform.GlobalThis = DomHelper.GetGlobalThis();
+
         if (BrowserWindowingPlatform.IsThreadingEnabled)
         {
             await RenderWorker.InitializeAsync();

+ 108 - 119
src/Browser/Avalonia.Browser/BrowserInputHandler.cs

@@ -21,8 +21,9 @@ internal class BrowserInputHandler
     private IInputRoot? _inputRoot;
 
     private static readonly PooledList<RawPointerPoint> s_intermediatePointsPooledList = new(ClearMode.Never);
+    private readonly RawEventGrouper? _rawEventGrouper;
 
-    public BrowserInputHandler(BrowserTopLevelImpl topLevelImpl, JSObject container)
+    public BrowserInputHandler(BrowserTopLevelImpl topLevelImpl, JSObject container, JSObject inputElement, int topLevelId)
     {
         _topLevelImpl = topLevelImpl;
         _container = container ?? throw new ArgumentNullException(nameof(container));
@@ -32,15 +33,19 @@ internal class BrowserInputHandler
         _wheelMouseDevice = new MouseDevice();
         _mouseDevices = new();
 
-        InputHelper.SubscribeKeyEvents(
-            container,
-            OnKeyDown,
-            OnKeyUp);
-        InputHelper.SubscribePointerEvents(container, OnPointerMove, OnPointerDown, OnPointerUp,
-            OnPointerCancel, OnWheel);
-        InputHelper.SubscribeDropEvents(container, OnDragEvent);
+        _rawEventGrouper = BrowserWindowingPlatform.EventGrouperDispatchQueue is not null
+            ? new RawEventGrouper(DispatchInput, BrowserWindowingPlatform.EventGrouperDispatchQueue)
+            : null;
+
+        TextInputMethod = new BrowserTextInputMethod(this, container, inputElement);
+        InputPane = new BrowserInputPane();
+
+        InputHelper.SubscribeInputEvents(container, topLevelId);
     }
 
+    public BrowserTextInputMethod TextInputMethod { get; }
+    public BrowserInputPane InputPane { get; }
+    
     public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds;
 
     internal void SetInputRoot(IInputRoot inputRoot)
@@ -48,57 +53,65 @@ internal class BrowserInputHandler
         _inputRoot = inputRoot;
     }
 
-    private static RawPointerPoint ExtractRawPointerFromJsArgs(JSObject args)
+    private static RawPointerPoint CreateRawPointer(double offsetX, double offsetY,
+        double pressure, double tiltX, double tiltY, double twist) => new()
     {
-        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)
+        Position = new Point(offsetX, offsetY),
+        Pressure = (float)pressure,
+        XTilt = (float)tiltX,
+        YTilt = (float)tiltY,
+        Twist = (float)twist
+    };
+
+    public bool OnPointerMove(string pointerType, long pointerId, double offsetX, double offsetY,
+        double pressure, double tiltX, double tiltY, double twist, int modifier, JSObject argsObj)
     {
-        var pointerType = args.GetPropertyAsString("pointerType");
-        var point = ExtractRawPointerFromJsArgs(args);
+        var point = CreateRawPointer(offsetX, offsetY, pressure, tiltX, tiltY, twist);
         var type = pointerType switch
         {
             "touch" => RawPointerEventType.TouchUpdate,
             _ => RawPointerEventType.Move
         };
 
-        var coalescedEvents = new Lazy<IReadOnlyList<RawPointerPoint>?>(() =>
+        Lazy<IReadOnlyList<RawPointerPoint>?>? coalescedEvents = null;
+        // Rely on native GetCoalescedEvents only when managed event grouping is not available.
+        if (_rawEventGrouper is null)
         {
-            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++)
+            coalescedEvents = new Lazy<IReadOnlyList<RawPointerPoint>?>(() =>
             {
-                var point = points[i];
-                s_intermediatePointsPooledList.Add(ExtractRawPointerFromJsArgs(point));
-            }
+                // To minimize JS interop usage, we resolve all points properties in a single call.
+                const int itemsPerPoint = 6;
+                var pointsProps = InputHelper.GetCoalescedEvents(argsObj);
+                argsObj.Dispose();
+                s_intermediatePointsPooledList.Clear();
+
+                var pointsCount = pointsProps.Length / itemsPerPoint;
+                s_intermediatePointsPooledList.Capacity = pointsCount - 1;
 
-            return s_intermediatePointsPooledList;
-        });
+                // Skip the last one, as it is already processed point.
+                for (var i = 0; i < pointsCount - 1; i += itemsPerPoint)
+                {
+                    s_intermediatePointsPooledList.Add(CreateRawPointer(
+                        pointsProps[i], pointsProps[i + 1],
+                        pointsProps[i + 2], pointsProps[i + 3],
+                        pointsProps[i + 4], pointsProps[i + 5]));
+                }
+
+                return s_intermediatePointsPooledList;
+            });
+        }
 
-        return RawPointerEvent(type, pointerType!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"),
+        return RawPointerEvent(type, pointerType!, point, (RawInputModifiers)modifier, pointerId,
             coalescedEvents);
     }
 
-    private bool OnPointerDown(JSObject args)
+    public bool OnPointerDown(string pointerType, long pointerId, int buttons, double offsetX, double offsetY,
+        double pressure, double tiltX, double tiltY, double twist, int modifier)
     {
-        var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse";
         var type = pointerType switch
         {
             "touch" => RawPointerEventType.TouchBegin,
-            _ => args.GetPropertyAsInt32("button") switch
+            _ => buttons switch
             {
                 0 => RawPointerEventType.LeftButtonDown,
                 1 => RawPointerEventType.MiddleButtonDown,
@@ -110,17 +123,17 @@ internal class BrowserInputHandler
             }
         };
 
-        var point = ExtractRawPointerFromJsArgs(args);
-        return RawPointerEvent(type, pointerType, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"));
+        var point = CreateRawPointer(offsetX, offsetY, pressure, tiltX, tiltY, twist);
+        return RawPointerEvent(type, pointerType, point, (RawInputModifiers)modifier, pointerId);
     }
 
-    private bool OnPointerUp(JSObject args)
+    public bool OnPointerUp(string pointerType, long pointerId, int buttons, double offsetX, double offsetY,
+        double pressure, double tiltX, double tiltY, double twist, int modifier)
     {
-        var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse";
         var type = pointerType switch
         {
             "touch" => RawPointerEventType.TouchEnd,
-            _ => args.GetPropertyAsInt32("button") switch
+            _ => buttons switch
             {
                 0 => RawPointerEventType.LeftButtonUp,
                 1 => RawPointerEventType.MiddleButtonUp,
@@ -132,70 +145,33 @@ internal class BrowserInputHandler
             }
         };
 
-        var point = ExtractRawPointerFromJsArgs(args);
-        return RawPointerEvent(type, pointerType, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"));
+        var point = CreateRawPointer(offsetX, offsetY, pressure, tiltX, tiltY, twist);
+        return RawPointerEvent(type, pointerType, point, (RawInputModifiers)modifier, pointerId);
     }
 
-    private bool OnPointerCancel(JSObject args)
+    public bool OnPointerCancel(string pointerType, long pointerId, double offsetX, double offsetY,
+        double pressure, double tiltX, double tiltY, double twist, int modifier)
     {
-        var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse";
         if (pointerType == "touch")
         {
-            var point = ExtractRawPointerFromJsArgs(args);
+            var point = CreateRawPointer(offsetX, offsetY, pressure, tiltX, tiltY, twist);
             RawPointerEvent(RawPointerEventType.TouchCancel, pointerType, point,
-                GetModifiers(args), args.GetPropertyAsInt32("pointerId"));
+                (RawInputModifiers)modifier, pointerId);
         }
 
         return false;
     }
 
-    private bool OnWheel(JSObject args)
+    public bool OnWheel(double offsetX, double offsetY, double deltaX, double deltaY, int modifier)
     {
-        return RawMouseWheelEvent(new Point(args.GetPropertyAsDouble("offsetX"), args.GetPropertyAsDouble("offsetY")),
-            new Vector(-(args.GetPropertyAsDouble("deltaX") / 50), -(args.GetPropertyAsDouble("deltaY") / 50)),
-            GetModifiers(args));
+        return RawMouseWheelEvent(new Point(offsetX, offsetY),
+            new Vector(-(deltaX / 50), -(deltaY / 50)),
+            (RawInputModifiers)modifier);
     }
 
-    private static RawInputModifiers GetModifiers(JSObject e)
+    public bool OnDragEvent(string type, double offsetX, double offsetY, int modifiers, string? effectAllowedStr, JSObject? dataTransfer)
     {
-        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
+        var eventType = type switch
         {
             "dragenter" => RawDragEventType.DragEnter,
             "dragover" => RawDragEventType.DragOver,
@@ -203,8 +179,7 @@ internal class BrowserInputHandler
             "drop" => RawDragEventType.Drop,
             _ => (RawDragEventType)(int)-1
         };
-        var dataObject = args?.GetPropertyAsJSObject("dataTransfer");
-        if (args is null || eventType < 0 || dataObject is null)
+        if (eventType < 0 || dataTransfer is null)
         {
             return false;
         }
@@ -213,10 +188,9 @@ internal class BrowserInputHandler
         // 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 position = new Point(offsetX, offsetY);
 
-        var effectAllowedStr = dataObject.GetPropertyAsString("effectAllowed") ?? "none";
+        effectAllowedStr ??= "none";
         var effectAllowed = DragDropEffects.None;
         if (effectAllowedStr.Contains("copy", StringComparison.OrdinalIgnoreCase))
         {
@@ -243,16 +217,18 @@ internal class BrowserInputHandler
             return false;
         }
 
-        var dropEffect = RawDragEvent(eventType, position, modifiers, new BrowserDataObject(dataObject), effectAllowed);
-        dataObject.SetProperty("dropEffect", dropEffect.ToString().ToLowerInvariant());
+        var dropEffect = RawDragEvent(eventType, position, (RawInputModifiers)modifiers, new BrowserDataObject(dataTransfer), effectAllowed);
+        dataTransfer.SetProperty("dropEffect", dropEffect.ToString().ToLowerInvariant());
 
+        // Note, due to complications of JS interop, we ignore this return value.
+        // And instead assume, that event is handled for any "drop" and "drag-over" stages.
         return eventType is RawDragEventType.Drop or RawDragEventType.DragOver
                && dropEffect != DragDropEffects.None;
     }
 
-    private bool OnKeyDown(string code, string key, string modifier)
+    public bool OnKeyDown(string code, string key, int modifier)
     {
-        var handled = RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)int.Parse(modifier));
+        var handled = RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)modifier);
 
         if (!handled && key.Length == 1)
         {
@@ -262,9 +238,9 @@ internal class BrowserInputHandler
         return handled;
     }
 
-    private bool OnKeyUp(string code, string key, string modifier)
+    public bool OnKeyUp(string code, string key, int modifier)
     {
-        return RawKeyboardEvent(RawKeyEventType.KeyUp, code, key, (RawInputModifiers)int.Parse(modifier));
+        return RawKeyboardEvent(RawKeyEventType.KeyUp, code, key, (RawInputModifiers)modifier);
     }
 
     private bool RawPointerEvent(
@@ -272,8 +248,7 @@ internal class BrowserInputHandler
         RawPointerPoint p, RawInputModifiers modifiers, long touchPointId,
         Lazy<IReadOnlyList<RawPointerPoint>?>? intermediatePoints = null)
     {
-        if (_inputRoot is { }
-            && _topLevelImpl.Input is { } input)
+        if (_inputRoot is not null)
         {
             var device = GetPointerDevice(pointerType, touchPointId);
             var args = device is TouchDevice ?
@@ -286,7 +261,7 @@ internal class BrowserInputHandler
                     RawPointerId = touchPointId, IntermediatePoints = intermediatePoints
                 };
 
-            input.Invoke(args);
+            ScheduleInput(args);
 
             return args.Handled;
         }
@@ -319,7 +294,7 @@ internal class BrowserInputHandler
         {
             var args = new RawMouseWheelEventArgs(_wheelMouseDevice, Timestamp, _inputRoot, p, v, modifiers);
 
-            _topLevelImpl.Input?.Invoke(args);
+            ScheduleInput(args);
 
             return args.Handled;
         }
@@ -347,14 +322,7 @@ internal class BrowserInputHandler
             keySymbol
         );
 
-        try
-        {
-            _topLevelImpl.Input?.Invoke(args);
-        }
-        catch (Exception ex)
-        {
-            Console.WriteLine(ex);
-        }
+        ScheduleInput(args);
 
         return args.Handled;
     }
@@ -364,7 +332,7 @@ internal class BrowserInputHandler
         if (_inputRoot is { })
         {
             var args = new RawTextInputEventArgs(BrowserWindowingPlatform.Keyboard, Timestamp, _inputRoot, text);
-            _topLevelImpl.Input?.Invoke(args);
+            ScheduleInput(args);
 
             return args.Handled;
         }
@@ -377,7 +345,28 @@ internal class BrowserInputHandler
     {
         var device = AvaloniaLocator.Current.GetRequiredService<IDragDropDevice>();
         var eventArgs = new RawDragEvent(device, eventType, _inputRoot!, position, dataObject, dropEffect, modifiers);
-        _topLevelImpl.Input?.Invoke(eventArgs);
+        ScheduleInput(eventArgs);
         return eventArgs.Effects;
     }
+
+    private void ScheduleInput(RawInputEventArgs args)
+    {
+        // _rawEventGrouper is available only when we use managed dispatcher.
+        if (_rawEventGrouper is not null)
+        {
+            _rawEventGrouper.HandleEvent(args);
+        }
+        else
+        {
+            DispatchInput(args);
+        }
+    }
+
+    private void DispatchInput(RawInputEventArgs args)
+    {
+        if (_inputRoot is null)
+            return;
+
+        _topLevelImpl.Input?.Invoke(args);
+    }
 }

+ 2 - 11
src/Browser/Avalonia.Browser/BrowserInputPane.cs

@@ -7,20 +7,11 @@ namespace Avalonia.Browser;
 
 internal class BrowserInputPane : InputPaneBase
 {
-    public BrowserInputPane(JSObject container)
-    {
-        InputHelper.SubscribeKeyboardGeometryChange(container, OnGeometryChange);
-    }
-
-    private bool OnGeometryChange(JSObject args)
+    public bool OnGeometryChange(double x, double y, double width, double height)
     {
         var oldState = (OccludedRect, State);
 
-        OccludedRect = new Rect(
-            args.GetPropertyAsDouble("x"),
-            args.GetPropertyAsDouble("y"),
-            args.GetPropertyAsDouble("width"),
-            args.GetPropertyAsDouble("height"));
+        OccludedRect = new Rect(x, y, width, height);
         State = OccludedRect.Width != 0 ? InputPaneState.Open : InputPaneState.Closed;
 
         if (oldState != (OccludedRect, State))

+ 3 - 8
src/Browser/Avalonia.Browser/BrowserInsetsManager.cs

@@ -6,20 +6,15 @@ namespace Avalonia.Browser
 {
     internal class BrowserInsetsManager : InsetsManagerBase
     {
-        public BrowserInsetsManager()
-        {
-            DomHelper.InitSafeAreaPadding();
-        }
-
         public override bool? IsSystemBarVisible
         {
             get
             {
-                return DomHelper.IsFullscreen();
+                return DomHelper.IsFullscreen(BrowserWindowingPlatform.GlobalThis);
             }
             set
             {
-                DomHelper.SetFullscreen(!value ?? false);
+                _ = DomHelper.SetFullscreen(BrowserWindowingPlatform.GlobalThis, !value ?? false);
             }
         }
 
@@ -29,7 +24,7 @@ namespace Avalonia.Browser
         {
             get
             {
-                var padding = DomHelper.GetSafeAreaPadding();
+                var padding = DomHelper.GetSafeAreaPadding(BrowserWindowingPlatform.GlobalThis);
 
                 return new Thickness(padding[0], padding[1], padding[2], padding[3]);
             }

+ 12 - 8
src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs

@@ -31,21 +31,25 @@ internal class BrowserPlatformSettings : DefaultPlatformSettings
         };
     }
 
+    public void OnValuesChanged(bool isDarkMode, bool isHighContrast)
+    {
+        _isDarkMode = isDarkMode;
+        _isHighContrast = isHighContrast;
+        OnColorValuesChanged(GetColorValues());
+    }
+    
     private void EnsureBackend()
     {
         if (!_isInitialized)
         {
             // WASM module has async nature of initialization. We can't native code right away during components registration. 
             _isInitialized = true;
-
-            var obj = DomHelper.ObserveDarkMode((isDarkMode, isHighContrast) =>
+            var values = DomHelper.GetDarkMode(BrowserWindowingPlatform.GlobalThis);
+            if (values.Length == 2)
             {
-                _isDarkMode = isDarkMode;
-                _isHighContrast = isHighContrast;
-                OnColorValuesChanged(GetColorValues());
-            });
-            _isDarkMode = obj.GetPropertyAsBoolean("isDarkMode");
-            _isHighContrast = obj.GetPropertyAsBoolean("isHighContrast");
+                _isDarkMode = values[0] > 0;
+                _isHighContrast = values[1] > 0;
+            }
         }
     }
 }

+ 9 - 14
src/Browser/Avalonia.Browser/BrowserSystemNavigationManager.cs

@@ -1,24 +1,19 @@
 using System;
-using Avalonia.Browser.Interop;
 using Avalonia.Interactivity;
 using Avalonia.Platform;
 
-namespace Avalonia.Browser
+namespace Avalonia.Browser;
+
+internal class BrowserSystemNavigationManagerImpl : ISystemNavigationManagerImpl
 {
-    internal class BrowserSystemNavigationManagerImpl : ISystemNavigationManagerImpl
-    {
-        public event EventHandler<RoutedEventArgs>? BackRequested;
+    public event EventHandler<RoutedEventArgs>? BackRequested;
 
-        public BrowserSystemNavigationManagerImpl()
-        {
-            NavigationHelper.AddBackHandler(() =>
-            {
-                var routedEventArgs = new RoutedEventArgs();
+    public bool OnBackRequested()
+    {
+        var routedEventArgs = new RoutedEventArgs();
 
-                BackRequested?.Invoke(this, routedEventArgs);
+        BackRequested?.Invoke(this, routedEventArgs);
 
-                return routedEventArgs.Handled;
-            });
-        }
+        return routedEventArgs.Handled;
     }
 }

+ 21 - 42
src/Browser/Avalonia.Browser/BrowserTextInputMethod.cs

@@ -5,27 +5,17 @@ using Avalonia.Input.TextInput;
 
 namespace Avalonia.Browser;
 
-internal class BrowserTextInputMethod : ITextInputMethodImpl
+internal class BrowserTextInputMethod(
+    BrowserInputHandler inputHandler,
+    JSObject containerElement,
+    JSObject inputElement)
+    : ITextInputMethodImpl
 {
-    private readonly JSObject _inputElement;
-    private readonly JSObject _containerElement;
-    private readonly BrowserInputHandler _inputHandler;
+    private readonly JSObject _inputElement = inputElement ?? throw new ArgumentNullException(nameof(inputElement));
+    private readonly JSObject _containerElement = containerElement ?? throw new ArgumentNullException(nameof(containerElement));
+    private readonly BrowserInputHandler _inputHandler = inputHandler ?? throw new ArgumentNullException(nameof(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()
@@ -95,12 +85,11 @@ internal class BrowserTextInputMethod : ITextInputMethodImpl
         InputHelper.SetSurroundingText(_inputElement, "", 0, 0);
     }
 
-    private bool OnBeforeInput(JSObject arg, int start, int end)
+    public void OnBeforeInput(string inputType, int start, int end)
     {
-        var type = arg.GetPropertyAsString("inputType");
-        if (type != "deleteByComposition")
+        if (inputType != "deleteByComposition")
         {
-            if (type == "deleteContentBackward")
+            if (inputType == "deleteContentBackward")
             {
                 start = _inputElement.GetPropertyAsInt32("selectionStart");
                 end = _inputElement.GetPropertyAsInt32("selectionEnd");
@@ -116,47 +105,37 @@ internal class BrowserTextInputMethod : ITextInputMethodImpl
         {
             _client.Selection = new TextSelection(start, end);
         }
-
-        return false;
     }
 
-    private bool OnCompositionStart(JSObject args)
+    public void OnCompositionStart()
     {
         if (_client == null)
-            return false;
+            return;
 
         _client.SetPreeditText(null);
         IsComposing = true;
-
-        return false;
     }
 
-    private bool OnCompositionUpdate(JSObject args)
+    public void OnCompositionUpdate(string? data)
     {
         if (_client == null)
-            return false;
+            return;
 
-        _client.SetPreeditText(args.GetPropertyAsString("data"));
-
-        return false;
+        _client.SetPreeditText(data);
     }
 
-    private bool OnCompositionEnd(JSObject args)
+    public void OnCompositionEnd(string? data)
     {
         if (_client == null)
-            return false;
+            return;
 
         IsComposing = false;
 
         _client.SetPreeditText(null);
-
-        var text = args.GetPropertyAsString("data");
-
-        if (text != null)
+        
+        if (data != null)
         {
-            return _inputHandler.RawTextEvent(text);
+            _inputHandler.RawTextEvent(data);
         }
-
-        return false;
     }
 }

+ 24 - 13
src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs

@@ -23,40 +23,49 @@ namespace Avalonia.Browser
 {
     internal class BrowserTopLevelImpl : ITopLevelImpl
     {
+        private static int s_lastTopLevelId = 0;
+        private static Dictionary<int, WeakReference<BrowserTopLevelImpl>> s_topLevels = new();
+        
         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 IInputPane _inputPane;
         private readonly JSObject _container;
         private readonly BrowserInputHandler _inputHandler;
         private string _currentCursor = CssCursor.Default;
         private BrowserSurface? _surface;
+        private readonly int _topLevelId;
 
         static BrowserTopLevelImpl()
         {
-            InputHelper.InitializeBackgroundHandlers();
+            DomHelper.InitGlobalDomEvents(BrowserWindowingPlatform.GlobalThis);
+            InputHelper.InitializeBackgroundHandlers(BrowserWindowingPlatform.GlobalThis);
         }
-        
+
+        public static BrowserTopLevelImpl? TryGetTopLevel(int id)
+        {
+            return s_topLevels.TryGetValue(id, out var weakReference) &&
+                weakReference.TryGetTarget(out var topLevelImpl) ?
+                topLevelImpl :
+                null;
+        }
+
         public BrowserTopLevelImpl(JSObject container, JSObject nativeControlHost, JSObject inputElement)
         {
             AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1);
 
-            _inputHandler = new BrowserInputHandler(this, container);
-            _textInputMethodImpl = new BrowserTextInputMethod(_inputHandler, container, inputElement);
+            _topLevelId = ++s_lastTopLevelId;
+            s_topLevels.Add(_topLevelId, new WeakReference<BrowserTopLevelImpl>(this));
+            _inputHandler = new BrowserInputHandler(this, container, inputElement, _topLevelId);
             _insetsManager = new BrowserInsetsManager();
             _nativeControlHost = new BrowserNativeControlHost(nativeControlHost);
             _storageProvider = new BrowserStorageProvider();
-            _systemNavigationManager = new BrowserSystemNavigationManagerImpl();
             _clipboard = new ClipboardImpl();
-            _inputPane = new BrowserInputPane(container);
 
             _container = container;
 
             var opts = AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions();
-            _surface = RenderTargetBrowserSurface.Create(container, opts.RenderingMode);
+            _surface = RenderTargetBrowserSurface.Create(container, opts.RenderingMode, _topLevelId);
 
             _surface.SizeChanged += OnSizeChanged;
             _surface.ScalingChanged += OnScalingChanged;
@@ -87,6 +96,8 @@ namespace Avalonia.Browser
         }
 
         public Compositor Compositor { get; }
+        public BrowserSurface? Surface => _surface;
+        public BrowserInputHandler InputHandler => _inputHandler;
 
         public void SetInputRoot(IInputRoot inputRoot) => _inputHandler.SetInputRoot(inputRoot);
 
@@ -144,12 +155,12 @@ namespace Avalonia.Browser
 
             if (featureType == typeof(ITextInputMethodImpl))
             {
-                return _textInputMethodImpl;
+                return _inputHandler.TextInputMethod;
             }
 
             if (featureType == typeof(ISystemNavigationManagerImpl))
             {
-                return _systemNavigationManager;
+                return AvaloniaLocator.Current.GetService<ISystemNavigationManagerImpl>();
             }
 
             if (featureType == typeof(INativeControlHostImpl))
@@ -169,7 +180,7 @@ namespace Avalonia.Browser
 
             if (featureType == typeof(IInputPane))
             {
-                return _inputPane;
+                return _inputHandler.InputPane;
             }
 
             return null;

+ 2 - 2
src/Browser/Avalonia.Browser/ClipboardImpl.cs

@@ -10,12 +10,12 @@ namespace Avalonia.Browser
     {
         public Task<string?> GetTextAsync()
         {
-            return InputHelper.ReadClipboardTextAsync()!;
+            return InputHelper.ReadClipboardTextAsync(BrowserWindowingPlatform.GlobalThis)!;
         }
 
         public Task SetTextAsync(string? text)
         {
-            return InputHelper.WriteClipboardTextAsync(text ?? string.Empty);
+            return InputHelper.WriteClipboardTextAsync(BrowserWindowingPlatform.GlobalThis, text ?? string.Empty);
         }
 
         public async Task ClearAsync() => await SetTextAsync("");

+ 20 - 13
src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs

@@ -1,9 +1,6 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-using System.Runtime.InteropServices.JavaScript;
+using System.Runtime.InteropServices.JavaScript;
+using System.Threading.Tasks;
+using Avalonia.Threading;
 
 namespace Avalonia.Browser.Interop;
 
@@ -11,15 +8,25 @@ internal record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int
 
 internal static partial class CanvasHelper
 {
-    [JSImport("CanvasSurface.onSizeChanged", AvaloniaModule.MainModuleName)]
-    public static partial void OnSizeChanged(
-        JSObject canvasSurface,
-        [JSMarshalAs<JSType.Function<JSType.Number, JSType.Number, JSType.Number>>]
-        // TODO: this callback should be <int, int, double>. Revert after next .NET 9 preview.  
-        Action<double, double, double> onSizeChanged);
+    [JSExport]
+    public static Task OnSizeChanged(int topLevelId, double width, double height, double dpr)
+    {
+        if (BrowserWindowingPlatform.IsThreadingEnabled)
+        {
+            return Dispatcher.UIThread.InvokeAsync(() => BrowserTopLevelImpl
+                    .TryGetTopLevel(topLevelId)?.Surface?.OnSizeChanged(width, height, dpr))
+                .GetTask();
+        }
+        else
+        {
+            BrowserTopLevelImpl
+                .TryGetTopLevel(topLevelId)?.Surface?.OnSizeChanged(width, height, dpr);
+            return Task.CompletedTask;
+        }
+    }
 
     [JSImport("CanvasSurface.create", AvaloniaModule.MainModuleName)]
-    public static partial JSObject CreateRenderTargetSurface(JSObject canvasSurface, int[] modes, int threadId);
+    public static partial JSObject CreateRenderTargetSurface(JSObject canvasSurface, int[] modes, int topLevelId, int threadId);
 
     [JSImport("CanvasSurface.destroy", AvaloniaModule.MainModuleName)]
     public static partial void Destroy(JSObject canvasSurface);

+ 31 - 14
src/Browser/Avalonia.Browser/Interop/DomHelper.cs

@@ -1,36 +1,53 @@
-using System;
-using System.Runtime.InteropServices.JavaScript;
+using System.Runtime.InteropServices.JavaScript;
+using System.Threading.Tasks;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Platform;
 
 namespace Avalonia.Browser.Interop;
 
 internal static partial class DomHelper
 {
-    [JSImport("globalThis.document.getElementById")]
-    internal static partial JSObject? GetElementById(string id);
+    [JSImport("AvaloniaDOM.getGlobalThis", AvaloniaModule.MainModuleName)]
+    internal static partial JSObject GetGlobalThis();
+
+    [JSImport("AvaloniaDOM.getFirstElementById", AvaloniaModule.MainModuleName)]
+    internal static partial JSObject? GetElementById(string id, JSObject parent);
 
     [JSImport("AvaloniaDOM.getFirstElementByClassName", AvaloniaModule.MainModuleName)]
-    internal static partial JSObject? GetElementsByClassName(string className, JSObject? parent);
+    internal static partial JSObject? GetElementsByClassName(string className, JSObject parent);
 
     [JSImport("AvaloniaDOM.createAvaloniaHost", AvaloniaModule.MainModuleName)]
     public static partial JSObject CreateAvaloniaHost(JSObject element);
 
     [JSImport("AvaloniaDOM.isFullscreen", AvaloniaModule.MainModuleName)]
-    public static partial bool IsFullscreen();
+    public static partial bool IsFullscreen(JSObject globalThis);
 
     [JSImport("AvaloniaDOM.setFullscreen", AvaloniaModule.MainModuleName)]
-    public static partial JSObject SetFullscreen(bool isFullscreen);
+    public static partial Task SetFullscreen(JSObject globalThis, bool isFullscreen);
 
     [JSImport("AvaloniaDOM.getSafeAreaPadding", AvaloniaModule.MainModuleName)]
-    public static partial double[] GetSafeAreaPadding();
+    public static partial double[] GetSafeAreaPadding(JSObject globalThis);
 
-    [JSImport("AvaloniaDOM.initSafeAreaPadding", AvaloniaModule.MainModuleName)]
-    public static partial void InitSafeAreaPadding();
+    [JSImport("AvaloniaDOM.getDarkMode", AvaloniaModule.MainModuleName)]
+    public static partial int[] GetDarkMode(JSObject globalThis);
 
     [JSImport("AvaloniaDOM.addClass", AvaloniaModule.MainModuleName)]
     public static partial void AddCssClass(JSObject element, string className);
 
-    [JSImport("AvaloniaDOM.observeDarkMode", AvaloniaModule.MainModuleName)]
-    public static partial JSObject ObserveDarkMode(
-        [JSMarshalAs<JSType.Function<JSType.Boolean, JSType.Boolean>>]
-        Action<bool, bool> observer);
+    [JSImport("AvaloniaDOM.initGlobalDomEvents", AvaloniaModule.MainModuleName)]
+    public static partial void InitGlobalDomEvents(JSObject globalThis);
+
+    [JSExport]
+    public static Task DarkModeChanged(bool isDarkMode, bool isHighContrast)
+    {
+        (AvaloniaLocator.Current.GetService<IPlatformSettings>() as BrowserPlatformSettings)?.OnValuesChanged(isDarkMode, isHighContrast);
+        return Task.CompletedTask;
+    }
+
+    [JSExport]
+    public static Task DocumentVisibilityChanged(string visibilityState)
+    {
+        (AvaloniaLocator.Current.GetService<IActivatableLifetime>() as BrowserActivatableLifetime)?.OnVisibilityStateChanged(visibilityState);
+        return Task.CompletedTask;
+    }
 }

+ 80 - 62
src/Browser/Avalonia.Browser/Interop/InputHelper.cs

@@ -7,69 +7,85 @@ namespace Avalonia.Browser.Interop;
 
 internal static partial class InputHelper
 {
-    [JSImport("InputHelper.subscribeKeyEvents", AvaloniaModule.MainModuleName)]
-    public static partial void SubscribeKeyEvents(
-        JSObject htmlElement,
-        [JSMarshalAs<JSType.Function<JSType.String, JSType.String, JSType.String, JSType.Boolean>>]
-        // TODO: this callback should be <string, string, int, bool>. Revert after next .NET 9 preview.  
-        Func<string, string, string, bool> keyDown,
-        [JSMarshalAs<JSType.Function<JSType.String, JSType.String, JSType.String, JSType.Boolean>>]
-        // TODO: this callback should be <string, string, int, bool>. Revert after next .NET 9 preview.
-        Func<string, string, string, bool> keyUp);
-
-    [JSImport("InputHelper.subscribeTextEvents", AvaloniaModule.MainModuleName)]
-    public static partial void SubscribeTextEvents(
-        JSObject htmlElement,
-        [JSMarshalAs<JSType.Function<JSType.Object, JSType.Number, JSType.Number, JSType.Boolean>>]
-        Func<JSObject, int, int, bool> onBeforeInput,
-        [JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
-        Func<JSObject, bool> onCompositionStart,
-        [JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
-        Func<JSObject, bool> onCompositionUpdate,
-        [JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
-        Func<JSObject, bool> onCompositionEnd);
-
-    [JSImport("InputHelper.subscribePointerEvents", AvaloniaModule.MainModuleName)]
-    public static partial void SubscribePointerEvents(
-        JSObject htmlElement,
-        [JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
-        Func<JSObject, bool> pointerMove,
-        [JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
-        Func<JSObject, bool> pointerDown,
-        [JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
-        Func<JSObject, bool> pointerUp,
-        [JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
-        Func<JSObject, bool> pointerCancel,
-        [JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
-        Func<JSObject, bool> wheel);
+    public static Task RedirectInputAsync(int topLevelId, Action<BrowserTopLevelImpl> handler)
+    {
+        if (BrowserTopLevelImpl.TryGetTopLevel(topLevelId) is { } topLevelImpl) handler(topLevelImpl);
+        return Task.CompletedTask;
+    }
 
     [JSImport("InputHelper.subscribeInputEvents", AvaloniaModule.MainModuleName)]
-    public static partial void SubscribeInputEvents(
-        JSObject htmlElement,
-        [JSMarshalAs<JSType.Function<JSType.String, JSType.Boolean>>]
-        Func<string, bool> input);
-
-    [JSImport("InputHelper.subscribeDropEvents", AvaloniaModule.MainModuleName)]
-    public static partial void SubscribeDropEvents(JSObject containerElement,
-        [JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>] Func<JSObject, bool> dragEvent);
-
-    [JSImport("InputHelper.subscribeKeyboardGeometryChange", AvaloniaModule.MainModuleName)]
-    public static partial void SubscribeKeyboardGeometryChange(JSObject containerElement,
-        [JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>] Func<JSObject, bool> handler);
-
-    [JSImport("InputHelper.subscribeVisibilityChange", AvaloniaModule.MainModuleName)]
-    public static partial bool SubscribeVisibilityChange([JSMarshalAs<JSType.Function<JSType.Boolean>>] Action<bool> handler);
+    public static partial void SubscribeInputEvents(JSObject htmlElement, int topLevelId);
+
+    [JSExport]
+    public static Task OnKeyDown(int topLevelId, string code, string key, int modifier) =>
+        RedirectInputAsync(topLevelId, t => t.InputHandler.OnKeyDown(code, key, modifier));
+
+    [JSExport]
+    public static Task OnKeyUp(int topLevelId, string code, string key, int modifier) =>
+        RedirectInputAsync(topLevelId, t => t.InputHandler.OnKeyUp(code, key, modifier));
+
+    [JSExport]
+    public static Task OnBeforeInput(int topLevelId, string inputType, int start, int end) =>
+        RedirectInputAsync(topLevelId, t => t.InputHandler.TextInputMethod.OnBeforeInput(inputType, start, end));
+
+    [JSExport]
+    public static Task OnCompositionStart(int topLevelId) =>
+        RedirectInputAsync(topLevelId, t => t.InputHandler.TextInputMethod.OnCompositionStart());
+
+    [JSExport]
+    public static Task OnCompositionUpdate(int topLevelId, string? data) =>
+        RedirectInputAsync(topLevelId, t => t.InputHandler.TextInputMethod.OnCompositionUpdate(data));
+
+    [JSExport]
+    public static Task OnCompositionEnd(int topLevelId, string? data) =>
+        RedirectInputAsync(topLevelId, t => t.InputHandler.TextInputMethod.OnCompositionEnd(data));
+
+    [JSExport]
+    public static Task OnPointerMove(int topLevelId, string pointerType, [JSMarshalAs<JSType.Number>] long pointerId,
+        double offsetX, double offsetY, double pressure, double tiltX, double tiltY, double twist, int modifier, JSObject argsObj) =>
+        RedirectInputAsync(topLevelId, t => t.InputHandler
+            .OnPointerMove(pointerType, pointerId, offsetX, offsetY, pressure, tiltX, tiltY, twist, modifier, argsObj));
+
+    [JSExport]
+    public static Task OnPointerDown(int topLevelId, string pointerType, [JSMarshalAs<JSType.Number>] long pointerId, int buttons,
+        double offsetX, double offsetY, double pressure, double tiltX, double tiltY, double twist, int modifier) =>
+        RedirectInputAsync(topLevelId, t => t.InputHandler
+            .OnPointerDown(pointerType, pointerId, buttons, offsetX, offsetY, pressure, tiltX, tiltY, twist, modifier));
+
+    [JSExport]
+    public static Task OnPointerUp(int topLevelId, string pointerType, [JSMarshalAs<JSType.Number>] long pointerId, int buttons,
+        double offsetX, double offsetY, double pressure, double tiltX, double tiltY, double twist, int modifier) =>
+        RedirectInputAsync(topLevelId, t => t.InputHandler
+            .OnPointerUp(pointerType, pointerId, buttons, offsetX, offsetY, pressure, tiltX, tiltY, twist, modifier));
+
+    [JSExport]
+    public static Task OnPointerCancel(int topLevelId, string pointerType, [JSMarshalAs<JSType.Number>] long pointerId,
+        double offsetX, double offsetY, double pressure, double tiltX, double tiltY, double twist, int modifier) =>
+        RedirectInputAsync(topLevelId, t => t.InputHandler
+            .OnPointerCancel(pointerType, pointerId, offsetX, offsetY, pressure, tiltX, tiltY, twist, modifier));
+
+    [JSExport]
+    public static Task OnWheel(int topLevelId,
+        double offsetX, double offsetY,
+        double deltaX, double deltaY, int modifier) =>
+        RedirectInputAsync(topLevelId, t => t.InputHandler.OnWheel(offsetX, offsetY, deltaX, deltaY, modifier));
+
+    [JSExport]
+    public static Task OnDragDrop(int topLevelId, string type, double offsetX, double offsetY, int modifiers, string? effectAllowedStr, JSObject? dataTransfer) =>
+        RedirectInputAsync(topLevelId, t => t.InputHandler.OnDragEvent(type, offsetX, offsetY, modifiers, effectAllowedStr, dataTransfer));
+
+    [JSExport]
+    public static Task OnKeyboardGeometryChange(int topLevelId, double x, double y, double width, double height) =>
+        RedirectInputAsync(topLevelId, t => t.InputHandler.InputPane
+            .OnGeometryChange(x, y, width, height));
 
     [JSImport("InputHelper.getCoalescedEvents", AvaloniaModule.MainModuleName)]
-    [return: JSMarshalAs<JSType.Array<JSType.Object>>]
-    public static partial JSObject[] GetCoalescedEvents(JSObject pointerEvent);
+    [return: JSMarshalAs<JSType.Array<JSType.Number>>]
+    public static partial double[] GetCoalescedEvents(JSObject pointerEvent);
 
     [JSImport("InputHelper.clearInput", AvaloniaModule.MainModuleName)]
     public static partial void ClearInputElement(JSObject htmlElement);
 
-    [JSImport("InputHelper.isInputElement", AvaloniaModule.MainModuleName)]
-    public static partial void IsInputElement(JSObject htmlElement);
-
     [JSImport("InputHelper.focusElement", AvaloniaModule.MainModuleName)]
     public static partial void FocusElement(JSObject htmlElement);
 
@@ -89,17 +105,19 @@ internal static partial class InputHelper
     public static partial void SetBounds(JSObject htmlElement, int x, int y, int width, int height, int caret);
 
     [JSImport("InputHelper.initializeBackgroundHandlers", AvaloniaModule.MainModuleName)]
-    public static partial void InitializeBackgroundHandlers();
+    public static partial void InitializeBackgroundHandlers(JSObject globalThis);
 
     [JSImport("InputHelper.readClipboardText", AvaloniaModule.MainModuleName)]
-    public static partial Task<string> ReadClipboardTextAsync();
+    public static partial Task<string> ReadClipboardTextAsync(JSObject globalThis);
+
+    [JSImport("InputHelper.writeClipboardText", AvaloniaModule.MainModuleName)]
+    public static partial Task WriteClipboardTextAsync(JSObject globalThis, string text);
 
     [JSImport("InputHelper.setPointerCapture", AvaloniaModule.MainModuleName)]
-    public static partial void SetPointerCapture(JSObject containerElement, [JSMarshalAs<JSType.Number>] long pointerId);
+    public static partial void
+        SetPointerCapture(JSObject containerElement, [JSMarshalAs<JSType.Number>] long pointerId);
 
     [JSImport("InputHelper.releasePointerCapture", AvaloniaModule.MainModuleName)]
-    public static partial void ReleasePointerCapture(JSObject containerElement, [JSMarshalAs<JSType.Number>] long pointerId);
-
-    [JSImport("globalThis.navigator.clipboard.writeText")]
-    public static partial Task WriteClipboardTextAsync(string text);
+    public static partial void ReleasePointerCapture(JSObject containerElement,
+        [JSMarshalAs<JSType.Number>] long pointerId);
 }

+ 9 - 0
src/Browser/Avalonia.Browser/Interop/NavigationHelper.cs

@@ -1,5 +1,7 @@
 using System;
 using System.Runtime.InteropServices.JavaScript;
+using System.Threading.Tasks;
+using Avalonia.Platform;
 
 namespace Avalonia.Browser.Interop;
 
@@ -8,6 +10,13 @@ internal static partial class NavigationHelper
     [JSImport("NavigationHelper.addBackHandler", AvaloniaModule.MainModuleName)]
     public static partial void AddBackHandler([JSMarshalAs<JSType.Function<JSType.Boolean>>] Func<bool> backHandlerCallback);
 
+    public static Task<bool> OnBackRequested()
+    {
+        var handled = (AvaloniaLocator.Current.GetService<ISystemNavigationManagerImpl>() as BrowserSystemNavigationManagerImpl)?
+            .OnBackRequested() ?? false;
+        return Task.FromResult(handled);
+    }
+    
     [JSImport("window.open")]
     public static partial JSObject? WindowOpen(string uri, string target);
 }

+ 0 - 18
src/Browser/Avalonia.Browser/ManualTriggerRenderTimer.cs

@@ -1,18 +0,0 @@
-using System;
-using System.Diagnostics;
-using Avalonia.Rendering;
-
-namespace Avalonia.Browser
-{
-    internal class ManualTriggerRenderTimer : IRenderTimer
-    {
-        private static readonly Stopwatch s_sw = Stopwatch.StartNew();
-
-        public static ManualTriggerRenderTimer Instance { get; } = new();
-
-        public void RaiseTick() => Tick?.Invoke(s_sw.Elapsed);
-
-        public event Action<TimeSpan>? Tick;
-        public bool RunsInBackground => false;
-    }
-}

+ 5 - 8
src/Browser/Avalonia.Browser/Rendering/BrowserSurface.cs

@@ -40,14 +40,12 @@ internal abstract class BrowserSurface : IDisposable
 
     protected virtual void Initialize()
     {
-        CanvasHelper.OnSizeChanged(JsSurface, OnSizeChanged);
-        var w = JsSurface.GetPropertyAsDouble("width");
-        var h = JsSurface.GetPropertyAsDouble("height");
+        var w = JsSurface.GetPropertyAsInt32("width");
+        var h = JsSurface.GetPropertyAsInt32("height");
         var s = JsSurface.GetPropertyAsDouble("scaling");
-        Console.WriteLine($"Initial size: {w} {h} {s}");
-        OnSizeChanged((int)w, (int)h, s);
+        OnSizeChanged(w, h, s);
     }
-    
+
     public virtual void Dispose()
     {
         CanvasHelper.Destroy(JsSurface);
@@ -57,9 +55,8 @@ internal abstract class BrowserSurface : IDisposable
         ClientSize = default;
     }
 
-    protected virtual void OnSizeChanged(double pixelWidth, double pixelHeight, double dpr)
+    public virtual void OnSizeChanged(double pixelWidth, double pixelHeight, double dpr)
     {
-        Console.WriteLine($"OnSizeChanged: {Dispatcher.UIThread.CheckAccess()} {pixelWidth} {pixelHeight} {dpr} ");
         var oldScaling = Scaling;
         var oldClientSize = ClientSize;
         RenderSize = new PixelSize((int)pixelWidth, (int)pixelHeight);

+ 5 - 5
src/Browser/Avalonia.Browser/Rendering/RenderTargetBrowserSurface.cs

@@ -14,7 +14,7 @@ internal class RenderTargetBrowserSurface : BrowserSurface
     private readonly BrowserPlatformGraphics _graphics;
 
     private record InitParams(Compositor Compositor, BrowserPlatformGraphics Graphics);
-    
+
     private static InitParams CreateCompositor(JSObject jsSurface)
     {
         var targetId = jsSurface.GetPropertyAsInt32("targetId");
@@ -36,7 +36,7 @@ internal class RenderTargetBrowserSurface : BrowserSurface
         return [_graphics.Target];
     }
 
-    protected override void OnSizeChanged(double pixelWidth, double pixelHeight, double dpr)
+    public override void OnSizeChanged(double pixelWidth, double pixelHeight, double dpr)
     {
         _graphics.CanvasSize = (Size: new PixelSize((int)pixelWidth, (int)pixelHeight), Scaling: dpr);
         base.OnSizeChanged(pixelWidth, pixelHeight, dpr);
@@ -93,9 +93,9 @@ internal class RenderTargetBrowserSurface : BrowserSurface
         base.Dispose();
     }
 
-    public static RenderTargetBrowserSurface Create(JSObject container, IReadOnlyList<BrowserRenderingMode> modes)
+    public static RenderTargetBrowserSurface Create(JSObject container, IReadOnlyList<BrowserRenderingMode> modes, int topLevelId)
     {
-        var js = CanvasHelper.CreateRenderTargetSurface(container, modes.Select(m => (int)m).ToArray(), RenderWorker.WorkerThreadId);
+        var js = CanvasHelper.CreateRenderTargetSurface(container, modes.Select(m => (int)m).ToArray(), topLevelId, RenderWorker.WorkerThreadId);
         return new RenderTargetBrowserSurface(js);
     }
-}
+}

+ 28 - 5
src/Browser/Avalonia.Browser/WindowingPlatform.cs

@@ -1,24 +1,37 @@
 using System;
 using System.Collections.Generic;
 using System.Reflection;
+using System.Runtime.InteropServices.JavaScript;
 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;
 using Avalonia.Platform.Internal;
-using Avalonia.Rendering;
 using Avalonia.Threading;
 
 namespace Avalonia.Browser;
 
 internal class BrowserWindowingPlatform : IWindowingPlatform
 {
+    internal static ManualRawEventGrouperDispatchQueue? EventGrouperDispatchQueue;
+
     internal static readonly bool IsThreadingEnabled = DetectThreadSupport();
-    
+
+    internal static bool IsManagedDispatcherEnabled =>
+        IsThreadingEnabled &&
+        AvaloniaLocator.Current.GetService<BrowserPlatformOptions>()?.PreferManagedThreadDispatcher != false; 
+
+    // Capture initial GlobalThis, so we can use it as a contextual bridge between threads.
+    private static JSObject? s_globalThis;
+    internal static JSObject GlobalThis
+    {
+        get => s_globalThis ?? throw new InvalidOperationException("Browser backend wasn't initialized. GlobalThis is null.");
+        set => s_globalThis = value;
+    }
+
     static bool DetectThreadSupport()
     {
         // TODO Replace with public API https://github.com/dotnet/runtime/issues/77541.
@@ -68,13 +81,23 @@ internal class BrowserWindowingPlatform : IWindowingPlatform
             .Bind<ICursorFactory>().ToSingleton<CssCursorFactory>()
             .Bind<IKeyboardDevice>().ToConstant(s_keyboard)
             .Bind<IPlatformSettings>().ToSingleton<BrowserPlatformSettings>()
+            .Bind<ISystemNavigationManagerImpl>().ToSingleton<BrowserSystemNavigationManagerImpl>()
             .Bind<IWindowingPlatform>().ToConstant(instance)
             .Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()
             .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
             .Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>() { }))
             .Bind<IActivatableLifetime>().ToSingleton<BrowserActivatableLifetime>();
-        AvaloniaLocator.CurrentMutable.Bind<IDispatcherImpl>().ToSingleton<BrowserDispatcherImpl>();
-        
+        if (IsManagedDispatcherEnabled)
+        {
+            EventGrouperDispatchQueue = new();
+            AvaloniaLocator.CurrentMutable.Bind<IDispatcherImpl>().ToConstant(
+                new ManagedDispatcherImpl(new ManualRawEventGrouperDispatchQueueDispatcherInputProvider(EventGrouperDispatchQueue)));
+        }
+        else
+        {
+            AvaloniaLocator.CurrentMutable.Bind<IDispatcherImpl>().ToSingleton<BrowserDispatcherImpl>();
+        }
+
         // GC thread is the same as the main one when MT is disabled
         if (IsThreadingEnabled)
             UnmanagedBlob.SuppressFinalizerWarning = true;

+ 1 - 1
src/Browser/Avalonia.Browser/webapp/build.js

@@ -8,7 +8,7 @@ require("esbuild").build({
     bundle: true,
     minify: true,
     format: "esm",
-    target: "es2018",
+    target: "es2019",
     platform: "browser",
     sourcemap: "linked",
     loader: { ".ts": "ts" }

+ 67 - 35
src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts

@@ -1,32 +1,28 @@
+import { JsExports } from "./jsExports";
 
 export class AvaloniaDOM {
+    public static getGlobalThis() {
+        return globalThis;
+    }
+
     public static addClass(element: HTMLElement, className: string): void {
         element.classList.add(className);
     }
 
-    static observeDarkMode(observer: (isDarkMode: boolean, isHighContrast: boolean) => boolean) {
-        if (globalThis.matchMedia === undefined) {
-            return false;
-        }
-
-        const colorShemeMedia = globalThis.matchMedia("(prefers-color-scheme: dark)");
-        const prefersContrastMedia = globalThis.matchMedia("(prefers-contrast: more)");
-
-        colorShemeMedia.addEventListener("change", (args: MediaQueryListEvent) => {
-            observer(args.matches, prefersContrastMedia.matches);
-        });
-        prefersContrastMedia.addEventListener("change", (args: MediaQueryListEvent) => {
-            observer(colorShemeMedia.matches, args.matches);
-        });
+    static getFirstElementById(className: string, parent: HTMLElement | Window): Element | null {
+        const parentNode = parent instanceof Window
+            ? parent.document
+            : parent.ownerDocument;
 
-        return {
-            isDarkMode: colorShemeMedia.matches,
-            isHighContrast: prefersContrastMedia.matches
-        };
+        return parentNode.getElementById(className);
     }
 
-    static getFirstElementByClassName(className: string, parent?: HTMLElement): Element | null {
-        const elements = (parent ?? globalThis.document).getElementsByClassName(className);
+    static getFirstElementByClassName(className: string, parent: HTMLElement | Window): Element | null {
+        const parentNode = parent instanceof Window
+            ? parent.document
+            : parent;
+
+        const elements = parentNode.getElementsByClassName(className);
         return elements ? elements[0] : null;
     }
 
@@ -107,32 +103,68 @@ export class AvaloniaDOM {
         };
     }
 
-    public static isFullscreen(): boolean {
-        return document.fullscreenElement != null;
+    public static isFullscreen(globalThis: Window): boolean {
+        return globalThis.document.fullscreenElement != null;
     }
 
-    public static async setFullscreen(isFullscreen: boolean) {
+    public static async setFullscreen(globalThis: Window, isFullscreen: boolean) {
         if (isFullscreen) {
-            const doc = document.documentElement;
+            const doc = globalThis.document.documentElement;
             await doc.requestFullscreen();
         } else {
-            await document.exitFullscreen();
+            await globalThis.document.exitFullscreen();
         }
     }
 
-    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 initGlobalDomEvents(globalThis: Window): void {
+        // Init Safe Area properties.
+        globalThis.document.documentElement.style.setProperty("--av-sat", "env(safe-area-inset-top)");
+        globalThis.document.documentElement.style.setProperty("--av-sar", "env(safe-area-inset-right)");
+        globalThis.document.documentElement.style.setProperty("--av-sab", "env(safe-area-inset-bottom)");
+        globalThis.document.documentElement.style.setProperty("--av-sal", "env(safe-area-inset-left)");
+
+        // Subscribe on DarkMode changes.
+        if (globalThis.matchMedia !== undefined) {
+            const colorSchemeMedia = globalThis.matchMedia("(prefers-color-scheme: dark)");
+            const prefersContrastMedia = globalThis.matchMedia("(prefers-contrast: more)");
+
+            colorSchemeMedia.addEventListener("change", (args: MediaQueryListEvent) => {
+                JsExports.DomHelper.DarkModeChanged(args.matches, prefersContrastMedia.matches);
+            });
+            prefersContrastMedia.addEventListener("change", (args: MediaQueryListEvent) => {
+                JsExports.DomHelper.DarkModeChanged(colorSchemeMedia.matches, args.matches);
+            });
+        }
+
+        globalThis.document.addEventListener("visibilitychange", () => {
+            JsExports.DomHelper.DocumentVisibilityChanged(globalThis.document.visibilityState);
+        });
+
+        // Report initial value.
+        if (globalThis.document.visibilityState === "visible") {
+            globalThis.setTimeout(() => {
+                JsExports.DomHelper.DocumentVisibilityChanged(globalThis.document.visibilityState);
+            }, 10);
+        }
     }
 
-    public static getSafeAreaPadding(): number[] {
-        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"));
+    public static getSafeAreaPadding(globalThis: Window): number[] {
+        const top = parseFloat(getComputedStyle(globalThis.document.documentElement).getPropertyValue("--av-sat"));
+        const bottom = parseFloat(getComputedStyle(globalThis.document.documentElement).getPropertyValue("--av-sab"));
+        const left = parseFloat(getComputedStyle(globalThis.document.documentElement).getPropertyValue("--av-sal"));
+        const right = parseFloat(getComputedStyle(globalThis.document.documentElement).getPropertyValue("--av-sar"));
 
         return [left, top, bottom, right];
     }
+
+    public static getDarkMode(globalThis: Window): number[] {
+        if (globalThis.matchMedia === undefined) return [0, 0];
+
+        const colorSchemeMedia = globalThis.matchMedia("(prefers-color-scheme: dark)");
+        const prefersContrastMedia = globalThis.matchMedia("(prefers-contrast: more)");
+        return [
+            colorSchemeMedia.matches ? 1 : 0,
+            prefersContrastMedia.matches ? 1 : 0
+        ];
+    }
 }

+ 96 - 88
src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts

@@ -1,4 +1,5 @@
 import { CaretHelper } from "./caretHelper";
+import { JsExports } from "./jsExports";
 
 enum RawInputModifiers {
     None = 0,
@@ -54,7 +55,7 @@ export class InputHelper {
         this.clipboardState = ClipboardState.Ready;
     }
 
-    public static async readClipboardText(): Promise<string> {
+    public static async readClipboardText(globalThis: Window): Promise<string> {
         if (globalThis.navigator.clipboard.readText) {
             return await globalThis.navigator.clipboard.readText();
         } else {
@@ -72,23 +73,38 @@ export class InputHelper {
         }
     }
 
-    public static subscribeKeyEvents(
-        element: HTMLInputElement,
-        keyDownCallback: (code: string, key: string, modifiers: string) => boolean,
-        keyUpCallback: (code: string, key: string, modifiers: string) => boolean) {
+    public static async writeClipboardText(globalThis: Window, text: string): Promise<void> {
+        return await globalThis.navigator.clipboard.writeText(text);
+    }
+
+    public static subscribeInputEvents(element: HTMLInputElement, topLevelId: number) {
+        const keySub = this.subscribeKeyEvents(element, topLevelId);
+        const pointerSub = this.subscribePointerEvents(element, topLevelId);
+        const textSub = this.subscribeTextEvents(element, topLevelId);
+        const dndSub = this.subscribeDropEvents(element, topLevelId);
+        const paneSub = this.subscribeKeyboardGeometryChange(element, topLevelId);
+
+        return () => {
+            keySub();
+            pointerSub();
+            textSub();
+            dndSub();
+            paneSub();
+        };
+    }
+
+    public static subscribeKeyEvents(element: HTMLInputElement, topLevelId: number) {
         const keyDownHandler = (args: KeyboardEvent) => {
-            if (keyDownCallback(args.code, args.key, this.getModifiers(args))) {
-                if (this.clipboardState !== ClipboardState.Pending) {
-                    args.preventDefault();
-                }
+            JsExports.InputHelper.OnKeyDown(topLevelId, args.code, args.key, this.getModifiers(args));
+            if (this.clipboardState !== ClipboardState.Pending) {
+                args.preventDefault();
             }
         };
         element.addEventListener("keydown", keyDownHandler);
 
         const keyUpHandler = (args: KeyboardEvent) => {
-            if (keyUpCallback(args.code, args.key, this.getModifiers(args))) {
-                args.preventDefault();
-            }
+            JsExports.InputHelper.OnKeyUp(topLevelId, args.code, args.key, this.getModifiers(args));
+            args.preventDefault();
             if (this.rejectClipboard) {
                 this.rejectClipboard();
             }
@@ -104,14 +120,9 @@ export class InputHelper {
 
     public static subscribeTextEvents(
         element: HTMLInputElement,
-        beforeInputCallback: (args: InputEvent, start: number, end: number) => boolean,
-        compositionStartCallback: (args: CompositionEvent) => boolean,
-        compositionUpdateCallback: (args: CompositionEvent) => boolean,
-        compositionEndCallback: (args: CompositionEvent) => boolean) {
+        topLevelId: number) {
         const compositionStartHandler = (args: CompositionEvent) => {
-            if (compositionStartCallback(args)) {
-                args.preventDefault();
-            }
+            JsExports.InputHelper.OnCompositionStart(topLevelId);
         };
         element.addEventListener("compositionstart", compositionStartHandler);
 
@@ -128,23 +139,19 @@ export class InputHelper {
                 start = 2;
                 end = start + 2;
             }
-            if (beforeInputCallback(args, start, end)) {
-                args.preventDefault();
-            }
+
+            JsExports.InputHelper.OnBeforeInput(topLevelId, args.inputType, start, end);
         };
         element.addEventListener("beforeinput", beforeInputHandler);
 
         const compositionUpdateHandler = (args: CompositionEvent) => {
-            if (compositionUpdateCallback(args)) {
-                args.preventDefault();
-            }
+            JsExports.InputHelper.OnCompositionUpdate(topLevelId, args.data);
         };
         element.addEventListener("compositionupdate", compositionUpdateHandler);
 
         const compositionEndHandler = (args: CompositionEvent) => {
-            if (compositionEndCallback(args)) {
-                args.preventDefault();
-            }
+            JsExports.InputHelper.OnCompositionEnd(topLevelId, args.data);
+            args.preventDefault();
         };
         element.addEventListener("compositionend", compositionEndHandler);
 
@@ -157,34 +164,38 @@ export class InputHelper {
 
     public static subscribePointerEvents(
         element: HTMLInputElement,
-        pointerMoveCallback: (args: PointerEvent) => boolean,
-        pointerDownCallback: (args: PointerEvent) => boolean,
-        pointerUpCallback: (args: PointerEvent) => boolean,
-        pointerCancelCallback: (args: PointerEvent) => boolean,
-        wheelCallback: (args: WheelEvent) => boolean
+        topLevelId: number
     ) {
         const pointerMoveHandler = (args: PointerEvent) => {
-            pointerMoveCallback(args);
+            JsExports.InputHelper.OnPointerMove(
+                topLevelId, args.pointerType, args.pointerId, args.offsetX, args.offsetY,
+                args.pressure, args.tiltX, args.tiltY, args.twist, this.getModifiers(args), args);
             args.preventDefault();
         };
 
         const pointerDownHandler = (args: PointerEvent) => {
-            pointerDownCallback(args);
+            JsExports.InputHelper.OnPointerDown(
+                topLevelId, args.pointerType, args.pointerId, args.button, args.offsetX, args.offsetY,
+                args.pressure, args.tiltX, args.tiltY, args.twist, this.getModifiers(args));
             args.preventDefault();
         };
 
         const pointerUpHandler = (args: PointerEvent) => {
-            pointerUpCallback(args);
+            JsExports.InputHelper.OnPointerUp(
+                topLevelId, args.pointerType, args.pointerId, args.button, args.offsetX, args.offsetY,
+                args.pressure, args.tiltX, args.tiltY, args.twist, this.getModifiers(args));
             args.preventDefault();
         };
 
         const pointerCancelHandler = (args: PointerEvent) => {
-            pointerCancelCallback(args);
-            args.preventDefault();
+            JsExports.InputHelper.OnPointerCancel(
+                topLevelId, args.pointerType, args.pointerId, args.offsetX, args.offsetY,
+                args.pressure, args.tiltX, args.tiltY, args.twist, this.getModifiers(args));
         };
 
         const wheelHandler = (args: WheelEvent) => {
-            wheelCallback(args);
+            JsExports.InputHelper.OnWheel(
+                topLevelId, args.offsetX, args.offsetY, args.deltaX, args.deltaY, this.getModifiers(args));
             args.preventDefault();
         };
 
@@ -203,72 +214,59 @@ export class InputHelper {
         };
     }
 
-    public static subscribeInputEvents(
-        element: HTMLInputElement,
-        inputCallback: (value: string) => boolean
-    ) {
-        const inputHandler = (args: Event) => {
-            if (inputCallback((args as any).value)) {
-                args.preventDefault();
-            }
-        };
-        element.addEventListener("input", inputHandler);
-
-        return () => {
-            element.removeEventListener("input", inputHandler);
-        };
-    }
-
     public static subscribeDropEvents(
         element: HTMLInputElement,
-        dragEvent: (args: any) => boolean
+        topLevelId: number
     ) {
-        const dragHandler = (args: Event) => {
-            if (dragEvent(args as any)) {
-                args.preventDefault();
-            }
+        const handler = (args: DragEvent) => {
+            const dataObject = args.dataTransfer;
+            JsExports.InputHelper.OnDragDrop(topLevelId, args.type, args.offsetX, args.offsetY, this.getModifiers(args), dataObject?.effectAllowed, dataObject);
         };
-        element.addEventListener("dragover", dragHandler);
-        element.addEventListener("dragenter", dragHandler);
-        element.addEventListener("dragleave", dragHandler);
-        element.addEventListener("drop", dragHandler);
+        const overAndDropHandler = (args: DragEvent) => {
+            args.preventDefault();
+            handler(args);
+        };
+        element.addEventListener("dragover", overAndDropHandler);
+        element.addEventListener("dragenter", handler);
+        element.addEventListener("dragleave", handler);
+        element.addEventListener("drop", overAndDropHandler);
 
         return () => {
-            element.removeEventListener("dragover", dragHandler);
-            element.removeEventListener("dragenter", dragHandler);
-            element.removeEventListener("dragleave", dragHandler);
-            element.removeEventListener("drop", dragHandler);
+            element.removeEventListener("dragover", overAndDropHandler);
+            element.removeEventListener("dragenter", handler);
+            element.removeEventListener("dragleave", handler);
+            element.removeEventListener("drop", overAndDropHandler);
         };
     }
 
-    public static getCoalescedEvents(pointerEvent: PointerEvent): PointerEvent[] {
-        return pointerEvent.getCoalescedEvents();
+    public static getCoalescedEvents(pointerEvent: PointerEvent): number[] {
+        return pointerEvent.getCoalescedEvents()
+            .flatMap(e => [e.offsetX, e.offsetY, e.pressure, e.tiltX, e.tiltY, e.twist]);
     }
 
     public static subscribeKeyboardGeometryChange(
         element: HTMLInputElement,
-        handler: (args: any) => boolean) {
+        topLevelId: number) {
         if ("virtualKeyboard" in navigator) {
             // (navigator as any).virtualKeyboard.overlaysContent = true;
-            (navigator as any).virtualKeyboard.addEventListener("geometrychange", (event: any) => {
+            const listener = (event: any) => {
                 const elementRect = element.getBoundingClientRect();
                 const keyboardRect = event.target.boundingRect as DOMRect;
-                handler({
-                    x: keyboardRect.x - elementRect.x,
-                    y: keyboardRect.y - elementRect.y,
-                    width: keyboardRect.width,
-                    height: keyboardRect.height
-                });
-            });
+
+                JsExports.InputHelper.OnKeyboardGeometryChange(
+                    topLevelId,
+                    keyboardRect.x - elementRect.x,
+                    keyboardRect.y - elementRect.y,
+                    keyboardRect.width,
+                    keyboardRect.height);
+            };
+            (navigator as any).virtualKeyboard.addEventListener("geometrychange", listener);
+            return () => {
+                (navigator as any).virtualKeyboard.removeEventListener("geometrychange", listener);
+            };
         }
-    }
 
-    public static subscribeVisibilityChange(
-        handler: (state: boolean) => void): boolean {
-        document.addEventListener("visibilitychange", () => {
-            handler(document.visibilityState === "visible");
-        });
-        return document.visibilityState === "visible";
+        return () => {};
     }
 
     public static clearInput(inputElement: HTMLInputElement) {
@@ -316,7 +314,7 @@ export class InputHelper {
         inputElement.style.width = `${inputElement.scrollWidth}px`;
     }
 
-    private static getModifiers(args: KeyboardEvent): string {
+    private static getModifiers(args: KeyboardEvent | PointerEvent | WheelEvent | DragEvent): number {
         let modifiers = RawInputModifiers.None;
 
         if (args.ctrlKey) { modifiers |= RawInputModifiers.Control; }
@@ -324,7 +322,17 @@ export class InputHelper {
         if (args.shiftKey) { modifiers |= RawInputModifiers.Shift; }
         if (args.metaKey) { modifiers |= RawInputModifiers.Meta; }
 
-        return modifiers.toString();
+        const buttons = (args as PointerEvent).buttons;
+        if (buttons) {
+            if (buttons & 1) { modifiers |= RawInputModifiers.LeftMouseButton; }
+            if (buttons & 2) { modifiers |= (args.type === "pen" ? RawInputModifiers.PenBarrelButton : RawInputModifiers.RightMouseButton); }
+            if (buttons & 4) { modifiers |= RawInputModifiers.MiddleMouseButton; }
+            if (buttons & 8) { modifiers |= RawInputModifiers.XButton1MouseButton; }
+            if (buttons & 16) { modifiers |= RawInputModifiers.XButton2MouseButton; }
+            if (buttons & 32) { modifiers |= RawInputModifiers.PenEraser; }
+        }
+
+        return modifiers;
     }
 
     public static setPointerCapture(containerElement: HTMLInputElement, pointerId: number): void {

+ 16 - 0
src/Browser/Avalonia.Browser/webapp/modules/avalonia/jsExports.ts

@@ -1,6 +1,22 @@
 export class JsExports {
     public static resolvedExports?: any;
     public static exportsPromise: Promise<any>;
+
+    public static get InputHelper(): any {
+        return this.resolvedExports?.Avalonia.Browser.Interop.InputHelper;
+    }
+
+    public static get DomHelper(): any {
+        return this.resolvedExports?.Avalonia.Browser.Interop.DomHelper;
+    }
+
+    public static get TimerHelper(): any {
+        return this.resolvedExports?.Avalonia.Browser.Interop.TimerHelper;
+    }
+
+    public static get CanvasHelper(): any {
+        return this.resolvedExports?.Avalonia.Browser.Interop.CanvasHelper;
+    }
 }
 async function resolveExports (): Promise<any> {
     const runtimeApi = await globalThis.getDotnetRuntime(0);

+ 5 - 19
src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/canvasSurface.ts

@@ -2,21 +2,18 @@ import { ResizeHandler } from "./resizeHandler";
 import { WebRenderTargetRegistry } from "./webRenderTargetRegistry";
 import { AvaloniaDOM } from "../dom";
 import { BrowserRenderingMode } from "./renderingMode";
+import { JsExports } from "../jsExports";
 
 export class CanvasSurface {
     public targetId: number;
     private sizeParams?: [number, number, number];
-    private sizeChangedCallback?: (width: number, height: number, dpr: number) => void;
 
-    constructor(public canvas: HTMLCanvasElement, modes: BrowserRenderingMode[], threadId: number) {
+    constructor(public canvas: HTMLCanvasElement, modes: BrowserRenderingMode[], topLevelId: number, threadId: number) {
         this.targetId = WebRenderTargetRegistry.create(threadId, canvas, modes);
-        // No need to ubsubscribe, 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);
-            }
+            JsExports.CanvasHelper?.OnSizeChanged(topLevelId, width, height, dpr);
         });
     }
 
@@ -36,20 +33,13 @@ export class CanvasSurface {
     }
 
     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;
-        // if (this.sizeParams) { this.sizeChangedCallback(this.sizeParams[0], this.sizeParams[1], this.sizeParams[2]); }
-    }
-
-    public static create(container: HTMLElement, modes: BrowserRenderingMode[], threadId: number): CanvasSurface {
+    public static create(container: HTMLElement, modes: BrowserRenderingMode[], topLevelId: number, threadId: number): CanvasSurface {
         const canvas = AvaloniaDOM.createAvaloniaCanvas(container);
         AvaloniaDOM.attachCanvas(container, canvas);
         try {
-            return new CanvasSurface(canvas, modes, threadId);
+            return new CanvasSurface(canvas, modes, topLevelId, threadId);
         } catch (ex) {
             AvaloniaDOM.detachCanvas(container, canvas);
             throw ex;
@@ -59,8 +49,4 @@ export class CanvasSurface {
     public static destroy(surface: CanvasSurface) {
         surface.destroy();
     }
-
-    public static onSizeChanged(surface: CanvasSurface, sizeChangedCallback: (width: number, height: number, dpr: number) => void) {
-        surface.onSizeChanged(sizeChangedCallback);
-    }
 }

+ 3 - 9
src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts

@@ -3,24 +3,18 @@ import { JsExports } from "./jsExports";
 export class TimerHelper {
     public static runAnimationFrames(): void {
         function render(time: number) {
-            if (JsExports.resolvedExports != null) {
-                JsExports.resolvedExports.Avalonia.Browser.Interop.TimerHelper.JsExportOnAnimationFrame(time);
-            }
+            JsExports.TimerHelper?.JsExportOnAnimationFrame();
             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"); }
+        JsExports.TimerHelper?.JsExportOnTimeout();
     }
 
     static onInterval() {
-        if (JsExports.resolvedExports != null) {
-            JsExports.resolvedExports.Avalonia.Browser.Interop.TimerHelper.JsExportOnInterval();
-        } else { console.error("TimerHelper.onInterval call while uninitialized"); }
+        JsExports.TimerHelper?.JsExportOnInterval();
     }
 
     public static setTimeout(interval: number): number {

+ 3 - 2
src/Browser/Avalonia.Browser/webapp/tsconfig.json

@@ -1,6 +1,6 @@
 {
     "compilerOptions": {
-      "target": "es2018",
+      "target": "es2019",
       "module": "es2020",
       "strict": true,
       "sourceMap": true,
@@ -11,6 +11,7 @@
       "lib": [
         "dom",
         "es2018",
+        "es2019",
         "esnext.asynciterable"
       ]
     },
@@ -18,4 +19,4 @@
       "node_modules"
     ]
   }
-  
+