diff --git a/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj b/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj
index 309d54722c..b921945f25 100644
--- a/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj
+++ b/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj
@@ -1,9 +1,7 @@
-
+
$(AvsCurrentBrowserTargetFramework)
- browser-wasm
- wwwroot/main.js
- Exe
+ false
true
true
5
@@ -22,10 +20,6 @@
-
-
-
-
diff --git a/samples/ControlCatalog.Browser/EmbedSample.Browser.cs b/samples/ControlCatalog.Browser/EmbedSample.Browser.cs
index 7bf3891a81..b8bc4ed35c 100644
--- a/samples/ControlCatalog.Browser/EmbedSample.Browser.cs
+++ b/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);
}
}
diff --git a/samples/ControlCatalog.Browser/Program.cs b/samples/ControlCatalog.Browser/Program.cs
index c50f1dcbdd..95cce73eb3 100644
--- a/samples/ControlCatalog.Browser/Program.cs
+++ b/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.
diff --git a/src/Browser/Avalonia.Browser/Avalonia.Browser.csproj b/src/Browser/Avalonia.Browser/Avalonia.Browser.csproj
index e84e9a95b7..440378d6fc 100644
--- a/src/Browser/Avalonia.Browser/Avalonia.Browser.csproj
+++ b/src/Browser/Avalonia.Browser/Avalonia.Browser.csproj
@@ -54,4 +54,10 @@
+
+
+ RawEventGrouping.cs
+
+
+
diff --git a/src/Browser/Avalonia.Browser/AvaloniaView.cs b/src/Browser/Avalonia.Browser/AvaloniaView.cs
index ff397a14b8..42d5d7dca6 100644
--- a/src/Browser/Avalonia.Browser/AvaloniaView.cs
+++ b/src/Browser/Avalonia.Browser/AvaloniaView.cs
@@ -12,7 +12,7 @@ namespace Avalonia.Browser
/// ID of the html element where avalonia content should be rendered.
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");
diff --git a/src/Browser/Avalonia.Browser/BrowserActivatableLifetime.cs b/src/Browser/Avalonia.Browser/BrowserActivatableLifetime.cs
index 30996d55df..bd8211de88 100644
--- a/src/Browser/Avalonia.Browser/BrowserActivatableLifetime.cs
+++ b/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? Activated;
- public event EventHandler? Deactivated;
-
- public bool TryLeaveBackground() => false;
- public bool TryEnterBackground() => false;
}
diff --git a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs
index 02fa530fae..c05548184f 100644
--- a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs
+++ b/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.
///
public bool PreferFileDialogPolyfill { get; set; }
+
+ ///
+ /// 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.
+ ///
+ public bool? PreferManagedThreadDispatcher { get; set; } = true;
}
public static class BrowserAppBuilder
@@ -63,8 +71,9 @@ public static class BrowserAppBuilder
/// Application builder.
/// ID of the html element where avalonia content should be rendered.
/// Browser backend specific options.
- 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);
+ }
}
///
@@ -102,12 +138,15 @@ public static class BrowserAppBuilder
internal static async Task PreSetupBrowser(AppBuilder builder, BrowserPlatformOptions? options)
{
- options ??= new BrowserPlatformOptions();
+ options ??= AvaloniaLocator.Current.GetService() ?? new BrowserPlatformOptions();
options.FrameworkAssetPathResolver ??= fileName => $"./{fileName}";
AvaloniaLocator.CurrentMutable.Bind().ToConstant(options);
await AvaloniaModule.ImportMain();
+
+ BrowserWindowingPlatform.GlobalThis = DomHelper.GetGlobalThis();
+
if (BrowserWindowingPlatform.IsThreadingEnabled)
{
await RenderWorker.InitializeAsync();
diff --git a/src/Browser/Avalonia.Browser/BrowserInputHandler.cs b/src/Browser/Avalonia.Browser/BrowserInputHandler.cs
index b264c86db5..d9fe7da666 100644
--- a/src/Browser/Avalonia.Browser/BrowserInputHandler.cs
+++ b/src/Browser/Avalonia.Browser/BrowserInputHandler.cs
@@ -21,8 +21,9 @@ internal class BrowserInputHandler
private IInputRoot? _inputRoot;
private static readonly PooledList 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?>(() =>
+ Lazy?>? 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?>(() =>
{
- 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?>? 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();
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);
+ }
}
diff --git a/src/Browser/Avalonia.Browser/BrowserInputPane.cs b/src/Browser/Avalonia.Browser/BrowserInputPane.cs
index be132ee956..792e6974e6 100644
--- a/src/Browser/Avalonia.Browser/BrowserInputPane.cs
+++ b/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))
diff --git a/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs
index 2c8eb03c72..dd34f744c1 100644
--- a/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs
+++ b/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]);
}
diff --git a/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs b/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs
index fa647d31b7..2a3cef4334 100644
--- a/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs
+++ b/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;
+ }
}
}
}
diff --git a/src/Browser/Avalonia.Browser/BrowserSystemNavigationManager.cs b/src/Browser/Avalonia.Browser/BrowserSystemNavigationManager.cs
index bc38067f4a..275fb44d1e 100644
--- a/src/Browser/Avalonia.Browser/BrowserSystemNavigationManager.cs
+++ b/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? BackRequested;
+ public event EventHandler? 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;
}
}
diff --git a/src/Browser/Avalonia.Browser/BrowserTextInputMethod.cs b/src/Browser/Avalonia.Browser/BrowserTextInputMethod.cs
index 8cb4296c48..fbcbf15ee5 100644
--- a/src/Browser/Avalonia.Browser/BrowserTextInputMethod.cs
+++ b/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;
}
}
diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs
index dc997b840a..2a776d719e 100644
--- a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs
+++ b/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> 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(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() ?? 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();
}
if (featureType == typeof(INativeControlHostImpl))
@@ -169,7 +180,7 @@ namespace Avalonia.Browser
if (featureType == typeof(IInputPane))
{
- return _inputPane;
+ return _inputHandler.InputPane;
}
return null;
diff --git a/src/Browser/Avalonia.Browser/ClipboardImpl.cs b/src/Browser/Avalonia.Browser/ClipboardImpl.cs
index c4f5e90777..5df09e555d 100644
--- a/src/Browser/Avalonia.Browser/ClipboardImpl.cs
+++ b/src/Browser/Avalonia.Browser/ClipboardImpl.cs
@@ -10,12 +10,12 @@ namespace Avalonia.Browser
{
public Task 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("");
diff --git a/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs b/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs
index 7274fa582f..f7198a53e4 100644
--- a/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs
+++ b/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>]
- // TODO: this callback should be . Revert after next .NET 9 preview.
- Action 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);
diff --git a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs
index a567973131..6817e6a6da 100644
--- a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs
+++ b/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>]
- Action 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() as BrowserPlatformSettings)?.OnValuesChanged(isDarkMode, isHighContrast);
+ return Task.CompletedTask;
+ }
+
+ [JSExport]
+ public static Task DocumentVisibilityChanged(string visibilityState)
+ {
+ (AvaloniaLocator.Current.GetService() as BrowserActivatableLifetime)?.OnVisibilityStateChanged(visibilityState);
+ return Task.CompletedTask;
+ }
}
diff --git a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs
index 928c0e23c6..29ddb36e9e 100644
--- a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs
+++ b/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>]
- // TODO: this callback should be . Revert after next .NET 9 preview.
- Func keyDown,
- [JSMarshalAs>]
- // TODO: this callback should be . Revert after next .NET 9 preview.
- Func keyUp);
-
- [JSImport("InputHelper.subscribeTextEvents", AvaloniaModule.MainModuleName)]
- public static partial void SubscribeTextEvents(
- JSObject htmlElement,
- [JSMarshalAs>]
- Func onBeforeInput,
- [JSMarshalAs>]
- Func onCompositionStart,
- [JSMarshalAs>]
- Func onCompositionUpdate,
- [JSMarshalAs>]
- Func onCompositionEnd);
-
- [JSImport("InputHelper.subscribePointerEvents", AvaloniaModule.MainModuleName)]
- public static partial void SubscribePointerEvents(
- JSObject htmlElement,
- [JSMarshalAs>]
- Func pointerMove,
- [JSMarshalAs>]
- Func pointerDown,
- [JSMarshalAs>]
- Func pointerUp,
- [JSMarshalAs>]
- Func pointerCancel,
- [JSMarshalAs>]
- Func wheel);
+ public static Task RedirectInputAsync(int topLevelId, Action 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>]
- Func input);
-
- [JSImport("InputHelper.subscribeDropEvents", AvaloniaModule.MainModuleName)]
- public static partial void SubscribeDropEvents(JSObject containerElement,
- [JSMarshalAs>] Func dragEvent);
-
- [JSImport("InputHelper.subscribeKeyboardGeometryChange", AvaloniaModule.MainModuleName)]
- public static partial void SubscribeKeyboardGeometryChange(JSObject containerElement,
- [JSMarshalAs>] Func handler);
-
- [JSImport("InputHelper.subscribeVisibilityChange", AvaloniaModule.MainModuleName)]
- public static partial bool SubscribeVisibilityChange([JSMarshalAs>] Action 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] 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] 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] 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] 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>]
- public static partial JSObject[] GetCoalescedEvents(JSObject pointerEvent);
+ [return: JSMarshalAs>]
+ 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 ReadClipboardTextAsync();
+ public static partial Task 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] long pointerId);
+ public static partial void
+ SetPointerCapture(JSObject containerElement, [JSMarshalAs] long pointerId);
[JSImport("InputHelper.releasePointerCapture", AvaloniaModule.MainModuleName)]
- public static partial void ReleasePointerCapture(JSObject containerElement, [JSMarshalAs] long pointerId);
-
- [JSImport("globalThis.navigator.clipboard.writeText")]
- public static partial Task WriteClipboardTextAsync(string text);
+ public static partial void ReleasePointerCapture(JSObject containerElement,
+ [JSMarshalAs] long pointerId);
}
diff --git a/src/Browser/Avalonia.Browser/Interop/NavigationHelper.cs b/src/Browser/Avalonia.Browser/Interop/NavigationHelper.cs
index b0032962f9..b79fe2a03d 100644
--- a/src/Browser/Avalonia.Browser/Interop/NavigationHelper.cs
+++ b/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>] Func backHandlerCallback);
+ public static Task OnBackRequested()
+ {
+ var handled = (AvaloniaLocator.Current.GetService() as BrowserSystemNavigationManagerImpl)?
+ .OnBackRequested() ?? false;
+ return Task.FromResult(handled);
+ }
+
[JSImport("window.open")]
public static partial JSObject? WindowOpen(string uri, string target);
}
diff --git a/src/Browser/Avalonia.Browser/ManualTriggerRenderTimer.cs b/src/Browser/Avalonia.Browser/ManualTriggerRenderTimer.cs
deleted file mode 100644
index e9a314e823..0000000000
--- a/src/Browser/Avalonia.Browser/ManualTriggerRenderTimer.cs
+++ /dev/null
@@ -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? Tick;
- public bool RunsInBackground => false;
- }
-}
diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserSurface.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserSurface.cs
index 9a4726481a..e93cdee703 100644
--- a/src/Browser/Avalonia.Browser/Rendering/BrowserSurface.cs
+++ b/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);
diff --git a/src/Browser/Avalonia.Browser/Rendering/RenderTargetBrowserSurface.cs b/src/Browser/Avalonia.Browser/Rendering/RenderTargetBrowserSurface.cs
index c9fc086675..f97b329734 100644
--- a/src/Browser/Avalonia.Browser/Rendering/RenderTargetBrowserSurface.cs
+++ b/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 modes)
+ public static RenderTargetBrowserSurface Create(JSObject container, IReadOnlyList 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);
}
-}
\ No newline at end of file
+}
diff --git a/src/Browser/Avalonia.Browser/WindowingPlatform.cs b/src/Browser/Avalonia.Browser/WindowingPlatform.cs
index bcc0a4a801..82d546e8bb 100644
--- a/src/Browser/Avalonia.Browser/WindowingPlatform.cs
+++ b/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()?.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().ToSingleton()
.Bind().ToConstant(s_keyboard)
.Bind().ToSingleton()
+ .Bind().ToSingleton()
.Bind().ToConstant(instance)
.Bind().ToSingleton()
.Bind().ToSingleton()
.Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { }))
.Bind().ToSingleton();
- AvaloniaLocator.CurrentMutable.Bind().ToSingleton();
-
+ if (IsManagedDispatcherEnabled)
+ {
+ EventGrouperDispatchQueue = new();
+ AvaloniaLocator.CurrentMutable.Bind().ToConstant(
+ new ManagedDispatcherImpl(new ManualRawEventGrouperDispatchQueueDispatcherInputProvider(EventGrouperDispatchQueue)));
+ }
+ else
+ {
+ AvaloniaLocator.CurrentMutable.Bind().ToSingleton();
+ }
+
// GC thread is the same as the main one when MT is disabled
if (IsThreadingEnabled)
UnmanagedBlob.SuppressFinalizerWarning = true;
diff --git a/src/Browser/Avalonia.Browser/webapp/build.js b/src/Browser/Avalonia.Browser/webapp/build.js
index 3b278e7dcb..a8b4ca3bff 100644
--- a/src/Browser/Avalonia.Browser/webapp/build.js
+++ b/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" }
diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts
index d51ee4b184..175e51c0da 100644
--- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts
+++ b/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
+ ];
+ }
}
diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts
index df4fc19e6b..5d14597642 100644
--- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts
+++ b/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 {
+ public static async readClipboardText(globalThis: Window): Promise {
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 {
+ 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 {
diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/jsExports.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/jsExports.ts
index 18fde7449a..6a148d57e5 100644
--- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/jsExports.ts
+++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/jsExports.ts
@@ -1,6 +1,22 @@
export class JsExports {
public static resolvedExports?: any;
public static exportsPromise: Promise;
+
+ 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 {
const runtimeApi = await globalThis.getDotnetRuntime(0);
diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/canvasSurface.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/canvasSurface.ts
index 35c4455b4b..ed53492365 100644
--- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/canvasSurface.ts
+++ b/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);
- }
}
diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts
index 3ccd669293..ca12392fd2 100644
--- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts
+++ b/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 {
diff --git a/src/Browser/Avalonia.Browser/webapp/tsconfig.json b/src/Browser/Avalonia.Browser/webapp/tsconfig.json
index 1450ce4c57..b3d7c9045f 100644
--- a/src/Browser/Avalonia.Browser/webapp/tsconfig.json
+++ b/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"
]
}
-
\ No newline at end of file
+