Browse Source

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
pull/15936/head
Max Katz 2 years ago
committed by GitHub
parent
commit
f140033e42
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 10
      samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj
  2. 2
      samples/ControlCatalog.Browser/EmbedSample.Browser.cs
  3. 9
      samples/ControlCatalog.Browser/Program.cs
  4. 6
      src/Browser/Avalonia.Browser/Avalonia.Browser.csproj
  5. 4
      src/Browser/Avalonia.Browser/AvaloniaView.cs
  6. 32
      src/Browser/Avalonia.Browser/BrowserActivatableLifetime.cs
  7. 49
      src/Browser/Avalonia.Browser/BrowserAppBuilder.cs
  8. 227
      src/Browser/Avalonia.Browser/BrowserInputHandler.cs
  9. 13
      src/Browser/Avalonia.Browser/BrowserInputPane.cs
  10. 11
      src/Browser/Avalonia.Browser/BrowserInsetsManager.cs
  11. 20
      src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs
  12. 23
      src/Browser/Avalonia.Browser/BrowserSystemNavigationManager.cs
  13. 63
      src/Browser/Avalonia.Browser/BrowserTextInputMethod.cs
  14. 37
      src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs
  15. 4
      src/Browser/Avalonia.Browser/ClipboardImpl.cs
  16. 33
      src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs
  17. 45
      src/Browser/Avalonia.Browser/Interop/DomHelper.cs
  18. 142
      src/Browser/Avalonia.Browser/Interop/InputHelper.cs
  19. 9
      src/Browser/Avalonia.Browser/Interop/NavigationHelper.cs
  20. 18
      src/Browser/Avalonia.Browser/ManualTriggerRenderTimer.cs
  21. 13
      src/Browser/Avalonia.Browser/Rendering/BrowserSurface.cs
  22. 10
      src/Browser/Avalonia.Browser/Rendering/RenderTargetBrowserSurface.cs
  23. 33
      src/Browser/Avalonia.Browser/WindowingPlatform.cs
  24. 2
      src/Browser/Avalonia.Browser/webapp/build.js
  25. 102
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts
  26. 184
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts
  27. 16
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/jsExports.ts
  28. 24
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/canvasSurface.ts
  29. 12
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts
  30. 5
      src/Browser/Avalonia.Browser/webapp/tsconfig.json

10
samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj

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

2
samples/ControlCatalog.Browser/EmbedSample.Browser.cs

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

9
samples/ControlCatalog.Browser/Program.cs

@ -33,10 +33,13 @@ internal partial class Program
}) })
.StartBrowserAppAsync("out", options); .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. // Test with multiple AvaloniaView at once.

6
src/Browser/Avalonia.Browser/Avalonia.Browser.csproj

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

4
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> /// <param name="divId">ID of the html element where avalonia content should be rendered.</param>
public AvaloniaView(string divId) 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.")) 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. // Try to get local splash-screen of the specific host.
// If couldn't find - get global one by ID for compatibility. // If couldn't find - get global one by ID for compatibility.
var splash = DomHelper.GetElementsByClassName("avalonia-splash", host) var splash = DomHelper.GetElementsByClassName("avalonia-splash", host)
?? DomHelper.GetElementById("avalonia-splash"); ?? DomHelper.GetElementById("avalonia-splash", BrowserWindowingPlatform.GlobalThis);
if (splash is not null) if (splash is not null)
{ {
DomHelper.AddCssClass(splash, "splash-close"); DomHelper.AddCssClass(splash, "splash-close");

32
src/Browser/Avalonia.Browser/BrowserActivatableLifetime.cs

@ -1,36 +1,20 @@
using System;
using Avalonia.Browser.Interop; using Avalonia.Browser.Interop;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Threading;
namespace Avalonia.Browser; 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; OnActivated(ActivationKind.Background);
(visible ? Activated : Deactivated)?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background)); }
}); else
// Trigger Activated as an initial state, if web page is visible, and wasn't hidden during initialization.
if (initiallyVisible == true)
{ {
_ = Dispatcher.UIThread.InvokeAsync(() => OnDeactivated(ActivationKind.Background);
{
if (initiallyVisible == true)
{
Activated?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background));
}
}, DispatcherPriority.Background);
} }
} }
public event EventHandler<ActivatedEventArgs>? Activated;
public event EventHandler<ActivatedEventArgs>? Deactivated;
public bool TryLeaveBackground() => false;
public bool TryEnterBackground() => false;
} }

