From 392cd874e01a00e9150cef01c67fc568a94e6c4a Mon Sep 17 00:00:00 2001 From: Johan Polson <40406620+johanpolson@users.noreply.github.com> Date: Mon, 1 Apr 2024 04:18:25 +0200 Subject: [PATCH] Fix for browser pointer capture not released when pointer up outside of browser (#15103) * init * rework --- .../Avalonia.Browser/BrowserMouseDevice.cs | 36 +++++++++++++++++++ .../Avalonia.Browser/BrowserTopLevelImpl.cs | 31 +++++++++++----- .../Avalonia.Browser/Interop/InputHelper.cs | 6 ++++ .../webapp/modules/avalonia/input.ts | 10 ++++++ 4 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 src/Browser/Avalonia.Browser/BrowserMouseDevice.cs diff --git a/src/Browser/Avalonia.Browser/BrowserMouseDevice.cs b/src/Browser/Avalonia.Browser/BrowserMouseDevice.cs new file mode 100644 index 0000000000..c8c23b6000 --- /dev/null +++ b/src/Browser/Avalonia.Browser/BrowserMouseDevice.cs @@ -0,0 +1,36 @@ +using System.Runtime.InteropServices.JavaScript; +using Avalonia.Input; +using Avalonia.Browser.Interop; + +namespace Avalonia.Browser +{ + internal class BrowserMouseDevice : MouseDevice + { + internal long PointerId { get; } + + public BrowserMouseDevice(long pointerId, JSObject container) : base( new BrowserMousePointer(pointerId, container)) + { + PointerId = pointerId; + } + + internal class BrowserMousePointer : Pointer + { + private readonly JSObject _container; + private readonly long _pointerId; + + internal BrowserMousePointer(long pointerId, JSObject container) : base(GetNextFreeId(),PointerType.Mouse, true) + { + _pointerId = pointerId; + _container = container; + } + + protected override void PlatformCapture(IInputElement? element) + { + if (element is { }) + InputHelper.SetPointerCapture(_container, _pointerId); + else + InputHelper.ReleasePointerCapture(_container, _pointerId); + } + } + } +} diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs index 3d3887f73d..629c42af9b 100644 --- a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs +++ b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs @@ -35,6 +35,9 @@ namespace Avalonia.Browser private readonly ClipboardImpl _clipboard; private readonly IInsetsManager? _insetsManager; private readonly IInputPane _inputPane; + private readonly List _mouseDevices; + private readonly JSObject _container; + public BrowserTopLevelImpl(AvaloniaView avaloniaView, JSObject container) { @@ -50,6 +53,9 @@ namespace Avalonia.Browser _systemNavigationManager = new BrowserSystemNavigationManagerImpl(); _clipboard = new ClipboardImpl(); _inputPane = new BrowserInputPane(container); + + _mouseDevices = new(); + _container = container; } public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds; @@ -90,7 +96,7 @@ namespace Avalonia.Browser if (_inputRoot is { } && Input is { } input) { - var device = GetPointerDevice(pointerType); + var device = GetPointerDevice(pointerType, touchPointId); var args = device is TouchDevice ? new RawTouchEventArgs(device, Timestamp, _inputRoot, eventType, p, modifiers, touchPointId) { @@ -110,21 +116,28 @@ namespace Avalonia.Browser return false; } - private IPointerDevice GetPointerDevice(string pointerType) + private IPointerDevice GetPointerDevice(string pointerType, long pointerId) { - return pointerType switch + if (pointerType == "touch") + return _touchDevice; + else if (pointerType == "pen") + return _penDevice; + + foreach (var mouseDevice in _mouseDevices) { - "touch" => _touchDevice, - "pen" => _penDevice, - _ => MouseDevice - }; + if (mouseDevice.PointerId == pointerId) + return mouseDevice; + } + var newMouseDevice = new BrowserMouseDevice(pointerId, _container); + _mouseDevices.Add(newMouseDevice); + return newMouseDevice; } public bool RawMouseWheelEvent(Point p, Vector v, RawInputModifiers modifiers) { if (_inputRoot is { }) { - var args = new RawMouseWheelEventArgs(MouseDevice, Timestamp, _inputRoot, p, v, modifiers); + var args = new RawMouseWheelEventArgs(WheelMouseDevice, Timestamp, _inputRoot, p, v, modifiers); Input?.Invoke(args); @@ -235,7 +248,7 @@ namespace Avalonia.Browser public Action? TransparencyLevelChanged { get; set; } public Action? Closed { get; set; } public Action? LostFocus { get; set; } - public IMouseDevice MouseDevice { get; } = new MouseDevice(); + public IMouseDevice WheelMouseDevice { get; } = new MouseDevice(); public IKeyboardDevice KeyboardDevice { get; } = BrowserWindowingPlatform.Keyboard; public WindowTransparencyLevel TransparencyLevel => WindowTransparencyLevel.None; diff --git a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs index 7f294e155e..0b9fe1d95c 100644 --- a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs @@ -92,6 +92,12 @@ internal static partial class InputHelper [JSImport("InputHelper.readClipboardText", AvaloniaModule.MainModuleName)] public static partial Task ReadClipboardTextAsync(); + [JSImport("InputHelper.setPointerCapture", AvaloniaModule.MainModuleName)] + 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); } diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts index e03aa9e532..8a2d1569f5 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts @@ -326,4 +326,14 @@ export class InputHelper { return modifiers; } + + public static setPointerCapture(containerElement: HTMLInputElement, pointerId: number): void { + containerElement.setPointerCapture(pointerId); + } + + public static releasePointerCapture(containerElement: HTMLInputElement, pointerId: number): void { + if (containerElement.hasPointerCapture(pointerId)) { + containerElement.releasePointerCapture(pointerId); + } + } }