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

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

9
samples/ControlCatalog.Browser/Program.cs

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

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

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

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>
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");

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

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

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

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

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

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

13
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))

11
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]);
}

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()
{
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;
}
}
}
}

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

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

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

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

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

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

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

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

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

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;
internal static partial class DomHelper
{
[JSImport("globalThis.document.getElementById")]
internal static partial JSObject? GetElementById(string id);
[JSImport("AvaloniaDOM.getGlobalThis", AvaloniaModule.MainModuleName)]
internal static partial JSObject GetGlobalThis();
[JSImport("AvaloniaDOM.getFirstElementById", AvaloniaModule.MainModuleName)]
internal static partial JSObject? GetElementById(string id, JSObject parent);
[JSImport("AvaloniaDOM.getFirstElementByClassName", AvaloniaModule.MainModuleName)]
internal static partial JSObject? GetElementsByClassName(string className, JSObject? parent);
internal static partial JSObject? GetElementsByClassName(string className, JSObject parent);
[JSImport("AvaloniaDOM.createAvaloniaHost", AvaloniaModule.MainModuleName)]
public static partial JSObject CreateAvaloniaHost(JSObject element);
[JSImport("AvaloniaDOM.isFullscreen", AvaloniaModule.MainModuleName)]
public static partial bool IsFullscreen();
public static partial bool IsFullscreen(JSObject globalThis);
[JSImport("AvaloniaDOM.setFullscreen", AvaloniaModule.MainModuleName)]
public static partial JSObject SetFullscreen(bool isFullscreen);
public static partial Task SetFullscreen(JSObject globalThis, bool isFullscreen);
[JSImport("AvaloniaDOM.getSafeAreaPadding", AvaloniaModule.MainModuleName)]
public static partial double[] GetSafeAreaPadding();
public static partial double[] GetSafeAreaPadding(JSObject globalThis);
[JSImport("AvaloniaDOM.initSafeAreaPadding", AvaloniaModule.MainModuleName)]
public static partial void InitSafeAreaPadding();
[JSImport("AvaloniaDOM.getDarkMode", AvaloniaModule.MainModuleName)]
public static partial int[] GetDarkMode(JSObject globalThis);
[JSImport("AvaloniaDOM.addClass", AvaloniaModule.MainModuleName)]
public static partial void AddCssClass(JSObject element, string className);
[JSImport("AvaloniaDOM.observeDarkMode", AvaloniaModule.MainModuleName)]
public static partial JSObject ObserveDarkMode(
[JSMarshalAs<JSType.Function<JSType.Boolean, JSType.Boolean>>]
Action<bool, bool> observer);
[JSImport("AvaloniaDOM.initGlobalDomEvents", AvaloniaModule.MainModuleName)]
public static partial void InitGlobalDomEvents(JSObject globalThis);
[JSExport]
public static Task DarkModeChanged(bool isDarkMode, bool isHighContrast)
{
(AvaloniaLocator.Current.GetService<IPlatformSettings>() as BrowserPlatformSettings)?.OnValuesChanged(isDarkMode, isHighContrast);
return Task.CompletedTask;
}
[JSExport]
public static Task DocumentVisibilityChanged(string visibilityState)
{
(AvaloniaLocator.Current.GetService<IActivatableLifetime>() as BrowserActivatableLifetime)?.OnVisibilityStateChanged(visibilityState);
return Task.CompletedTask;
}
}

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

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

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

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

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()
{
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);

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

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

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

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

2
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" }

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

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

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

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

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

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

12
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 {

5
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"
]
}

Loading…
Cancel
Save