49
src/Browser/Avalonia.Browser/BrowserAppBuilder.cs

@ -1,8 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Browser.Interop; using Avalonia.Browser.Interop;
using Avalonia.Browser.Rendering; using Avalonia.Browser.Rendering;
using Avalonia.Controls;
using Avalonia.Metadata; using Avalonia.Metadata;
namespace Avalonia.Browser; 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. /// For more details, see https://github.com/jimmywarting/native-file-system-adapter#a-note-when-downloading-with-the-polyfilled-version.
/// </summary> /// </summary>
public bool PreferFileDialogPolyfill { get; set; } 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 public static class BrowserAppBuilder
@ -63,8 +71,9 @@ public static class BrowserAppBuilder
/// <param name="builder">Application builder.</param> /// <param name="builder">Application builder.</param>
/// <param name="mainDivId">ID of the html element where avalonia content should be rendered.</param> /// <param name="mainDivId">ID of the html element where avalonia content should be rendered.</param>
/// <param name="options">Browser backend specific options.</param> /// <param name="options">Browser backend specific options.</param>
public static async Task StartBrowserAppAsync(this AppBuilder builder, string mainDivId, public static async Task StartBrowserAppAsync(
BrowserPlatformOptions? options = null) this AppBuilder builder,
string mainDivId, BrowserPlatformOptions? options = null)
{ {
if (mainDivId is null) if (mainDivId is null)
{ {
@ -78,8 +87,35 @@ public static class BrowserAppBuilder
.AfterApplicationSetup(_ => .AfterApplicationSetup(_ =>
{ {
lifetime.View = new AvaloniaView(mainDivId); 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> /// <summary>
@ -102,12 +138,15 @@ public static class BrowserAppBuilder
internal static async Task<AppBuilder> PreSetupBrowser(AppBuilder builder, BrowserPlatformOptions? options) internal static async Task<AppBuilder> PreSetupBrowser(AppBuilder builder, BrowserPlatformOptions? options)
{ {
options ??= new BrowserPlatformOptions(); options ??= AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions();
options.FrameworkAssetPathResolver ??= fileName => $"./{fileName}"; options.FrameworkAssetPathResolver ??= fileName => $"./{fileName}";
AvaloniaLocator.CurrentMutable.Bind<BrowserPlatformOptions>().ToConstant(options); AvaloniaLocator.CurrentMutable.Bind<BrowserPlatformOptions>().ToConstant(options);
await AvaloniaModule.ImportMain(); await AvaloniaModule.ImportMain();
BrowserWindowingPlatform.GlobalThis = DomHelper.GetGlobalThis();
if (BrowserWindowingPlatform.IsThreadingEnabled) if (BrowserWindowingPlatform.IsThreadingEnabled)
{ {
await RenderWorker.InitializeAsync(); await RenderWorker.InitializeAsync();

227
src/Browser/Avalonia.Browser/BrowserInputHandler.cs

@ -21,8 +21,9 @@ internal class BrowserInputHandler
private IInputRoot? _inputRoot; private IInputRoot? _inputRoot;
private static readonly PooledList<RawPointerPoint> s_intermediatePointsPooledList = new(ClearMode.Never); 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; _topLevelImpl = topLevelImpl;
_container = container ?? throw new ArgumentNullException(nameof(container)); _container = container ?? throw new ArgumentNullException(nameof(container));
@ -32,15 +33,19 @@ internal class BrowserInputHandler
_wheelMouseDevice = new MouseDevice(); _wheelMouseDevice = new MouseDevice();
_mouseDevices = new(); _mouseDevices = new();
InputHelper.SubscribeKeyEvents( _rawEventGrouper = BrowserWindowingPlatform.EventGrouperDispatchQueue is not null
container, ? new RawEventGrouper(DispatchInput, BrowserWindowingPlatform.EventGrouperDispatchQueue)
OnKeyDown, : null;
OnKeyUp);
InputHelper.SubscribePointerEvents(container, OnPointerMove, OnPointerDown, OnPointerUp, TextInputMethod = new BrowserTextInputMethod(this, container, inputElement);
OnPointerCancel, OnWheel); InputPane = new BrowserInputPane();
InputHelper.SubscribeDropEvents(container, OnDragEvent);
InputHelper.SubscribeInputEvents(container, topLevelId);
} }
public BrowserTextInputMethod TextInputMethod { get; }
public BrowserInputPane InputPane { get; }
public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds; public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds;
internal void SetInputRoot(IInputRoot inputRoot) internal void SetInputRoot(IInputRoot inputRoot)
@ -48,57 +53,65 @@ internal class BrowserInputHandler
_inputRoot = inputRoot; _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(offsetX, offsetY),
{ Pressure = (float)pressure,
Position = new Point(args.GetPropertyAsDouble("offsetX"), args.GetPropertyAsDouble("offsetY")), XTilt = (float)tiltX,
Pressure = (float)args.GetPropertyAsDouble("pressure"), YTilt = (float)tiltY,
XTilt = (float)args.GetPropertyAsDouble("tiltX"), Twist = (float)twist
YTilt = (float)args.GetPropertyAsDouble("tiltY"), };
Twist = (float)args.GetPropertyAsDouble("twist")
}; public bool OnPointerMove(string pointerType, long pointerId, double offsetX, double offsetY,
double pressure, double tiltX, double tiltY, double twist, int modifier, JSObject argsObj)
return point;
}
private bool OnPointerMove(JSObject args)
{ {
var pointerType = args.GetPropertyAsString("pointerType"); var point = CreateRawPointer(offsetX, offsetY, pressure, tiltX, tiltY, twist);
var point = ExtractRawPointerFromJsArgs(args);
var type = pointerType switch var type = pointerType switch
{ {
"touch" => RawPointerEventType.TouchUpdate, "touch" => RawPointerEventType.TouchUpdate,
_ => RawPointerEventType.Move _ => 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); coalescedEvents = new Lazy<IReadOnlyList<RawPointerPoint>?>(() =>
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++)
{ {
var point = points[i]; // To minimize JS interop usage, we resolve all points properties in a single call.
s_intermediatePointsPooledList.Add(ExtractRawPointerFromJsArgs(point)); 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); 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 var type = pointerType switch
{ {
"touch" => RawPointerEventType.TouchBegin, "touch" => RawPointerEventType.TouchBegin,
_ => args.GetPropertyAsInt32("button") switch _ => buttons switch
{ {
0 => RawPointerEventType.LeftButtonDown, 0 => RawPointerEventType.LeftButtonDown,
1 => RawPointerEventType.MiddleButtonDown, 1 => RawPointerEventType.MiddleButtonDown,
@ -110,17 +123,17 @@ internal class BrowserInputHandler
} }
}; };
var point = ExtractRawPointerFromJsArgs(args); var point = CreateRawPointer(offsetX, offsetY, pressure, tiltX, tiltY, twist);
return RawPointerEvent(type, pointerType, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); 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 var type = pointerType switch
{ {
"touch" => RawPointerEventType.TouchEnd, "touch" => RawPointerEventType.TouchEnd,
_ => args.GetPropertyAsInt32("button") switch _ => buttons switch
{ {
0 => RawPointerEventType.LeftButtonUp, 0 => RawPointerEventType.LeftButtonUp,
1 => RawPointerEventType.MiddleButtonUp, 1 => RawPointerEventType.MiddleButtonUp,
@ -132,70 +145,33 @@ internal class BrowserInputHandler
} }
}; };
var point = ExtractRawPointerFromJsArgs(args); var point = CreateRawPointer(offsetX, offsetY, pressure, tiltX, tiltY, twist);
return RawPointerEvent(type, pointerType, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); 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") if (pointerType == "touch")
{ {
var point = ExtractRawPointerFromJsArgs(args); var point = CreateRawPointer(offsetX, offsetY, pressure, tiltX, tiltY, twist);
RawPointerEvent(RawPointerEventType.TouchCancel, pointerType, point, RawPointerEvent(RawPointerEventType.TouchCancel, pointerType, point,
GetModifiers(args), args.GetPropertyAsInt32("pointerId")); (RawInputModifiers)modifier, pointerId);
} }
return false; 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")), return RawMouseWheelEvent(new Point(offsetX, offsetY),
new Vector(-(args.GetPropertyAsDouble("deltaX") / 50), -(args.GetPropertyAsDouble("deltaY") / 50)), new Vector(-(deltaX / 50), -(deltaY / 50)),
GetModifiers(args)); (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; var eventType = type switch
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
{ {
"dragenter" => RawDragEventType.DragEnter, "dragenter" => RawDragEventType.DragEnter,
"dragover" => RawDragEventType.DragOver, "dragover" => RawDragEventType.DragOver,
@ -203,8 +179,7 @@ internal class BrowserInputHandler
"drop" => RawDragEventType.Drop, "drop" => RawDragEventType.Drop,
_ => (RawDragEventType)(int)-1 _ => (RawDragEventType)(int)-1
}; };
var dataObject = args?.GetPropertyAsJSObject("dataTransfer"); if (eventType < 0 || dataTransfer is null)
if (args is null || eventType < 0 || dataObject is null)
{ {
return false; return false;
} }
@ -213,10 +188,9 @@ internal class BrowserInputHandler
// TODO: restructure JS files, so it's not needed. // TODO: restructure JS files, so it's not needed.
_ = AvaloniaModule.ImportStorage(); _ = AvaloniaModule.ImportStorage();
var position = new Point(args.GetPropertyAsDouble("offsetX"), args.GetPropertyAsDouble("offsetY")); var position = new Point(offsetX, offsetY);
var modifiers = GetModifiers(args);
var effectAllowedStr = dataObject.GetPropertyAsString("effectAllowed") ?? "none"; effectAllowedStr ??= "none";
var effectAllowed = DragDropEffects.None; var effectAllowed = DragDropEffects.None;
if (effectAllowedStr.Contains("copy", StringComparison.OrdinalIgnoreCase)) if (effectAllowedStr.Contains("copy", StringComparison.OrdinalIgnoreCase))
{ {
@ -243,16 +217,18 @@ internal class BrowserInputHandler
return false; return false;
} }
var dropEffect = RawDragEvent(eventType, position, modifiers, new BrowserDataObject(dataObject), effectAllowed); var dropEffect = RawDragEvent(eventType, position, (RawInputModifiers)modifiers, new BrowserDataObject(dataTransfer), effectAllowed);
dataObject.SetProperty("dropEffect", dropEffect.ToString().ToLowerInvariant()); 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 return eventType is RawDragEventType.Drop or RawDragEventType.DragOver
&& dropEffect != DragDropEffects.None; && 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) if (!handled && key.Length == 1)
{ {
@ -262,9 +238,9 @@ internal class BrowserInputHandler
return handled; 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( private bool RawPointerEvent(
@ -272,8 +248,7 @@ internal class BrowserInputHandler
RawPointerPoint p, RawInputModifiers modifiers, long touchPointId, RawPointerPoint p, RawInputModifiers modifiers, long touchPointId,
Lazy<IReadOnlyList<RawPointerPoint>?>? intermediatePoints = null) Lazy<IReadOnlyList<RawPointerPoint>?>? intermediatePoints = null)
{ {
if (_inputRoot is { } if (_inputRoot is not null)
&& _topLevelImpl.Input is { } input)
{ {
var device = GetPointerDevice(pointerType, touchPointId); var device = GetPointerDevice(pointerType, touchPointId);
var args = device is TouchDevice ? var args = device is TouchDevice ?
@ -286,7 +261,7 @@ internal class BrowserInputHandler
RawPointerId = touchPointId, IntermediatePoints = intermediatePoints RawPointerId = touchPointId, IntermediatePoints = intermediatePoints
}; };
input.Invoke(args); ScheduleInput(args);
return args.Handled; return args.Handled;
} }
@ -319,7 +294,7 @@ internal class BrowserInputHandler
{ {
var args = new RawMouseWheelEventArgs(_wheelMouseDevice, Timestamp, _inputRoot, p, v, modifiers); var args = new RawMouseWheelEventArgs(_wheelMouseDevice, Timestamp, _inputRoot, p, v, modifiers);
_topLevelImpl.Input?.Invoke(args); ScheduleInput(args);
return args.Handled; return args.Handled;
} }
@ -347,14 +322,7 @@ internal class BrowserInputHandler
keySymbol keySymbol
); );
try ScheduleInput(args);
{
_topLevelImpl.Input?.Invoke(args);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
return args.Handled; return args.Handled;
} }
@ -364,7 +332,7 @@ internal class BrowserInputHandler
if (_inputRoot is { }) if (_inputRoot is { })
{ {
var args = new RawTextInputEventArgs(BrowserWindowingPlatform.Keyboard, Timestamp, _inputRoot, text); var args = new RawTextInputEventArgs(BrowserWindowingPlatform.Keyboard, Timestamp, _inputRoot, text);
_topLevelImpl.Input?.Invoke(args); ScheduleInput(args);
return args.Handled; return args.Handled;
} }
@ -377,7 +345,28 @@ internal class BrowserInputHandler
{ {
var device = AvaloniaLocator.Current.GetRequiredService<IDragDropDevice>(); var device = AvaloniaLocator.Current.GetRequiredService<IDragDropDevice>();
var eventArgs = new RawDragEvent(device, eventType, _inputRoot!, position, dataObject, dropEffect, modifiers); var eventArgs = new RawDragEvent(device, eventType, _inputRoot!, position, dataObject, dropEffect, modifiers);
_topLevelImpl.Input?.Invoke(eventArgs); ScheduleInput(eventArgs);
return eventArgs.Effects; 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);
}
} }

13
src/Browser/Avalonia.Browser/BrowserInputPane.cs

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

11
src/Browser/Avalonia.Browser/BrowserInsetsManager.cs

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

20
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() private void EnsureBackend()
{ {
if (!_isInitialized) if (!_isInitialized)
{ {
// WASM module has async nature of initialization. We can't native code right away during components registration. // WASM module has async nature of initialization. We can't native code right away during components registration.
_isInitialized = true; _isInitialized = true;
var values = DomHelper.GetDarkMode(BrowserWindowingPlatform.GlobalThis);
var obj = DomHelper.ObserveDarkMode((isDarkMode, isHighContrast) => if (values.Length == 2)
{ {
_isDarkMode = isDarkMode; _isDarkMode = values[0] > 0;
_isHighContrast = isHighContrast; _isHighContrast = values[1] > 0;
OnColorValuesChanged(GetColorValues()); }
});
_isDarkMode = obj.GetPropertyAsBoolean("isDarkMode");
_isHighContrast = obj.GetPropertyAsBoolean("isHighContrast");
} }
} }
} }

23
src/Browser/Avalonia.Browser/BrowserSystemNavigationManager.cs

@ -1,24 +1,19 @@
using System; using System;
using Avalonia.Browser.Interop;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Platform; 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() public bool OnBackRequested()
{ {
NavigationHelper.AddBackHandler(() => var routedEventArgs = new RoutedEventArgs();
{
var routedEventArgs = new RoutedEventArgs();
BackRequested?.Invoke(this, routedEventArgs); BackRequested?.Invoke(this, routedEventArgs);
return routedEventArgs.Handled; return routedEventArgs.Handled;
});
}
} }
} }

63
src/Browser/Avalonia.Browser/BrowserTextInputMethod.cs

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

37
src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs

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

4
src/Browser/Avalonia.Browser/ClipboardImpl.cs

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

33
src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs

@ -1,9 +1,6 @@
using System; using System.Runtime.InteropServices.JavaScript;
using System.Collections.Generic; using System.Threading.Tasks;
using System.Linq; using Avalonia.Threading;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.JavaScript;
namespace Avalonia.Browser.Interop; 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 internal static partial class CanvasHelper
{ {
[JSImport("CanvasSurface.onSizeChanged", AvaloniaModule.MainModuleName)] [JSExport]
public static partial void OnSizeChanged( public static Task OnSizeChanged(int topLevelId, double width, double height, double dpr)
JSObject canvasSurface, {
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Number, JSType.Number>>] if (BrowserWindowingPlatform.IsThreadingEnabled)
// TODO: this callback should be <int, int, double>. Revert after next .NET 9 preview. {
Action<double, double, double> onSizeChanged); 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)] [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)] [JSImport("CanvasSurface.destroy", AvaloniaModule.MainModuleName)]
public static partial void Destroy(JSObject canvasSurface); public static partial void Destroy(JSObject canvasSurface);

45
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; namespace Avalonia.Browser.Interop;
internal static partial class DomHelper internal static partial class DomHelper
{ {
[JSImport("globalThis.document.getElementById")] [JSImport("AvaloniaDOM.getGlobalThis", AvaloniaModule.MainModuleName)]
internal static partial JSObject? GetElementById(string id); internal static partial JSObject GetGlobalThis();
[JSImport("AvaloniaDOM.getFirstElementById", AvaloniaModule.MainModuleName)]
internal static partial JSObject? GetElementById(string id, JSObject parent);
[JSImport("AvaloniaDOM.getFirstElementByClassName", AvaloniaModule.MainModuleName)] [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)] [JSImport("AvaloniaDOM.createAvaloniaHost", AvaloniaModule.MainModuleName)]
public static partial JSObject CreateAvaloniaHost(JSObject element); public static partial JSObject CreateAvaloniaHost(JSObject element);
[JSImport("AvaloniaDOM.isFullscreen", AvaloniaModule.MainModuleName)] [JSImport("AvaloniaDOM.isFullscreen", AvaloniaModule.MainModuleName)]
public static partial bool IsFullscreen(); public static partial bool IsFullscreen(JSObject globalThis);
[JSImport("AvaloniaDOM.setFullscreen", AvaloniaModule.MainModuleName)] [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)] [JSImport("AvaloniaDOM.getSafeAreaPadding", AvaloniaModule.MainModuleName)]
public static partial double[] GetSafeAreaPadding(); public static partial double[] GetSafeAreaPadding(JSObject globalThis);
[JSImport("AvaloniaDOM.initSafeAreaPadding", AvaloniaModule.MainModuleName)] [JSImport("AvaloniaDOM.getDarkMode", AvaloniaModule.MainModuleName)]
public static partial void InitSafeAreaPadding(); public static partial int[] GetDarkMode(JSObject globalThis);
[JSImport("AvaloniaDOM.addClass", AvaloniaModule.MainModuleName)] [JSImport("AvaloniaDOM.addClass", AvaloniaModule.MainModuleName)]
public static partial void AddCssClass(JSObject element, string className); public static partial void AddCssClass(JSObject element, string className);
[JSImport("AvaloniaDOM.observeDarkMode", AvaloniaModule.MainModuleName)] [JSImport("AvaloniaDOM.initGlobalDomEvents", AvaloniaModule.MainModuleName)]
public static partial JSObject ObserveDarkMode( public static partial void InitGlobalDomEvents(JSObject globalThis);
[JSMarshalAs<JSType.Function<JSType.Boolean, JSType.Boolean>>]
Action<bool, bool> observer); [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;
}
} }

