diff --git a/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj b/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj index 309d54722c..b921945f25 100644 --- a/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj +++ b/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj @@ -1,9 +1,7 @@ - + $(AvsCurrentBrowserTargetFramework) - browser-wasm - wwwroot/main.js - Exe + false true true 5 @@ -22,10 +20,6 @@ - - - - diff --git a/samples/ControlCatalog.Browser/EmbedSample.Browser.cs b/samples/ControlCatalog.Browser/EmbedSample.Browser.cs index 7bf3891a81..b8bc4ed35c 100644 --- a/samples/ControlCatalog.Browser/EmbedSample.Browser.cs +++ b/samples/ControlCatalog.Browser/EmbedSample.Browser.cs @@ -29,7 +29,7 @@ public class EmbedSampleWeb : INativeDemoControl static async void AddButton(JSObject parent) { - await JSHost.ImportAsync("embed.js", "./embed.js"); + await JSHost.ImportAsync("embed.js", "../embed.js"); EmbedInterop.AddAppButton(parent); } } diff --git a/samples/ControlCatalog.Browser/Program.cs b/samples/ControlCatalog.Browser/Program.cs index c50f1dcbdd..95cce73eb3 100644 --- a/samples/ControlCatalog.Browser/Program.cs +++ b/samples/ControlCatalog.Browser/Program.cs @@ -33,10 +33,13 @@ internal partial class Program }) .StartBrowserAppAsync("out", options); - if (Application.Current!.ApplicationLifetime is ISingleTopLevelApplicationLifetime lifetime) + Dispatcher.UIThread.Invoke(() => { - lifetime.TopLevel!.RendererDiagnostics.DebugOverlays = RendererDebugOverlays.Fps; - } + if (Application.Current!.ApplicationLifetime is ISingleTopLevelApplicationLifetime lifetime) + { + lifetime.TopLevel!.RendererDiagnostics.DebugOverlays = RendererDebugOverlays.Fps; + } + }); } // Test with multiple AvaloniaView at once. diff --git a/src/Browser/Avalonia.Browser/Avalonia.Browser.csproj b/src/Browser/Avalonia.Browser/Avalonia.Browser.csproj index e84e9a95b7..440378d6fc 100644 --- a/src/Browser/Avalonia.Browser/Avalonia.Browser.csproj +++ b/src/Browser/Avalonia.Browser/Avalonia.Browser.csproj @@ -54,4 +54,10 @@ + + + RawEventGrouping.cs + + + diff --git a/src/Browser/Avalonia.Browser/AvaloniaView.cs b/src/Browser/Avalonia.Browser/AvaloniaView.cs index ff397a14b8..42d5d7dca6 100644 --- a/src/Browser/Avalonia.Browser/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser/AvaloniaView.cs @@ -12,7 +12,7 @@ namespace Avalonia.Browser /// ID of the html element where avalonia content should be rendered. public AvaloniaView(string divId) - : this(DomHelper.GetElementById(divId) ?? + : this(DomHelper.GetElementById(divId, BrowserWindowingPlatform.GlobalThis) ?? throw new Exception($"Element with id '{divId}' was not found in the html document.")) { } @@ -47,7 +47,7 @@ namespace Avalonia.Browser // Try to get local splash-screen of the specific host. // If couldn't find - get global one by ID for compatibility. var splash = DomHelper.GetElementsByClassName("avalonia-splash", host) - ?? DomHelper.GetElementById("avalonia-splash"); + ?? DomHelper.GetElementById("avalonia-splash", BrowserWindowingPlatform.GlobalThis); if (splash is not null) { DomHelper.AddCssClass(splash, "splash-close"); diff --git a/src/Browser/Avalonia.Browser/BrowserActivatableLifetime.cs b/src/Browser/Avalonia.Browser/BrowserActivatableLifetime.cs index 30996d55df..bd8211de88 100644 --- a/src/Browser/Avalonia.Browser/BrowserActivatableLifetime.cs +++ b/src/Browser/Avalonia.Browser/BrowserActivatableLifetime.cs @@ -1,36 +1,20 @@ -using System; using Avalonia.Browser.Interop; using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Threading; namespace Avalonia.Browser; -internal class BrowserActivatableLifetime : IActivatableLifetime +internal class BrowserActivatableLifetime : ActivatableLifetimeBase { - public BrowserActivatableLifetime() + public void OnVisibilityStateChanged(string visibilityState) { - bool? initiallyVisible = InputHelper.SubscribeVisibilityChange(visible => + var visible = visibilityState == "visible"; + if (visible) { - initiallyVisible = null; - (visible ? Activated : Deactivated)?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background)); - }); - - // Trigger Activated as an initial state, if web page is visible, and wasn't hidden during initialization. - if (initiallyVisible == true) + OnActivated(ActivationKind.Background); + } + else { - _ = Dispatcher.UIThread.InvokeAsync(() => - { - if (initiallyVisible == true) - { - Activated?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background)); - } - }, DispatcherPriority.Background); + OnDeactivated(ActivationKind.Background); } } - - public event EventHandler? Activated; - public event EventHandler? Deactivated; - - public bool TryLeaveBackground() => false; - public bool TryEnterBackground() => false; } diff --git a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs index 02fa530fae..c05548184f 100644 --- a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs +++ b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Avalonia.Browser.Interop; using Avalonia.Browser.Rendering; +using Avalonia.Controls; using Avalonia.Metadata; namespace Avalonia.Browser; @@ -53,6 +55,12 @@ public record BrowserPlatformOptions /// For more details, see https://github.com/jimmywarting/native-file-system-adapter#a-note-when-downloading-with-the-polyfilled-version. /// public bool PreferFileDialogPolyfill { get; set; } + + /// + /// Defines if Avalonia should create a controlled dispatcher loop on the web worker thread. + /// If used only when WasmEnableThreads is set to true. Default value is true. + /// + public bool? PreferManagedThreadDispatcher { get; set; } = true; } public static class BrowserAppBuilder @@ -63,8 +71,9 @@ public static class BrowserAppBuilder /// Application builder. /// ID of the html element where avalonia content should be rendered. /// Browser backend specific options. - public static async Task StartBrowserAppAsync(this AppBuilder builder, string mainDivId, - BrowserPlatformOptions? options = null) + public static async Task StartBrowserAppAsync( + this AppBuilder builder, + string mainDivId, BrowserPlatformOptions? options = null) { if (mainDivId is null) { @@ -78,8 +87,35 @@ public static class BrowserAppBuilder .AfterApplicationSetup(_ => { lifetime.View = new AvaloniaView(mainDivId); - }) - .SetupWithLifetime(lifetime); + }); + + if (BrowserWindowingPlatform.IsManagedDispatcherEnabled) + { + var tcs = new TaskCompletionSource(); + var thread = new Thread(() => + { + try + { + builder + .SetupWithLifetime(lifetime); + tcs.TrySetResult(); + builder.Instance!.Run(CancellationToken.None); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + }); +#pragma warning disable CA1416 + thread.Start(); +#pragma warning restore CA1416 + await tcs.Task; + } + else + { + builder + .SetupWithLifetime(lifetime); + } } /// @@ -102,12 +138,15 @@ public static class BrowserAppBuilder internal static async Task PreSetupBrowser(AppBuilder builder, BrowserPlatformOptions? options) { - options ??= new BrowserPlatformOptions(); + options ??= AvaloniaLocator.Current.GetService() ?? new BrowserPlatformOptions(); options.FrameworkAssetPathResolver ??= fileName => $"./{fileName}"; AvaloniaLocator.CurrentMutable.Bind().ToConstant(options); await AvaloniaModule.ImportMain(); + + BrowserWindowingPlatform.GlobalThis = DomHelper.GetGlobalThis(); + if (BrowserWindowingPlatform.IsThreadingEnabled) { await RenderWorker.InitializeAsync(); diff --git a/src/Browser/Avalonia.Browser/BrowserInputHandler.cs b/src/Browser/Avalonia.Browser/BrowserInputHandler.cs index b264c86db5..d9fe7da666 100644 --- a/src/Browser/Avalonia.Browser/BrowserInputHandler.cs +++ b/src/Browser/Avalonia.Browser/BrowserInputHandler.cs @@ -21,8 +21,9 @@ internal class BrowserInputHandler private IInputRoot? _inputRoot; private static readonly PooledList s_intermediatePointsPooledList = new(ClearMode.Never); + private readonly RawEventGrouper? _rawEventGrouper; - public BrowserInputHandler(BrowserTopLevelImpl topLevelImpl, JSObject container) + public BrowserInputHandler(BrowserTopLevelImpl topLevelImpl, JSObject container, JSObject inputElement, int topLevelId) { _topLevelImpl = topLevelImpl; _container = container ?? throw new ArgumentNullException(nameof(container)); @@ -32,15 +33,19 @@ internal class BrowserInputHandler _wheelMouseDevice = new MouseDevice(); _mouseDevices = new(); - InputHelper.SubscribeKeyEvents( - container, - OnKeyDown, - OnKeyUp); - InputHelper.SubscribePointerEvents(container, OnPointerMove, OnPointerDown, OnPointerUp, - OnPointerCancel, OnWheel); - InputHelper.SubscribeDropEvents(container, OnDragEvent); + _rawEventGrouper = BrowserWindowingPlatform.EventGrouperDispatchQueue is not null + ? new RawEventGrouper(DispatchInput, BrowserWindowingPlatform.EventGrouperDispatchQueue) + : null; + + TextInputMethod = new BrowserTextInputMethod(this, container, inputElement); + InputPane = new BrowserInputPane(); + + InputHelper.SubscribeInputEvents(container, topLevelId); } + public BrowserTextInputMethod TextInputMethod { get; } + public BrowserInputPane InputPane { get; } + public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds; internal void SetInputRoot(IInputRoot inputRoot) @@ -48,57 +53,65 @@ internal class BrowserInputHandler _inputRoot = inputRoot; } - private static RawPointerPoint ExtractRawPointerFromJsArgs(JSObject args) + private static RawPointerPoint CreateRawPointer(double offsetX, double offsetY, + double pressure, double tiltX, double tiltY, double twist) => new() { - var point = new RawPointerPoint - { - Position = new Point(args.GetPropertyAsDouble("offsetX"), args.GetPropertyAsDouble("offsetY")), - Pressure = (float)args.GetPropertyAsDouble("pressure"), - XTilt = (float)args.GetPropertyAsDouble("tiltX"), - YTilt = (float)args.GetPropertyAsDouble("tiltY"), - Twist = (float)args.GetPropertyAsDouble("twist") - }; - - return point; - } - - private bool OnPointerMove(JSObject args) + Position = new Point(offsetX, offsetY), + Pressure = (float)pressure, + XTilt = (float)tiltX, + YTilt = (float)tiltY, + Twist = (float)twist + }; + + public bool OnPointerMove(string pointerType, long pointerId, double offsetX, double offsetY, + double pressure, double tiltX, double tiltY, double twist, int modifier, JSObject argsObj) { - var pointerType = args.GetPropertyAsString("pointerType"); - var point = ExtractRawPointerFromJsArgs(args); + var point = CreateRawPointer(offsetX, offsetY, pressure, tiltX, tiltY, twist); var type = pointerType switch { "touch" => RawPointerEventType.TouchUpdate, _ => RawPointerEventType.Move }; - var coalescedEvents = new Lazy?>(() => + Lazy?>? coalescedEvents = null; + // Rely on native GetCoalescedEvents only when managed event grouping is not available. + if (_rawEventGrouper is null) { - var points = InputHelper.GetCoalescedEvents(args); - s_intermediatePointsPooledList.Clear(); - s_intermediatePointsPooledList.Capacity = points.Length - 1; - - // Skip the last one, as it is already processed point. - for (var i = 0; i < points.Length - 1; i++) + coalescedEvents = new Lazy?>(() => { - var point = points[i]; - s_intermediatePointsPooledList.Add(ExtractRawPointerFromJsArgs(point)); - } + // To minimize JS interop usage, we resolve all points properties in a single call. + const int itemsPerPoint = 6; + var pointsProps = InputHelper.GetCoalescedEvents(argsObj); + argsObj.Dispose(); + s_intermediatePointsPooledList.Clear(); + + var pointsCount = pointsProps.Length / itemsPerPoint; + s_intermediatePointsPooledList.Capacity = pointsCount - 1; - return s_intermediatePointsPooledList; - }); + // Skip the last one, as it is already processed point. + for (var i = 0; i < pointsCount - 1; i += itemsPerPoint) + { + s_intermediatePointsPooledList.Add(CreateRawPointer( + pointsProps[i], pointsProps[i + 1], + pointsProps[i + 2], pointsProps[i + 3], + pointsProps[i + 4], pointsProps[i + 5])); + } + + return s_intermediatePointsPooledList; + }); + } - return RawPointerEvent(type, pointerType!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"), + return RawPointerEvent(type, pointerType!, point, (RawInputModifiers)modifier, pointerId, coalescedEvents); } - private bool OnPointerDown(JSObject args) + public bool OnPointerDown(string pointerType, long pointerId, int buttons, double offsetX, double offsetY, + double pressure, double tiltX, double tiltY, double twist, int modifier) { - var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse"; var type = pointerType switch { "touch" => RawPointerEventType.TouchBegin, - _ => args.GetPropertyAsInt32("button") switch + _ => buttons switch { 0 => RawPointerEventType.LeftButtonDown, 1 => RawPointerEventType.MiddleButtonDown, @@ -110,17 +123,17 @@ internal class BrowserInputHandler } }; - var point = ExtractRawPointerFromJsArgs(args); - return RawPointerEvent(type, pointerType, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); + var point = CreateRawPointer(offsetX, offsetY, pressure, tiltX, tiltY, twist); + return RawPointerEvent(type, pointerType, point, (RawInputModifiers)modifier, pointerId); } - private bool OnPointerUp(JSObject args) + public bool OnPointerUp(string pointerType, long pointerId, int buttons, double offsetX, double offsetY, + double pressure, double tiltX, double tiltY, double twist, int modifier) { - var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse"; var type = pointerType switch { "touch" => RawPointerEventType.TouchEnd, - _ => args.GetPropertyAsInt32("button") switch + _ => buttons switch { 0 => RawPointerEventType.LeftButtonUp, 1 => RawPointerEventType.MiddleButtonUp, @@ -132,70 +145,33 @@ internal class BrowserInputHandler } }; - var point = ExtractRawPointerFromJsArgs(args); - return RawPointerEvent(type, pointerType, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); + var point = CreateRawPointer(offsetX, offsetY, pressure, tiltX, tiltY, twist); + return RawPointerEvent(type, pointerType, point, (RawInputModifiers)modifier, pointerId); } - private bool OnPointerCancel(JSObject args) + public bool OnPointerCancel(string pointerType, long pointerId, double offsetX, double offsetY, + double pressure, double tiltX, double tiltY, double twist, int modifier) { - var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse"; if (pointerType == "touch") { - var point = ExtractRawPointerFromJsArgs(args); + var point = CreateRawPointer(offsetX, offsetY, pressure, tiltX, tiltY, twist); RawPointerEvent(RawPointerEventType.TouchCancel, pointerType, point, - GetModifiers(args), args.GetPropertyAsInt32("pointerId")); + (RawInputModifiers)modifier, pointerId); } return false; } - private bool OnWheel(JSObject args) + public bool OnWheel(double offsetX, double offsetY, double deltaX, double deltaY, int modifier) { - return RawMouseWheelEvent(new Point(args.GetPropertyAsDouble("offsetX"), args.GetPropertyAsDouble("offsetY")), - new Vector(-(args.GetPropertyAsDouble("deltaX") / 50), -(args.GetPropertyAsDouble("deltaY") / 50)), - GetModifiers(args)); + return RawMouseWheelEvent(new Point(offsetX, offsetY), + new Vector(-(deltaX / 50), -(deltaY / 50)), + (RawInputModifiers)modifier); } - private static RawInputModifiers GetModifiers(JSObject e) + public bool OnDragEvent(string type, double offsetX, double offsetY, int modifiers, string? effectAllowedStr, JSObject? dataTransfer) { - var modifiers = RawInputModifiers.None; - - if (e.GetPropertyAsBoolean("ctrlKey")) - modifiers |= RawInputModifiers.Control; - if (e.GetPropertyAsBoolean("altKey")) - modifiers |= RawInputModifiers.Alt; - if (e.GetPropertyAsBoolean("shiftKey")) - modifiers |= RawInputModifiers.Shift; - if (e.GetPropertyAsBoolean("metaKey")) - modifiers |= RawInputModifiers.Meta; - - var buttons = e.GetPropertyAsInt32("buttons"); - if ((buttons & 1L) == 1) - modifiers |= RawInputModifiers.LeftMouseButton; - - if ((buttons & 2L) == 2) - modifiers |= e.GetPropertyAsString("type") == "pen" ? - RawInputModifiers.PenBarrelButton : - RawInputModifiers.RightMouseButton; - - if ((buttons & 4L) == 4) - modifiers |= RawInputModifiers.MiddleMouseButton; - - if ((buttons & 8L) == 8) - modifiers |= RawInputModifiers.XButton1MouseButton; - - if ((buttons & 16L) == 16) - modifiers |= RawInputModifiers.XButton2MouseButton; - - if ((buttons & 32L) == 32) - modifiers |= RawInputModifiers.PenEraser; - - return modifiers; - } - - public bool OnDragEvent(JSObject args) - { - var eventType = args?.GetPropertyAsString("type") switch + var eventType = type switch { "dragenter" => RawDragEventType.DragEnter, "dragover" => RawDragEventType.DragOver, @@ -203,8 +179,7 @@ internal class BrowserInputHandler "drop" => RawDragEventType.Drop, _ => (RawDragEventType)(int)-1 }; - var dataObject = args?.GetPropertyAsJSObject("dataTransfer"); - if (args is null || eventType < 0 || dataObject is null) + if (eventType < 0 || dataTransfer is null) { return false; } @@ -213,10 +188,9 @@ internal class BrowserInputHandler // TODO: restructure JS files, so it's not needed. _ = AvaloniaModule.ImportStorage(); - var position = new Point(args.GetPropertyAsDouble("offsetX"), args.GetPropertyAsDouble("offsetY")); - var modifiers = GetModifiers(args); + var position = new Point(offsetX, offsetY); - var effectAllowedStr = dataObject.GetPropertyAsString("effectAllowed") ?? "none"; + effectAllowedStr ??= "none"; var effectAllowed = DragDropEffects.None; if (effectAllowedStr.Contains("copy", StringComparison.OrdinalIgnoreCase)) { @@ -243,16 +217,18 @@ internal class BrowserInputHandler return false; } - var dropEffect = RawDragEvent(eventType, position, modifiers, new BrowserDataObject(dataObject), effectAllowed); - dataObject.SetProperty("dropEffect", dropEffect.ToString().ToLowerInvariant()); + var dropEffect = RawDragEvent(eventType, position, (RawInputModifiers)modifiers, new BrowserDataObject(dataTransfer), effectAllowed); + dataTransfer.SetProperty("dropEffect", dropEffect.ToString().ToLowerInvariant()); + // Note, due to complications of JS interop, we ignore this return value. + // And instead assume, that event is handled for any "drop" and "drag-over" stages. return eventType is RawDragEventType.Drop or RawDragEventType.DragOver && dropEffect != DragDropEffects.None; } - private bool OnKeyDown(string code, string key, string modifier) + public bool OnKeyDown(string code, string key, int modifier) { - var handled = RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)int.Parse(modifier)); + var handled = RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)modifier); if (!handled && key.Length == 1) { @@ -262,9 +238,9 @@ internal class BrowserInputHandler return handled; } - private bool OnKeyUp(string code, string key, string modifier) + public bool OnKeyUp(string code, string key, int modifier) { - return RawKeyboardEvent(RawKeyEventType.KeyUp, code, key, (RawInputModifiers)int.Parse(modifier)); + return RawKeyboardEvent(RawKeyEventType.KeyUp, code, key, (RawInputModifiers)modifier); } private bool RawPointerEvent( @@ -272,8 +248,7 @@ internal class BrowserInputHandler RawPointerPoint p, RawInputModifiers modifiers, long touchPointId, Lazy?>? intermediatePoints = null) { - if (_inputRoot is { } - && _topLevelImpl.Input is { } input) + if (_inputRoot is not null) { var device = GetPointerDevice(pointerType, touchPointId); var args = device is TouchDevice ? @@ -286,7 +261,7 @@ internal class BrowserInputHandler RawPointerId = touchPointId, IntermediatePoints = intermediatePoints }; - input.Invoke(args); + ScheduleInput(args); return args.Handled; } @@ -319,7 +294,7 @@ internal class BrowserInputHandler { var args = new RawMouseWheelEventArgs(_wheelMouseDevice, Timestamp, _inputRoot, p, v, modifiers); - _topLevelImpl.Input?.Invoke(args); + ScheduleInput(args); return args.Handled; } @@ -347,14 +322,7 @@ internal class BrowserInputHandler keySymbol ); - try - { - _topLevelImpl.Input?.Invoke(args); - } - catch (Exception ex) - { - Console.WriteLine(ex); - } + ScheduleInput(args); return args.Handled; } @@ -364,7 +332,7 @@ internal class BrowserInputHandler if (_inputRoot is { }) { var args = new RawTextInputEventArgs(BrowserWindowingPlatform.Keyboard, Timestamp, _inputRoot, text); - _topLevelImpl.Input?.Invoke(args); + ScheduleInput(args); return args.Handled; } @@ -377,7 +345,28 @@ internal class BrowserInputHandler { var device = AvaloniaLocator.Current.GetRequiredService(); var eventArgs = new RawDragEvent(device, eventType, _inputRoot!, position, dataObject, dropEffect, modifiers); - _topLevelImpl.Input?.Invoke(eventArgs); + ScheduleInput(eventArgs); return eventArgs.Effects; } + + private void ScheduleInput(RawInputEventArgs args) + { + // _rawEventGrouper is available only when we use managed dispatcher. + if (_rawEventGrouper is not null) + { + _rawEventGrouper.HandleEvent(args); + } + else + { + DispatchInput(args); + } + } + + private void DispatchInput(RawInputEventArgs args) + { + if (_inputRoot is null) + return; + + _topLevelImpl.Input?.Invoke(args); + } } diff --git a/src/Browser/Avalonia.Browser/BrowserInputPane.cs b/src/Browser/Avalonia.Browser/BrowserInputPane.cs index be132ee956..792e6974e6 100644 --- a/src/Browser/Avalonia.Browser/BrowserInputPane.cs +++ b/src/Browser/Avalonia.Browser/BrowserInputPane.cs @@ -7,20 +7,11 @@ namespace Avalonia.Browser; internal class BrowserInputPane : InputPaneBase { - public BrowserInputPane(JSObject container) - { - InputHelper.SubscribeKeyboardGeometryChange(container, OnGeometryChange); - } - - private bool OnGeometryChange(JSObject args) + public bool OnGeometryChange(double x, double y, double width, double height) { var oldState = (OccludedRect, State); - OccludedRect = new Rect( - args.GetPropertyAsDouble("x"), - args.GetPropertyAsDouble("y"), - args.GetPropertyAsDouble("width"), - args.GetPropertyAsDouble("height")); + OccludedRect = new Rect(x, y, width, height); State = OccludedRect.Width != 0 ? InputPaneState.Open : InputPaneState.Closed; if (oldState != (OccludedRect, State)) diff --git a/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs index 2c8eb03c72..dd34f744c1 100644 --- a/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs +++ b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs @@ -6,20 +6,15 @@ namespace Avalonia.Browser { internal class BrowserInsetsManager : InsetsManagerBase { - public BrowserInsetsManager() - { - DomHelper.InitSafeAreaPadding(); - } - public override bool? IsSystemBarVisible { get { - return DomHelper.IsFullscreen(); + return DomHelper.IsFullscreen(BrowserWindowingPlatform.GlobalThis); } set { - DomHelper.SetFullscreen(!value ?? false); + _ = DomHelper.SetFullscreen(BrowserWindowingPlatform.GlobalThis, !value ?? false); } } @@ -29,7 +24,7 @@ namespace Avalonia.Browser { get { - var padding = DomHelper.GetSafeAreaPadding(); + var padding = DomHelper.GetSafeAreaPadding(BrowserWindowingPlatform.GlobalThis); return new Thickness(padding[0], padding[1], padding[2], padding[3]); } diff --git a/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs b/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs index fa647d31b7..2a3cef4334 100644 --- a/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs +++ b/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs @@ -31,21 +31,25 @@ internal class BrowserPlatformSettings : DefaultPlatformSettings }; } + public void OnValuesChanged(bool isDarkMode, bool isHighContrast) + { + _isDarkMode = isDarkMode; + _isHighContrast = isHighContrast; + OnColorValuesChanged(GetColorValues()); + } + private void EnsureBackend() { if (!_isInitialized) { // WASM module has async nature of initialization. We can't native code right away during components registration. _isInitialized = true; - - var obj = DomHelper.ObserveDarkMode((isDarkMode, isHighContrast) => + var values = DomHelper.GetDarkMode(BrowserWindowingPlatform.GlobalThis); + if (values.Length == 2) { - _isDarkMode = isDarkMode; - _isHighContrast = isHighContrast; - OnColorValuesChanged(GetColorValues()); - }); - _isDarkMode = obj.GetPropertyAsBoolean("isDarkMode"); - _isHighContrast = obj.GetPropertyAsBoolean("isHighContrast"); + _isDarkMode = values[0] > 0; + _isHighContrast = values[1] > 0; + } } } } diff --git a/src/Browser/Avalonia.Browser/BrowserSystemNavigationManager.cs b/src/Browser/Avalonia.Browser/BrowserSystemNavigationManager.cs index bc38067f4a..275fb44d1e 100644 --- a/src/Browser/Avalonia.Browser/BrowserSystemNavigationManager.cs +++ b/src/Browser/Avalonia.Browser/BrowserSystemNavigationManager.cs @@ -1,24 +1,19 @@ using System; -using Avalonia.Browser.Interop; using Avalonia.Interactivity; using Avalonia.Platform; -namespace Avalonia.Browser +namespace Avalonia.Browser; + +internal class BrowserSystemNavigationManagerImpl : ISystemNavigationManagerImpl { - internal class BrowserSystemNavigationManagerImpl : ISystemNavigationManagerImpl - { - public event EventHandler? BackRequested; + public event EventHandler? BackRequested; - public BrowserSystemNavigationManagerImpl() - { - NavigationHelper.AddBackHandler(() => - { - var routedEventArgs = new RoutedEventArgs(); + public bool OnBackRequested() + { + var routedEventArgs = new RoutedEventArgs(); - BackRequested?.Invoke(this, routedEventArgs); + BackRequested?.Invoke(this, routedEventArgs); - return routedEventArgs.Handled; - }); - } + return routedEventArgs.Handled; } } diff --git a/src/Browser/Avalonia.Browser/BrowserTextInputMethod.cs b/src/Browser/Avalonia.Browser/BrowserTextInputMethod.cs index 8cb4296c48..fbcbf15ee5 100644 --- a/src/Browser/Avalonia.Browser/BrowserTextInputMethod.cs +++ b/src/Browser/Avalonia.Browser/BrowserTextInputMethod.cs @@ -5,27 +5,17 @@ using Avalonia.Input.TextInput; namespace Avalonia.Browser; -internal class BrowserTextInputMethod : ITextInputMethodImpl +internal class BrowserTextInputMethod( + BrowserInputHandler inputHandler, + JSObject containerElement, + JSObject inputElement) + : ITextInputMethodImpl { - private readonly JSObject _inputElement; - private readonly JSObject _containerElement; - private readonly BrowserInputHandler _inputHandler; + private readonly JSObject _inputElement = inputElement ?? throw new ArgumentNullException(nameof(inputElement)); + private readonly JSObject _containerElement = containerElement ?? throw new ArgumentNullException(nameof(containerElement)); + private readonly BrowserInputHandler _inputHandler = inputHandler ?? throw new ArgumentNullException(nameof(inputHandler)); private TextInputMethodClient? _client; - public BrowserTextInputMethod(BrowserInputHandler inputHandler, JSObject containerElement, JSObject inputElement) - { - _inputHandler = inputHandler ?? throw new ArgumentNullException(nameof(inputHandler)); - _containerElement = containerElement ?? throw new ArgumentNullException(nameof(containerElement)); - _inputElement = inputElement ?? throw new ArgumentNullException(nameof(inputElement)); - - InputHelper.SubscribeTextEvents( - _inputElement, - OnBeforeInput, - OnCompositionStart, - OnCompositionUpdate, - OnCompositionEnd); - } - public bool IsComposing { get; private set; } private void HideIme() @@ -95,12 +85,11 @@ internal class BrowserTextInputMethod : ITextInputMethodImpl InputHelper.SetSurroundingText(_inputElement, "", 0, 0); } - private bool OnBeforeInput(JSObject arg, int start, int end) + public void OnBeforeInput(string inputType, int start, int end) { - var type = arg.GetPropertyAsString("inputType"); - if (type != "deleteByComposition") + if (inputType != "deleteByComposition") { - if (type == "deleteContentBackward") + if (inputType == "deleteContentBackward") { start = _inputElement.GetPropertyAsInt32("selectionStart"); end = _inputElement.GetPropertyAsInt32("selectionEnd"); @@ -116,47 +105,37 @@ internal class BrowserTextInputMethod : ITextInputMethodImpl { _client.Selection = new TextSelection(start, end); } - - return false; } - private bool OnCompositionStart(JSObject args) + public void OnCompositionStart() { if (_client == null) - return false; + return; _client.SetPreeditText(null); IsComposing = true; - - return false; } - private bool OnCompositionUpdate(JSObject args) + public void OnCompositionUpdate(string? data) { if (_client == null) - return false; + return; - _client.SetPreeditText(args.GetPropertyAsString("data")); - - return false; + _client.SetPreeditText(data); } - private bool OnCompositionEnd(JSObject args) + public void OnCompositionEnd(string? data) { if (_client == null) - return false; + return; IsComposing = false; _client.SetPreeditText(null); - - var text = args.GetPropertyAsString("data"); - - if (text != null) + + if (data != null) { - return _inputHandler.RawTextEvent(text); + _inputHandler.RawTextEvent(data); } - - return false; } } diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs index dc997b840a..2a776d719e 100644 --- a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs +++ b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs @@ -23,40 +23,49 @@ namespace Avalonia.Browser { internal class BrowserTopLevelImpl : ITopLevelImpl { + private static int s_lastTopLevelId = 0; + private static Dictionary> s_topLevels = new(); + private readonly INativeControlHostImpl _nativeControlHost; private readonly IStorageProvider _storageProvider; - private readonly ISystemNavigationManagerImpl _systemNavigationManager; - private readonly ITextInputMethodImpl _textInputMethodImpl; private readonly ClipboardImpl _clipboard; private readonly IInsetsManager _insetsManager; - private readonly IInputPane _inputPane; private readonly JSObject _container; private readonly BrowserInputHandler _inputHandler; private string _currentCursor = CssCursor.Default; private BrowserSurface? _surface; + private readonly int _topLevelId; static BrowserTopLevelImpl() { - InputHelper.InitializeBackgroundHandlers(); + DomHelper.InitGlobalDomEvents(BrowserWindowingPlatform.GlobalThis); + InputHelper.InitializeBackgroundHandlers(BrowserWindowingPlatform.GlobalThis); } - + + public static BrowserTopLevelImpl? TryGetTopLevel(int id) + { + return s_topLevels.TryGetValue(id, out var weakReference) && + weakReference.TryGetTarget(out var topLevelImpl) ? + topLevelImpl : + null; + } + public BrowserTopLevelImpl(JSObject container, JSObject nativeControlHost, JSObject inputElement) { AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1); - _inputHandler = new BrowserInputHandler(this, container); - _textInputMethodImpl = new BrowserTextInputMethod(_inputHandler, container, inputElement); + _topLevelId = ++s_lastTopLevelId; + s_topLevels.Add(_topLevelId, new WeakReference(this)); + _inputHandler = new BrowserInputHandler(this, container, inputElement, _topLevelId); _insetsManager = new BrowserInsetsManager(); _nativeControlHost = new BrowserNativeControlHost(nativeControlHost); _storageProvider = new BrowserStorageProvider(); - _systemNavigationManager = new BrowserSystemNavigationManagerImpl(); _clipboard = new ClipboardImpl(); - _inputPane = new BrowserInputPane(container); _container = container; var opts = AvaloniaLocator.Current.GetService() ?? new BrowserPlatformOptions(); - _surface = RenderTargetBrowserSurface.Create(container, opts.RenderingMode); + _surface = RenderTargetBrowserSurface.Create(container, opts.RenderingMode, _topLevelId); _surface.SizeChanged += OnSizeChanged; _surface.ScalingChanged += OnScalingChanged; @@ -87,6 +96,8 @@ namespace Avalonia.Browser } public Compositor Compositor { get; } + public BrowserSurface? Surface => _surface; + public BrowserInputHandler InputHandler => _inputHandler; public void SetInputRoot(IInputRoot inputRoot) => _inputHandler.SetInputRoot(inputRoot); @@ -144,12 +155,12 @@ namespace Avalonia.Browser if (featureType == typeof(ITextInputMethodImpl)) { - return _textInputMethodImpl; + return _inputHandler.TextInputMethod; } if (featureType == typeof(ISystemNavigationManagerImpl)) { - return _systemNavigationManager; + return AvaloniaLocator.Current.GetService(); } if (featureType == typeof(INativeControlHostImpl)) @@ -169,7 +180,7 @@ namespace Avalonia.Browser if (featureType == typeof(IInputPane)) { - return _inputPane; + return _inputHandler.InputPane; } return null; diff --git a/src/Browser/Avalonia.Browser/ClipboardImpl.cs b/src/Browser/Avalonia.Browser/ClipboardImpl.cs index c4f5e90777..5df09e555d 100644 --- a/src/Browser/Avalonia.Browser/ClipboardImpl.cs +++ b/src/Browser/Avalonia.Browser/ClipboardImpl.cs @@ -10,12 +10,12 @@ namespace Avalonia.Browser { public Task GetTextAsync() { - return InputHelper.ReadClipboardTextAsync()!; + return InputHelper.ReadClipboardTextAsync(BrowserWindowingPlatform.GlobalThis)!; } public Task SetTextAsync(string? text) { - return InputHelper.WriteClipboardTextAsync(text ?? string.Empty); + return InputHelper.WriteClipboardTextAsync(BrowserWindowingPlatform.GlobalThis, text ?? string.Empty); } public async Task ClearAsync() => await SetTextAsync(""); diff --git a/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs b/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs index 7274fa582f..f7198a53e4 100644 --- a/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/CanvasHelper.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.JavaScript; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; +using Avalonia.Threading; namespace Avalonia.Browser.Interop; @@ -11,15 +8,25 @@ internal record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int internal static partial class CanvasHelper { - [JSImport("CanvasSurface.onSizeChanged", AvaloniaModule.MainModuleName)] - public static partial void OnSizeChanged( - JSObject canvasSurface, - [JSMarshalAs>] - // TODO: this callback should be . Revert after next .NET 9 preview. - Action onSizeChanged); + [JSExport] + public static Task OnSizeChanged(int topLevelId, double width, double height, double dpr) + { + if (BrowserWindowingPlatform.IsThreadingEnabled) + { + return Dispatcher.UIThread.InvokeAsync(() => BrowserTopLevelImpl + .TryGetTopLevel(topLevelId)?.Surface?.OnSizeChanged(width, height, dpr)) + .GetTask(); + } + else + { + BrowserTopLevelImpl + .TryGetTopLevel(topLevelId)?.Surface?.OnSizeChanged(width, height, dpr); + return Task.CompletedTask; + } + } [JSImport("CanvasSurface.create", AvaloniaModule.MainModuleName)] - public static partial JSObject CreateRenderTargetSurface(JSObject canvasSurface, int[] modes, int threadId); + public static partial JSObject CreateRenderTargetSurface(JSObject canvasSurface, int[] modes, int topLevelId, int threadId); [JSImport("CanvasSurface.destroy", AvaloniaModule.MainModuleName)] public static partial void Destroy(JSObject canvasSurface); diff --git a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs index a567973131..6817e6a6da 100644 --- a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs @@ -1,36 +1,53 @@ -using System; -using System.Runtime.InteropServices.JavaScript; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Platform; namespace Avalonia.Browser.Interop; internal static partial class DomHelper { - [JSImport("globalThis.document.getElementById")] - internal static partial JSObject? GetElementById(string id); + [JSImport("AvaloniaDOM.getGlobalThis", AvaloniaModule.MainModuleName)] + internal static partial JSObject GetGlobalThis(); + + [JSImport("AvaloniaDOM.getFirstElementById", AvaloniaModule.MainModuleName)] + internal static partial JSObject? GetElementById(string id, JSObject parent); [JSImport("AvaloniaDOM.getFirstElementByClassName", AvaloniaModule.MainModuleName)] - internal static partial JSObject? GetElementsByClassName(string className, JSObject? parent); + internal static partial JSObject? GetElementsByClassName(string className, JSObject parent); [JSImport("AvaloniaDOM.createAvaloniaHost", AvaloniaModule.MainModuleName)] public static partial JSObject CreateAvaloniaHost(JSObject element); [JSImport("AvaloniaDOM.isFullscreen", AvaloniaModule.MainModuleName)] - public static partial bool IsFullscreen(); + public static partial bool IsFullscreen(JSObject globalThis); [JSImport("AvaloniaDOM.setFullscreen", AvaloniaModule.MainModuleName)] - public static partial JSObject SetFullscreen(bool isFullscreen); + public static partial Task SetFullscreen(JSObject globalThis, bool isFullscreen); [JSImport("AvaloniaDOM.getSafeAreaPadding", AvaloniaModule.MainModuleName)] - public static partial double[] GetSafeAreaPadding(); + public static partial double[] GetSafeAreaPadding(JSObject globalThis); - [JSImport("AvaloniaDOM.initSafeAreaPadding", AvaloniaModule.MainModuleName)] - public static partial void InitSafeAreaPadding(); + [JSImport("AvaloniaDOM.getDarkMode", AvaloniaModule.MainModuleName)] + public static partial int[] GetDarkMode(JSObject globalThis); [JSImport("AvaloniaDOM.addClass", AvaloniaModule.MainModuleName)] public static partial void AddCssClass(JSObject element, string className); - [JSImport("AvaloniaDOM.observeDarkMode", AvaloniaModule.MainModuleName)] - public static partial JSObject ObserveDarkMode( - [JSMarshalAs>] - Action observer); + [JSImport("AvaloniaDOM.initGlobalDomEvents", AvaloniaModule.MainModuleName)] + public static partial void InitGlobalDomEvents(JSObject globalThis); + + [JSExport] + public static Task DarkModeChanged(bool isDarkMode, bool isHighContrast) + { + (AvaloniaLocator.Current.GetService() as BrowserPlatformSettings)?.OnValuesChanged(isDarkMode, isHighContrast); + return Task.CompletedTask; + } + + [JSExport] + public static Task DocumentVisibilityChanged(string visibilityState) + { + (AvaloniaLocator.Current.GetService() as BrowserActivatableLifetime)?.OnVisibilityStateChanged(visibilityState); + return Task.CompletedTask; + } } diff --git a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs index 928c0e23c6..29ddb36e9e 100644 --- a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs @@ -7,69 +7,85 @@ namespace Avalonia.Browser.Interop; internal static partial class InputHelper { - [JSImport("InputHelper.subscribeKeyEvents", AvaloniaModule.MainModuleName)] - public static partial void SubscribeKeyEvents( - JSObject htmlElement, - [JSMarshalAs>] - // TODO: this callback should be . Revert after next .NET 9 preview. - Func keyDown, - [JSMarshalAs>] - // TODO: this callback should be . Revert after next .NET 9 preview. - Func keyUp); - - [JSImport("InputHelper.subscribeTextEvents", AvaloniaModule.MainModuleName)] - public static partial void SubscribeTextEvents( - JSObject htmlElement, - [JSMarshalAs>] - Func onBeforeInput, - [JSMarshalAs>] - Func onCompositionStart, - [JSMarshalAs>] - Func onCompositionUpdate, - [JSMarshalAs>] - Func onCompositionEnd); - - [JSImport("InputHelper.subscribePointerEvents", AvaloniaModule.MainModuleName)] - public static partial void SubscribePointerEvents( - JSObject htmlElement, - [JSMarshalAs>] - Func pointerMove, - [JSMarshalAs>] - Func pointerDown, - [JSMarshalAs>] - Func pointerUp, - [JSMarshalAs>] - Func pointerCancel, - [JSMarshalAs>] - Func wheel); + public static Task RedirectInputAsync(int topLevelId, Action handler) + { + if (BrowserTopLevelImpl.TryGetTopLevel(topLevelId) is { } topLevelImpl) handler(topLevelImpl); + return Task.CompletedTask; + } [JSImport("InputHelper.subscribeInputEvents", AvaloniaModule.MainModuleName)] - public static partial void SubscribeInputEvents( - JSObject htmlElement, - [JSMarshalAs>] - Func input); - - [JSImport("InputHelper.subscribeDropEvents", AvaloniaModule.MainModuleName)] - public static partial void SubscribeDropEvents(JSObject containerElement, - [JSMarshalAs>] Func dragEvent); - - [JSImport("InputHelper.subscribeKeyboardGeometryChange", AvaloniaModule.MainModuleName)] - public static partial void SubscribeKeyboardGeometryChange(JSObject containerElement, - [JSMarshalAs>] Func handler); - - [JSImport("InputHelper.subscribeVisibilityChange", AvaloniaModule.MainModuleName)] - public static partial bool SubscribeVisibilityChange([JSMarshalAs>] Action handler); + public static partial void SubscribeInputEvents(JSObject htmlElement, int topLevelId); + + [JSExport] + public static Task OnKeyDown(int topLevelId, string code, string key, int modifier) => + RedirectInputAsync(topLevelId, t => t.InputHandler.OnKeyDown(code, key, modifier)); + + [JSExport] + public static Task OnKeyUp(int topLevelId, string code, string key, int modifier) => + RedirectInputAsync(topLevelId, t => t.InputHandler.OnKeyUp(code, key, modifier)); + + [JSExport] + public static Task OnBeforeInput(int topLevelId, string inputType, int start, int end) => + RedirectInputAsync(topLevelId, t => t.InputHandler.TextInputMethod.OnBeforeInput(inputType, start, end)); + + [JSExport] + public static Task OnCompositionStart(int topLevelId) => + RedirectInputAsync(topLevelId, t => t.InputHandler.TextInputMethod.OnCompositionStart()); + + [JSExport] + public static Task OnCompositionUpdate(int topLevelId, string? data) => + RedirectInputAsync(topLevelId, t => t.InputHandler.TextInputMethod.OnCompositionUpdate(data)); + + [JSExport] + public static Task OnCompositionEnd(int topLevelId, string? data) => + RedirectInputAsync(topLevelId, t => t.InputHandler.TextInputMethod.OnCompositionEnd(data)); + + [JSExport] + public static Task OnPointerMove(int topLevelId, string pointerType, [JSMarshalAs] long pointerId, + double offsetX, double offsetY, double pressure, double tiltX, double tiltY, double twist, int modifier, JSObject argsObj) => + RedirectInputAsync(topLevelId, t => t.InputHandler + .OnPointerMove(pointerType, pointerId, offsetX, offsetY, pressure, tiltX, tiltY, twist, modifier, argsObj)); + + [JSExport] + public static Task OnPointerDown(int topLevelId, string pointerType, [JSMarshalAs] long pointerId, int buttons, + double offsetX, double offsetY, double pressure, double tiltX, double tiltY, double twist, int modifier) => + RedirectInputAsync(topLevelId, t => t.InputHandler + .OnPointerDown(pointerType, pointerId, buttons, offsetX, offsetY, pressure, tiltX, tiltY, twist, modifier)); + + [JSExport] + public static Task OnPointerUp(int topLevelId, string pointerType, [JSMarshalAs] long pointerId, int buttons, + double offsetX, double offsetY, double pressure, double tiltX, double tiltY, double twist, int modifier) => + RedirectInputAsync(topLevelId, t => t.InputHandler + .OnPointerUp(pointerType, pointerId, buttons, offsetX, offsetY, pressure, tiltX, tiltY, twist, modifier)); + + [JSExport] + public static Task OnPointerCancel(int topLevelId, string pointerType, [JSMarshalAs] long pointerId, + double offsetX, double offsetY, double pressure, double tiltX, double tiltY, double twist, int modifier) => + RedirectInputAsync(topLevelId, t => t.InputHandler + .OnPointerCancel(pointerType, pointerId, offsetX, offsetY, pressure, tiltX, tiltY, twist, modifier)); + + [JSExport] + public static Task OnWheel(int topLevelId, + double offsetX, double offsetY, + double deltaX, double deltaY, int modifier) => + RedirectInputAsync(topLevelId, t => t.InputHandler.OnWheel(offsetX, offsetY, deltaX, deltaY, modifier)); + + [JSExport] + public static Task OnDragDrop(int topLevelId, string type, double offsetX, double offsetY, int modifiers, string? effectAllowedStr, JSObject? dataTransfer) => + RedirectInputAsync(topLevelId, t => t.InputHandler.OnDragEvent(type, offsetX, offsetY, modifiers, effectAllowedStr, dataTransfer)); + + [JSExport] + public static Task OnKeyboardGeometryChange(int topLevelId, double x, double y, double width, double height) => + RedirectInputAsync(topLevelId, t => t.InputHandler.InputPane + .OnGeometryChange(x, y, width, height)); [JSImport("InputHelper.getCoalescedEvents", AvaloniaModule.MainModuleName)] - [return: JSMarshalAs>] - public static partial JSObject[] GetCoalescedEvents(JSObject pointerEvent); + [return: JSMarshalAs>] + public static partial double[] GetCoalescedEvents(JSObject pointerEvent); [JSImport("InputHelper.clearInput", AvaloniaModule.MainModuleName)] public static partial void ClearInputElement(JSObject htmlElement); - [JSImport("InputHelper.isInputElement", AvaloniaModule.MainModuleName)] - public static partial void IsInputElement(JSObject htmlElement); - [JSImport("InputHelper.focusElement", AvaloniaModule.MainModuleName)] public static partial void FocusElement(JSObject htmlElement); @@ -89,17 +105,19 @@ internal static partial class InputHelper public static partial void SetBounds(JSObject htmlElement, int x, int y, int width, int height, int caret); [JSImport("InputHelper.initializeBackgroundHandlers", AvaloniaModule.MainModuleName)] - public static partial void InitializeBackgroundHandlers(); + public static partial void InitializeBackgroundHandlers(JSObject globalThis); [JSImport("InputHelper.readClipboardText", AvaloniaModule.MainModuleName)] - public static partial Task ReadClipboardTextAsync(); + public static partial Task ReadClipboardTextAsync(JSObject globalThis); + + [JSImport("InputHelper.writeClipboardText", AvaloniaModule.MainModuleName)] + public static partial Task WriteClipboardTextAsync(JSObject globalThis, string text); [JSImport("InputHelper.setPointerCapture", AvaloniaModule.MainModuleName)] - public static partial void SetPointerCapture(JSObject containerElement, [JSMarshalAs] long pointerId); + public static partial void + SetPointerCapture(JSObject containerElement, [JSMarshalAs] long pointerId); [JSImport("InputHelper.releasePointerCapture", AvaloniaModule.MainModuleName)] - public static partial void ReleasePointerCapture(JSObject containerElement, [JSMarshalAs] long pointerId); - - [JSImport("globalThis.navigator.clipboard.writeText")] - public static partial Task WriteClipboardTextAsync(string text); + public static partial void ReleasePointerCapture(JSObject containerElement, + [JSMarshalAs] long pointerId); } diff --git a/src/Browser/Avalonia.Browser/Interop/NavigationHelper.cs b/src/Browser/Avalonia.Browser/Interop/NavigationHelper.cs index b0032962f9..b79fe2a03d 100644 --- a/src/Browser/Avalonia.Browser/Interop/NavigationHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/NavigationHelper.cs @@ -1,5 +1,7 @@ using System; using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; +using Avalonia.Platform; namespace Avalonia.Browser.Interop; @@ -8,6 +10,13 @@ internal static partial class NavigationHelper [JSImport("NavigationHelper.addBackHandler", AvaloniaModule.MainModuleName)] public static partial void AddBackHandler([JSMarshalAs>] Func backHandlerCallback); + public static Task OnBackRequested() + { + var handled = (AvaloniaLocator.Current.GetService() as BrowserSystemNavigationManagerImpl)? + .OnBackRequested() ?? false; + return Task.FromResult(handled); + } + [JSImport("window.open")] public static partial JSObject? WindowOpen(string uri, string target); } diff --git a/src/Browser/Avalonia.Browser/ManualTriggerRenderTimer.cs b/src/Browser/Avalonia.Browser/ManualTriggerRenderTimer.cs deleted file mode 100644 index e9a314e823..0000000000 --- a/src/Browser/Avalonia.Browser/ManualTriggerRenderTimer.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Diagnostics; -using Avalonia.Rendering; - -namespace Avalonia.Browser -{ - internal class ManualTriggerRenderTimer : IRenderTimer - { - private static readonly Stopwatch s_sw = Stopwatch.StartNew(); - - public static ManualTriggerRenderTimer Instance { get; } = new(); - - public void RaiseTick() => Tick?.Invoke(s_sw.Elapsed); - - public event Action? Tick; - public bool RunsInBackground => false; - } -} diff --git a/src/Browser/Avalonia.Browser/Rendering/BrowserSurface.cs b/src/Browser/Avalonia.Browser/Rendering/BrowserSurface.cs index 9a4726481a..e93cdee703 100644 --- a/src/Browser/Avalonia.Browser/Rendering/BrowserSurface.cs +++ b/src/Browser/Avalonia.Browser/Rendering/BrowserSurface.cs @@ -40,14 +40,12 @@ internal abstract class BrowserSurface : IDisposable protected virtual void Initialize() { - CanvasHelper.OnSizeChanged(JsSurface, OnSizeChanged); - var w = JsSurface.GetPropertyAsDouble("width"); - var h = JsSurface.GetPropertyAsDouble("height"); + var w = JsSurface.GetPropertyAsInt32("width"); + var h = JsSurface.GetPropertyAsInt32("height"); var s = JsSurface.GetPropertyAsDouble("scaling"); - Console.WriteLine($"Initial size: {w} {h} {s}"); - OnSizeChanged((int)w, (int)h, s); + OnSizeChanged(w, h, s); } - + public virtual void Dispose() { CanvasHelper.Destroy(JsSurface); @@ -57,9 +55,8 @@ internal abstract class BrowserSurface : IDisposable ClientSize = default; } - protected virtual void OnSizeChanged(double pixelWidth, double pixelHeight, double dpr) + public virtual void OnSizeChanged(double pixelWidth, double pixelHeight, double dpr) { - Console.WriteLine($"OnSizeChanged: {Dispatcher.UIThread.CheckAccess()} {pixelWidth} {pixelHeight} {dpr} "); var oldScaling = Scaling; var oldClientSize = ClientSize; RenderSize = new PixelSize((int)pixelWidth, (int)pixelHeight); diff --git a/src/Browser/Avalonia.Browser/Rendering/RenderTargetBrowserSurface.cs b/src/Browser/Avalonia.Browser/Rendering/RenderTargetBrowserSurface.cs index c9fc086675..f97b329734 100644 --- a/src/Browser/Avalonia.Browser/Rendering/RenderTargetBrowserSurface.cs +++ b/src/Browser/Avalonia.Browser/Rendering/RenderTargetBrowserSurface.cs @@ -14,7 +14,7 @@ internal class RenderTargetBrowserSurface : BrowserSurface private readonly BrowserPlatformGraphics _graphics; private record InitParams(Compositor Compositor, BrowserPlatformGraphics Graphics); - + private static InitParams CreateCompositor(JSObject jsSurface) { var targetId = jsSurface.GetPropertyAsInt32("targetId"); @@ -36,7 +36,7 @@ internal class RenderTargetBrowserSurface : BrowserSurface return [_graphics.Target]; } - protected override void OnSizeChanged(double pixelWidth, double pixelHeight, double dpr) + public override void OnSizeChanged(double pixelWidth, double pixelHeight, double dpr) { _graphics.CanvasSize = (Size: new PixelSize((int)pixelWidth, (int)pixelHeight), Scaling: dpr); base.OnSizeChanged(pixelWidth, pixelHeight, dpr); @@ -93,9 +93,9 @@ internal class RenderTargetBrowserSurface : BrowserSurface base.Dispose(); } - public static RenderTargetBrowserSurface Create(JSObject container, IReadOnlyList modes) + public static RenderTargetBrowserSurface Create(JSObject container, IReadOnlyList modes, int topLevelId) { - var js = CanvasHelper.CreateRenderTargetSurface(container, modes.Select(m => (int)m).ToArray(), RenderWorker.WorkerThreadId); + var js = CanvasHelper.CreateRenderTargetSurface(container, modes.Select(m => (int)m).ToArray(), topLevelId, RenderWorker.WorkerThreadId); return new RenderTargetBrowserSurface(js); } -} \ No newline at end of file +} diff --git a/src/Browser/Avalonia.Browser/WindowingPlatform.cs b/src/Browser/Avalonia.Browser/WindowingPlatform.cs index bcc0a4a801..82d546e8bb 100644 --- a/src/Browser/Avalonia.Browser/WindowingPlatform.cs +++ b/src/Browser/Avalonia.Browser/WindowingPlatform.cs @@ -1,24 +1,37 @@ using System; using System.Collections.Generic; using System.Reflection; +using System.Runtime.InteropServices.JavaScript; using System.Threading; using Avalonia.Browser.Interop; -using Avalonia.Browser.Skia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Platform; using Avalonia.Platform.Internal; -using Avalonia.Rendering; using Avalonia.Threading; namespace Avalonia.Browser; internal class BrowserWindowingPlatform : IWindowingPlatform { + internal static ManualRawEventGrouperDispatchQueue? EventGrouperDispatchQueue; + internal static readonly bool IsThreadingEnabled = DetectThreadSupport(); - + + internal static bool IsManagedDispatcherEnabled => + IsThreadingEnabled && + AvaloniaLocator.Current.GetService()?.PreferManagedThreadDispatcher != false; + + // Capture initial GlobalThis, so we can use it as a contextual bridge between threads. + private static JSObject? s_globalThis; + internal static JSObject GlobalThis + { + get => s_globalThis ?? throw new InvalidOperationException("Browser backend wasn't initialized. GlobalThis is null."); + set => s_globalThis = value; + } + static bool DetectThreadSupport() { // TODO Replace with public API https://github.com/dotnet/runtime/issues/77541. @@ -68,13 +81,23 @@ internal class BrowserWindowingPlatform : IWindowingPlatform .Bind().ToSingleton() .Bind().ToConstant(s_keyboard) .Bind().ToSingleton() + .Bind().ToSingleton() .Bind().ToConstant(instance) .Bind().ToSingleton() .Bind().ToSingleton() .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { })) .Bind().ToSingleton(); - AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); - + if (IsManagedDispatcherEnabled) + { + EventGrouperDispatchQueue = new(); + AvaloniaLocator.CurrentMutable.Bind().ToConstant( + new ManagedDispatcherImpl(new ManualRawEventGrouperDispatchQueueDispatcherInputProvider(EventGrouperDispatchQueue))); + } + else + { + AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); + } + // GC thread is the same as the main one when MT is disabled if (IsThreadingEnabled) UnmanagedBlob.SuppressFinalizerWarning = true; diff --git a/src/Browser/Avalonia.Browser/webapp/build.js b/src/Browser/Avalonia.Browser/webapp/build.js index 3b278e7dcb..a8b4ca3bff 100644 --- a/src/Browser/Avalonia.Browser/webapp/build.js +++ b/src/Browser/Avalonia.Browser/webapp/build.js @@ -8,7 +8,7 @@ require("esbuild").build({ bundle: true, minify: true, format: "esm", - target: "es2018", + target: "es2019", platform: "browser", sourcemap: "linked", loader: { ".ts": "ts" } diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts index d51ee4b184..175e51c0da 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts @@ -1,32 +1,28 @@ +import { JsExports } from "./jsExports"; export class AvaloniaDOM { + public static getGlobalThis() { + return globalThis; + } + public static addClass(element: HTMLElement, className: string): void { element.classList.add(className); } - static observeDarkMode(observer: (isDarkMode: boolean, isHighContrast: boolean) => boolean) { - if (globalThis.matchMedia === undefined) { - return false; - } - - const colorShemeMedia = globalThis.matchMedia("(prefers-color-scheme: dark)"); - const prefersContrastMedia = globalThis.matchMedia("(prefers-contrast: more)"); - - colorShemeMedia.addEventListener("change", (args: MediaQueryListEvent) => { - observer(args.matches, prefersContrastMedia.matches); - }); - prefersContrastMedia.addEventListener("change", (args: MediaQueryListEvent) => { - observer(colorShemeMedia.matches, args.matches); - }); + static getFirstElementById(className: string, parent: HTMLElement | Window): Element | null { + const parentNode = parent instanceof Window + ? parent.document + : parent.ownerDocument; - return { - isDarkMode: colorShemeMedia.matches, - isHighContrast: prefersContrastMedia.matches - }; + return parentNode.getElementById(className); } - static getFirstElementByClassName(className: string, parent?: HTMLElement): Element | null { - const elements = (parent ?? globalThis.document).getElementsByClassName(className); + static getFirstElementByClassName(className: string, parent: HTMLElement | Window): Element | null { + const parentNode = parent instanceof Window + ? parent.document + : parent; + + const elements = parentNode.getElementsByClassName(className); return elements ? elements[0] : null; } @@ -107,32 +103,68 @@ export class AvaloniaDOM { }; } - public static isFullscreen(): boolean { - return document.fullscreenElement != null; + public static isFullscreen(globalThis: Window): boolean { + return globalThis.document.fullscreenElement != null; } - public static async setFullscreen(isFullscreen: boolean) { + public static async setFullscreen(globalThis: Window, isFullscreen: boolean) { if (isFullscreen) { - const doc = document.documentElement; + const doc = globalThis.document.documentElement; await doc.requestFullscreen(); } else { - await document.exitFullscreen(); + await globalThis.document.exitFullscreen(); } } - public static initSafeAreaPadding(): void { - document.documentElement.style.setProperty("--av-sat", "env(safe-area-inset-top)"); - document.documentElement.style.setProperty("--av-sar", "env(safe-area-inset-right)"); - document.documentElement.style.setProperty("--av-sab", "env(safe-area-inset-bottom)"); - document.documentElement.style.setProperty("--av-sal", "env(safe-area-inset-left)"); + public static initGlobalDomEvents(globalThis: Window): void { + // Init Safe Area properties. + globalThis.document.documentElement.style.setProperty("--av-sat", "env(safe-area-inset-top)"); + globalThis.document.documentElement.style.setProperty("--av-sar", "env(safe-area-inset-right)"); + globalThis.document.documentElement.style.setProperty("--av-sab", "env(safe-area-inset-bottom)"); + globalThis.document.documentElement.style.setProperty("--av-sal", "env(safe-area-inset-left)"); + + // Subscribe on DarkMode changes. + if (globalThis.matchMedia !== undefined) { + const colorSchemeMedia = globalThis.matchMedia("(prefers-color-scheme: dark)"); + const prefersContrastMedia = globalThis.matchMedia("(prefers-contrast: more)"); + + colorSchemeMedia.addEventListener("change", (args: MediaQueryListEvent) => { + JsExports.DomHelper.DarkModeChanged(args.matches, prefersContrastMedia.matches); + }); + prefersContrastMedia.addEventListener("change", (args: MediaQueryListEvent) => { + JsExports.DomHelper.DarkModeChanged(colorSchemeMedia.matches, args.matches); + }); + } + + globalThis.document.addEventListener("visibilitychange", () => { + JsExports.DomHelper.DocumentVisibilityChanged(globalThis.document.visibilityState); + }); + + // Report initial value. + if (globalThis.document.visibilityState === "visible") { + globalThis.setTimeout(() => { + JsExports.DomHelper.DocumentVisibilityChanged(globalThis.document.visibilityState); + }, 10); + } } - public static getSafeAreaPadding(): number[] { - const top = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--av-sat")); - const bottom = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--av-sab")); - const left = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--av-sal")); - const right = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--av-sar")); + public static getSafeAreaPadding(globalThis: Window): number[] { + const top = parseFloat(getComputedStyle(globalThis.document.documentElement).getPropertyValue("--av-sat")); + const bottom = parseFloat(getComputedStyle(globalThis.document.documentElement).getPropertyValue("--av-sab")); + const left = parseFloat(getComputedStyle(globalThis.document.documentElement).getPropertyValue("--av-sal")); + const right = parseFloat(getComputedStyle(globalThis.document.documentElement).getPropertyValue("--av-sar")); return [left, top, bottom, right]; } + + public static getDarkMode(globalThis: Window): number[] { + if (globalThis.matchMedia === undefined) return [0, 0]; + + const colorSchemeMedia = globalThis.matchMedia("(prefers-color-scheme: dark)"); + const prefersContrastMedia = globalThis.matchMedia("(prefers-contrast: more)"); + return [ + colorSchemeMedia.matches ? 1 : 0, + prefersContrastMedia.matches ? 1 : 0 + ]; + } } diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts index df4fc19e6b..5d14597642 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts @@ -1,4 +1,5 @@ import { CaretHelper } from "./caretHelper"; +import { JsExports } from "./jsExports"; enum RawInputModifiers { None = 0, @@ -54,7 +55,7 @@ export class InputHelper { this.clipboardState = ClipboardState.Ready; } - public static async readClipboardText(): Promise { + public static async readClipboardText(globalThis: Window): Promise { if (globalThis.navigator.clipboard.readText) { return await globalThis.navigator.clipboard.readText(); } else { @@ -72,23 +73,38 @@ export class InputHelper { } } - public static subscribeKeyEvents( - element: HTMLInputElement, - keyDownCallback: (code: string, key: string, modifiers: string) => boolean, - keyUpCallback: (code: string, key: string, modifiers: string) => boolean) { + public static async writeClipboardText(globalThis: Window, text: string): Promise { + return await globalThis.navigator.clipboard.writeText(text); + } + + public static subscribeInputEvents(element: HTMLInputElement, topLevelId: number) { + const keySub = this.subscribeKeyEvents(element, topLevelId); + const pointerSub = this.subscribePointerEvents(element, topLevelId); + const textSub = this.subscribeTextEvents(element, topLevelId); + const dndSub = this.subscribeDropEvents(element, topLevelId); + const paneSub = this.subscribeKeyboardGeometryChange(element, topLevelId); + + return () => { + keySub(); + pointerSub(); + textSub(); + dndSub(); + paneSub(); + }; + } + + public static subscribeKeyEvents(element: HTMLInputElement, topLevelId: number) { const keyDownHandler = (args: KeyboardEvent) => { - if (keyDownCallback(args.code, args.key, this.getModifiers(args))) { - if (this.clipboardState !== ClipboardState.Pending) { - args.preventDefault(); - } + JsExports.InputHelper.OnKeyDown(topLevelId, args.code, args.key, this.getModifiers(args)); + if (this.clipboardState !== ClipboardState.Pending) { + args.preventDefault(); } }; element.addEventListener("keydown", keyDownHandler); const keyUpHandler = (args: KeyboardEvent) => { - if (keyUpCallback(args.code, args.key, this.getModifiers(args))) { - args.preventDefault(); - } + JsExports.InputHelper.OnKeyUp(topLevelId, args.code, args.key, this.getModifiers(args)); + args.preventDefault(); if (this.rejectClipboard) { this.rejectClipboard(); } @@ -104,14 +120,9 @@ export class InputHelper { public static subscribeTextEvents( element: HTMLInputElement, - beforeInputCallback: (args: InputEvent, start: number, end: number) => boolean, - compositionStartCallback: (args: CompositionEvent) => boolean, - compositionUpdateCallback: (args: CompositionEvent) => boolean, - compositionEndCallback: (args: CompositionEvent) => boolean) { + topLevelId: number) { const compositionStartHandler = (args: CompositionEvent) => { - if (compositionStartCallback(args)) { - args.preventDefault(); - } + JsExports.InputHelper.OnCompositionStart(topLevelId); }; element.addEventListener("compositionstart", compositionStartHandler); @@ -128,23 +139,19 @@ export class InputHelper { start = 2; end = start + 2; } - if (beforeInputCallback(args, start, end)) { - args.preventDefault(); - } + + JsExports.InputHelper.OnBeforeInput(topLevelId, args.inputType, start, end); }; element.addEventListener("beforeinput", beforeInputHandler); const compositionUpdateHandler = (args: CompositionEvent) => { - if (compositionUpdateCallback(args)) { - args.preventDefault(); - } + JsExports.InputHelper.OnCompositionUpdate(topLevelId, args.data); }; element.addEventListener("compositionupdate", compositionUpdateHandler); const compositionEndHandler = (args: CompositionEvent) => { - if (compositionEndCallback(args)) { - args.preventDefault(); - } + JsExports.InputHelper.OnCompositionEnd(topLevelId, args.data); + args.preventDefault(); }; element.addEventListener("compositionend", compositionEndHandler); @@ -157,34 +164,38 @@ export class InputHelper { public static subscribePointerEvents( element: HTMLInputElement, - pointerMoveCallback: (args: PointerEvent) => boolean, - pointerDownCallback: (args: PointerEvent) => boolean, - pointerUpCallback: (args: PointerEvent) => boolean, - pointerCancelCallback: (args: PointerEvent) => boolean, - wheelCallback: (args: WheelEvent) => boolean + topLevelId: number ) { const pointerMoveHandler = (args: PointerEvent) => { - pointerMoveCallback(args); + JsExports.InputHelper.OnPointerMove( + topLevelId, args.pointerType, args.pointerId, args.offsetX, args.offsetY, + args.pressure, args.tiltX, args.tiltY, args.twist, this.getModifiers(args), args); args.preventDefault(); }; const pointerDownHandler = (args: PointerEvent) => { - pointerDownCallback(args); + JsExports.InputHelper.OnPointerDown( + topLevelId, args.pointerType, args.pointerId, args.button, args.offsetX, args.offsetY, + args.pressure, args.tiltX, args.tiltY, args.twist, this.getModifiers(args)); args.preventDefault(); }; const pointerUpHandler = (args: PointerEvent) => { - pointerUpCallback(args); + JsExports.InputHelper.OnPointerUp( + topLevelId, args.pointerType, args.pointerId, args.button, args.offsetX, args.offsetY, + args.pressure, args.tiltX, args.tiltY, args.twist, this.getModifiers(args)); args.preventDefault(); }; const pointerCancelHandler = (args: PointerEvent) => { - pointerCancelCallback(args); - args.preventDefault(); + JsExports.InputHelper.OnPointerCancel( + topLevelId, args.pointerType, args.pointerId, args.offsetX, args.offsetY, + args.pressure, args.tiltX, args.tiltY, args.twist, this.getModifiers(args)); }; const wheelHandler = (args: WheelEvent) => { - wheelCallback(args); + JsExports.InputHelper.OnWheel( + topLevelId, args.offsetX, args.offsetY, args.deltaX, args.deltaY, this.getModifiers(args)); args.preventDefault(); }; @@ -203,72 +214,59 @@ export class InputHelper { }; } - public static subscribeInputEvents( - element: HTMLInputElement, - inputCallback: (value: string) => boolean - ) { - const inputHandler = (args: Event) => { - if (inputCallback((args as any).value)) { - args.preventDefault(); - } - }; - element.addEventListener("input", inputHandler); - - return () => { - element.removeEventListener("input", inputHandler); - }; - } - public static subscribeDropEvents( element: HTMLInputElement, - dragEvent: (args: any) => boolean + topLevelId: number ) { - const dragHandler = (args: Event) => { - if (dragEvent(args as any)) { - args.preventDefault(); - } + const handler = (args: DragEvent) => { + const dataObject = args.dataTransfer; + JsExports.InputHelper.OnDragDrop(topLevelId, args.type, args.offsetX, args.offsetY, this.getModifiers(args), dataObject?.effectAllowed, dataObject); }; - element.addEventListener("dragover", dragHandler); - element.addEventListener("dragenter", dragHandler); - element.addEventListener("dragleave", dragHandler); - element.addEventListener("drop", dragHandler); + const overAndDropHandler = (args: DragEvent) => { + args.preventDefault(); + handler(args); + }; + element.addEventListener("dragover", overAndDropHandler); + element.addEventListener("dragenter", handler); + element.addEventListener("dragleave", handler); + element.addEventListener("drop", overAndDropHandler); return () => { - element.removeEventListener("dragover", dragHandler); - element.removeEventListener("dragenter", dragHandler); - element.removeEventListener("dragleave", dragHandler); - element.removeEventListener("drop", dragHandler); + element.removeEventListener("dragover", overAndDropHandler); + element.removeEventListener("dragenter", handler); + element.removeEventListener("dragleave", handler); + element.removeEventListener("drop", overAndDropHandler); }; } - public static getCoalescedEvents(pointerEvent: PointerEvent): PointerEvent[] { - return pointerEvent.getCoalescedEvents(); + public static getCoalescedEvents(pointerEvent: PointerEvent): number[] { + return pointerEvent.getCoalescedEvents() + .flatMap(e => [e.offsetX, e.offsetY, e.pressure, e.tiltX, e.tiltY, e.twist]); } public static subscribeKeyboardGeometryChange( element: HTMLInputElement, - handler: (args: any) => boolean) { + topLevelId: number) { if ("virtualKeyboard" in navigator) { // (navigator as any).virtualKeyboard.overlaysContent = true; - (navigator as any).virtualKeyboard.addEventListener("geometrychange", (event: any) => { + const listener = (event: any) => { const elementRect = element.getBoundingClientRect(); const keyboardRect = event.target.boundingRect as DOMRect; - handler({ - x: keyboardRect.x - elementRect.x, - y: keyboardRect.y - elementRect.y, - width: keyboardRect.width, - height: keyboardRect.height - }); - }); + + JsExports.InputHelper.OnKeyboardGeometryChange( + topLevelId, + keyboardRect.x - elementRect.x, + keyboardRect.y - elementRect.y, + keyboardRect.width, + keyboardRect.height); + }; + (navigator as any).virtualKeyboard.addEventListener("geometrychange", listener); + return () => { + (navigator as any).virtualKeyboard.removeEventListener("geometrychange", listener); + }; } - } - public static subscribeVisibilityChange( - handler: (state: boolean) => void): boolean { - document.addEventListener("visibilitychange", () => { - handler(document.visibilityState === "visible"); - }); - return document.visibilityState === "visible"; + return () => {}; } public static clearInput(inputElement: HTMLInputElement) { @@ -316,7 +314,7 @@ export class InputHelper { inputElement.style.width = `${inputElement.scrollWidth}px`; } - private static getModifiers(args: KeyboardEvent): string { + private static getModifiers(args: KeyboardEvent | PointerEvent | WheelEvent | DragEvent): number { let modifiers = RawInputModifiers.None; if (args.ctrlKey) { modifiers |= RawInputModifiers.Control; } @@ -324,7 +322,17 @@ export class InputHelper { if (args.shiftKey) { modifiers |= RawInputModifiers.Shift; } if (args.metaKey) { modifiers |= RawInputModifiers.Meta; } - return modifiers.toString(); + const buttons = (args as PointerEvent).buttons; + if (buttons) { + if (buttons & 1) { modifiers |= RawInputModifiers.LeftMouseButton; } + if (buttons & 2) { modifiers |= (args.type === "pen" ? RawInputModifiers.PenBarrelButton : RawInputModifiers.RightMouseButton); } + if (buttons & 4) { modifiers |= RawInputModifiers.MiddleMouseButton; } + if (buttons & 8) { modifiers |= RawInputModifiers.XButton1MouseButton; } + if (buttons & 16) { modifiers |= RawInputModifiers.XButton2MouseButton; } + if (buttons & 32) { modifiers |= RawInputModifiers.PenEraser; } + } + + return modifiers; } public static setPointerCapture(containerElement: HTMLInputElement, pointerId: number): void { diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/jsExports.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/jsExports.ts index 18fde7449a..6a148d57e5 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/jsExports.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/jsExports.ts @@ -1,6 +1,22 @@ export class JsExports { public static resolvedExports?: any; public static exportsPromise: Promise; + + public static get InputHelper(): any { + return this.resolvedExports?.Avalonia.Browser.Interop.InputHelper; + } + + public static get DomHelper(): any { + return this.resolvedExports?.Avalonia.Browser.Interop.DomHelper; + } + + public static get TimerHelper(): any { + return this.resolvedExports?.Avalonia.Browser.Interop.TimerHelper; + } + + public static get CanvasHelper(): any { + return this.resolvedExports?.Avalonia.Browser.Interop.CanvasHelper; + } } async function resolveExports (): Promise { const runtimeApi = await globalThis.getDotnetRuntime(0); diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/canvasSurface.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/canvasSurface.ts index 35c4455b4b..ed53492365 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/canvasSurface.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/rendering/canvasSurface.ts @@ -2,21 +2,18 @@ import { ResizeHandler } from "./resizeHandler"; import { WebRenderTargetRegistry } from "./webRenderTargetRegistry"; import { AvaloniaDOM } from "../dom"; import { BrowserRenderingMode } from "./renderingMode"; +import { JsExports } from "../jsExports"; export class CanvasSurface { public targetId: number; private sizeParams?: [number, number, number]; - private sizeChangedCallback?: (width: number, height: number, dpr: number) => void; - constructor(public canvas: HTMLCanvasElement, modes: BrowserRenderingMode[], threadId: number) { + constructor(public canvas: HTMLCanvasElement, modes: BrowserRenderingMode[], topLevelId: number, threadId: number) { this.targetId = WebRenderTargetRegistry.create(threadId, canvas, modes); - // No need to ubsubscribe, canvas never leaves JS world, it should be GC'ed with all callbacks. ResizeHandler.observeSize(canvas, (width, height, dpr) => { this.sizeParams = [width, height, dpr]; - if (this.sizeChangedCallback) { - this.sizeChangedCallback(width, height, dpr); - } + JsExports.CanvasHelper?.OnSizeChanged(topLevelId, width, height, dpr); }); } @@ -36,20 +33,13 @@ export class CanvasSurface { } public destroy(): void { - delete this.sizeChangedCallback; } - public onSizeChanged(sizeChangedCallback: (width: number, height: number, dpr: number) => void) { - if (this.sizeChangedCallback) { throw new Error("For simplicity, we don't support multiple size changed callbacks per surface, not needed yet."); } - this.sizeChangedCallback = sizeChangedCallback; - // if (this.sizeParams) { this.sizeChangedCallback(this.sizeParams[0], this.sizeParams[1], this.sizeParams[2]); } - } - - public static create(container: HTMLElement, modes: BrowserRenderingMode[], threadId: number): CanvasSurface { + public static create(container: HTMLElement, modes: BrowserRenderingMode[], topLevelId: number, threadId: number): CanvasSurface { const canvas = AvaloniaDOM.createAvaloniaCanvas(container); AvaloniaDOM.attachCanvas(container, canvas); try { - return new CanvasSurface(canvas, modes, threadId); + return new CanvasSurface(canvas, modes, topLevelId, threadId); } catch (ex) { AvaloniaDOM.detachCanvas(container, canvas); throw ex; @@ -59,8 +49,4 @@ export class CanvasSurface { public static destroy(surface: CanvasSurface) { surface.destroy(); } - - public static onSizeChanged(surface: CanvasSurface, sizeChangedCallback: (width: number, height: number, dpr: number) => void) { - surface.onSizeChanged(sizeChangedCallback); - } } diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts index 3ccd669293..ca12392fd2 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/timer.ts @@ -3,24 +3,18 @@ import { JsExports } from "./jsExports"; export class TimerHelper { public static runAnimationFrames(): void { function render(time: number) { - if (JsExports.resolvedExports != null) { - JsExports.resolvedExports.Avalonia.Browser.Interop.TimerHelper.JsExportOnAnimationFrame(time); - } + JsExports.TimerHelper?.JsExportOnAnimationFrame(); self.requestAnimationFrame(render); } self.requestAnimationFrame(render); } static onTimeout() { - if (JsExports.resolvedExports != null) { - JsExports.resolvedExports.Avalonia.Browser.Interop.TimerHelper.JsExportOnTimeout(); - } else { console.error("TimerHelper.onTimeout call while uninitialized"); } + JsExports.TimerHelper?.JsExportOnTimeout(); } static onInterval() { - if (JsExports.resolvedExports != null) { - JsExports.resolvedExports.Avalonia.Browser.Interop.TimerHelper.JsExportOnInterval(); - } else { console.error("TimerHelper.onInterval call while uninitialized"); } + JsExports.TimerHelper?.JsExportOnInterval(); } public static setTimeout(interval: number): number { diff --git a/src/Browser/Avalonia.Browser/webapp/tsconfig.json b/src/Browser/Avalonia.Browser/webapp/tsconfig.json index 1450ce4c57..b3d7c9045f 100644 --- a/src/Browser/Avalonia.Browser/webapp/tsconfig.json +++ b/src/Browser/Avalonia.Browser/webapp/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es2018", + "target": "es2019", "module": "es2020", "strict": true, "sourceMap": true, @@ -11,6 +11,7 @@ "lib": [ "dom", "es2018", + "es2019", "esnext.asynciterable" ] }, @@ -18,4 +19,4 @@ "node_modules" ] } - \ No newline at end of file +