142
src/Browser/Avalonia.Browser/Interop/InputHelper.cs

@ -7,69 +7,85 @@ namespace Avalonia.Browser.Interop;
internal static partial class InputHelper internal static partial class InputHelper
{ {
[JSImport("InputHelper.subscribeKeyEvents", AvaloniaModule.MainModuleName)] public static Task RedirectInputAsync(int topLevelId, Action<BrowserTopLevelImpl> handler)
public static partial void SubscribeKeyEvents( {
JSObject htmlElement, if (BrowserTopLevelImpl.TryGetTopLevel(topLevelId) is { } topLevelImpl) handler(topLevelImpl);
[JSMarshalAs<JSType.Function<JSType.String, JSType.String, JSType.String, JSType.Boolean>>] return Task.CompletedTask;
// 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);
[JSImport("InputHelper.subscribeInputEvents", AvaloniaModule.MainModuleName)] [JSImport("InputHelper.subscribeInputEvents", AvaloniaModule.MainModuleName)]
public static partial void SubscribeInputEvents( public static partial void SubscribeInputEvents(JSObject htmlElement, int topLevelId);
JSObject htmlElement,
[JSMarshalAs<JSType.Function<JSType.String, JSType.Boolean>>] [JSExport]
Func<string, bool> input); public static Task OnKeyDown(int topLevelId, string code, string key, int modifier) =>
RedirectInputAsync(topLevelId, t => t.InputHandler.OnKeyDown(code, key, modifier));
[JSImport("InputHelper.subscribeDropEvents", AvaloniaModule.MainModuleName)]
public static partial void SubscribeDropEvents(JSObject containerElement, [JSExport]
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>] Func<JSObject, bool> dragEvent); public static Task OnKeyUp(int topLevelId, string code, string key, int modifier) =>
RedirectInputAsync(topLevelId, t => t.InputHandler.OnKeyUp(code, key, modifier));
[JSImport("InputHelper.subscribeKeyboardGeometryChange", AvaloniaModule.MainModuleName)]
public static partial void SubscribeKeyboardGeometryChange(JSObject containerElement, [JSExport]
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>] Func<JSObject, bool> handler); public static Task OnBeforeInput(int topLevelId, string inputType, int start, int end) =>
RedirectInputAsync(topLevelId, t => t.InputHandler.TextInputMethod.OnBeforeInput(inputType, start, end));
[JSImport("InputHelper.subscribeVisibilityChange", AvaloniaModule.MainModuleName)]
public static partial bool SubscribeVisibilityChange([JSMarshalAs<JSType.Function<JSType.Boolean>>] Action<bool> handler); [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)] [JSImport("InputHelper.getCoalescedEvents", AvaloniaModule.MainModuleName)]
[return: JSMarshalAs<JSType.Array<JSType.Object>>] [return: JSMarshalAs<JSType.Array<JSType.Number>>]
public static partial JSObject[] GetCoalescedEvents(JSObject pointerEvent); public static partial double[] GetCoalescedEvents(JSObject pointerEvent);
[JSImport("InputHelper.clearInput", AvaloniaModule.MainModuleName)] [JSImport("InputHelper.clearInput", AvaloniaModule.MainModuleName)]
public static partial void ClearInputElement(JSObject htmlElement); public static partial void ClearInputElement(JSObject htmlElement);
[JSImport("InputHelper.isInputElement", AvaloniaModule.MainModuleName)]
public static partial void IsInputElement(JSObject htmlElement);
[JSImport("InputHelper.focusElement", AvaloniaModule.MainModuleName)] [JSImport("InputHelper.focusElement", AvaloniaModule.MainModuleName)]
public static partial void FocusElement(JSObject htmlElement); 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); public static partial void SetBounds(JSObject htmlElement, int x, int y, int width, int height, int caret);
[JSImport("InputHelper.initializeBackgroundHandlers", AvaloniaModule.MainModuleName)] [JSImport("InputHelper.initializeBackgroundHandlers", AvaloniaModule.MainModuleName)]
public static partial void InitializeBackgroundHandlers(); public static partial void InitializeBackgroundHandlers(JSObject globalThis);
[JSImport("InputHelper.readClipboardText", AvaloniaModule.MainModuleName)] [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)] [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)] [JSImport("InputHelper.releasePointerCapture", AvaloniaModule.MainModuleName)]
public static partial void ReleasePointerCapture(JSObject containerElement, [JSMarshalAs<JSType.Number>] long pointerId); public static partial void ReleasePointerCapture(JSObject containerElement,
[JSMarshalAs<JSType.Number>] long pointerId);
[JSImport("globalThis.navigator.clipboard.writeText")]
public static partial Task WriteClipboardTextAsync(string text);
} }

9
src/Browser/Avalonia.Browser/Interop/NavigationHelper.cs

@ -1,5 +1,7 @@
using System; using System;
using System.Runtime.InteropServices.JavaScript; using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
using Avalonia.Platform;
namespace Avalonia.Browser.Interop; namespace Avalonia.Browser.Interop;
@ -8,6 +10,13 @@ internal static partial class NavigationHelper
[JSImport("NavigationHelper.addBackHandler", AvaloniaModule.MainModuleName)] [JSImport("NavigationHelper.addBackHandler", AvaloniaModule.MainModuleName)]
public static partial void AddBackHandler([JSMarshalAs<JSType.Function<JSType.Boolean>>] Func<bool> backHandlerCallback); 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")] [JSImport("window.open")]
public static partial JSObject? WindowOpen(string uri, string target); public static partial JSObject? WindowOpen(string uri, string target);
} }

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

13
src/Browser/Avalonia.Browser/Rendering/BrowserSurface.cs

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

10
src/Browser/Avalonia.Browser/Rendering/RenderTargetBrowserSurface.cs

@ -14,7 +14,7 @@ internal class RenderTargetBrowserSurface : BrowserSurface
private readonly BrowserPlatformGraphics _graphics; private readonly BrowserPlatformGraphics _graphics;
private record InitParams(Compositor Compositor, BrowserPlatformGraphics Graphics); private record InitParams(Compositor Compositor, BrowserPlatformGraphics Graphics);
private static InitParams CreateCompositor(JSObject jsSurface) private static InitParams CreateCompositor(JSObject jsSurface)
{ {
var targetId = jsSurface.GetPropertyAsInt32("targetId"); var targetId = jsSurface.GetPropertyAsInt32("targetId");
@ -36,7 +36,7 @@ internal class RenderTargetBrowserSurface : BrowserSurface
return [_graphics.Target]; 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); _graphics.CanvasSize = (Size: new PixelSize((int)pixelWidth, (int)pixelHeight), Scaling: dpr);
base.OnSizeChanged(pixelWidth, pixelHeight, dpr); base.OnSizeChanged(pixelWidth, pixelHeight, dpr);
@ -93,9 +93,9 @@ internal class RenderTargetBrowserSurface : BrowserSurface
base.Dispose(); 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); return new RenderTargetBrowserSurface(js);
} }
} }

33
src/Browser/Avalonia.Browser/WindowingPlatform.cs

@ -1,24 +1,37 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices.JavaScript;
using System.Threading; using System.Threading;
using Avalonia.Browser.Interop; using Avalonia.Browser.Interop;
using Avalonia.Browser.Skia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Platform.Internal; using Avalonia.Platform.Internal;
using Avalonia.Rendering;
using Avalonia.Threading; using Avalonia.Threading;
namespace Avalonia.Browser; namespace Avalonia.Browser;
internal class BrowserWindowingPlatform : IWindowingPlatform internal class BrowserWindowingPlatform : IWindowingPlatform
{ {
internal static ManualRawEventGrouperDispatchQueue? EventGrouperDispatchQueue;
internal static readonly bool IsThreadingEnabled = DetectThreadSupport(); 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() static bool DetectThreadSupport()
{ {
// TODO Replace with public API https://github.com/dotnet/runtime/issues/77541. // 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<ICursorFactory>().ToSingleton<CssCursorFactory>()
.Bind<IKeyboardDevice>().ToConstant(s_keyboard) .Bind<IKeyboardDevice>().ToConstant(s_keyboard)
.Bind<IPlatformSettings>().ToSingleton<BrowserPlatformSettings>() .Bind<IPlatformSettings>().ToSingleton<BrowserPlatformSettings>()
.Bind<ISystemNavigationManagerImpl>().ToSingleton<BrowserSystemNavigationManagerImpl>()
.Bind<IWindowingPlatform>().ToConstant(instance) .Bind<IWindowingPlatform>().ToConstant(instance)
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>() .Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>() .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>() { })) .Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>() { }))
.Bind<IActivatableLifetime>().ToSingleton<BrowserActivatableLifetime>(); .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 // GC thread is the same as the main one when MT is disabled
if (IsThreadingEnabled) if (IsThreadingEnabled)
UnmanagedBlob.SuppressFinalizerWarning = true; UnmanagedBlob.SuppressFinalizerWarning = true;

2
src/Browser/Avalonia.Browser/webapp/build.js

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

102
src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts

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

184
src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts

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

16
src/Browser/Avalonia.Browser/webapp/modules/avalonia/jsExports.ts

@ -1,6 +1,22 @@
export class JsExports { export class JsExports {
public static resolvedExports?: any; public static resolvedExports?: any;
public static exportsPromise: Promise<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> { async function resolveExports (): Promise<any> {
const runtimeApi = await globalThis.getDotnetRuntime(0); const runtimeApi = await globalThis.getDotnetRuntime(0);

24
src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/canvasSurface.ts

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

12
src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts

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

5
src/Browser/Avalonia.Browser/webapp/tsconfig.json

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

Loading…
Cancel
Save