From 6827179476e80fba8914fa034788cd3c0e31e8db Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 29 May 2019 00:54:56 +0300 Subject: [PATCH] Implemented TouchDevice and touch input support for X11 backend --- src/Avalonia.Input/IPointer.cs | 7 ++ src/Avalonia.Input/MouseDevice.cs | 1 + src/Avalonia.Input/Pointer.cs | 60 ++++++++++++++++ src/Avalonia.Input/TouchDevice.cs | 70 +++++++++++++++++++ .../TapGestureRecognizer.cs | 60 ++++++++++++++++ src/Avalonia.X11/X11Platform.cs | 4 +- src/Avalonia.X11/XI2Manager.cs | 55 ++++++++++++--- src/Avalonia.X11/XIStructs.cs | 9 ++- .../MouseTestHelper.cs | 2 + .../DefaultMenuInteractionHandlerTests.cs | 2 + 10 files changed, 257 insertions(+), 13 deletions(-) create mode 100644 src/Avalonia.Input/Pointer.cs create mode 100644 src/Avalonia.Input/TouchDevice.cs create mode 100644 src/Avalonia.Input/TouchGestureRecognizers/TapGestureRecognizer.cs diff --git a/src/Avalonia.Input/IPointer.cs b/src/Avalonia.Input/IPointer.cs index 15f1fef531..c4b2c5bf99 100644 --- a/src/Avalonia.Input/IPointer.cs +++ b/src/Avalonia.Input/IPointer.cs @@ -2,6 +2,7 @@ namespace Avalonia.Input { public interface IPointer { + int Id { get; } void Capture(IInputElement control); IInputElement Captured { get; } PointerType Type { get; } @@ -14,4 +15,10 @@ namespace Avalonia.Input Mouse, Touch } + + public class PointerIds + { + private static int s_nextPointerId = 1000; + public static int Next() => s_nextPointerId++; + } } diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index 352c69a726..2bceb53366 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -24,6 +24,7 @@ namespace Avalonia.Input PointerType IPointer.Type => PointerType.Mouse; bool IPointer.IsPrimary => true; + int IPointer.Id { get; } = PointerIds.Next(); /// /// Gets the control that is currently capturing by the mouse, if any. diff --git a/src/Avalonia.Input/Pointer.cs b/src/Avalonia.Input/Pointer.cs new file mode 100644 index 0000000000..c1f50108ae --- /dev/null +++ b/src/Avalonia.Input/Pointer.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq; +using Avalonia.VisualTree; + +namespace Avalonia.Input +{ + public class Pointer : IPointer, IDisposable + { + public Pointer(int id, PointerType type, bool isPrimary, IInputElement implicitlyCaptured) + { + Id = id; + Type = type; + IsPrimary = isPrimary; + ImplicitlyCaptured = implicitlyCaptured; + if (ImplicitlyCaptured != null) + ImplicitlyCaptured.DetachedFromVisualTree += OnImplicitCaptureDetached; + } + + public int Id { get; } + + public void Capture(IInputElement control) + { + if (Captured != null) + Captured.DetachedFromVisualTree -= OnCaptureDetached; + Captured = control; + if (Captured != null) + Captured.DetachedFromVisualTree += OnCaptureDetached; + } + + IInputElement GetNextCapture(IVisual parent) => + parent as IInputElement ?? parent.GetVisualAncestors().OfType().FirstOrDefault(); + + private void OnCaptureDetached(object sender, VisualTreeAttachmentEventArgs e) + { + Capture(GetNextCapture(e.Parent)); + } + + private void OnImplicitCaptureDetached(object sender, VisualTreeAttachmentEventArgs e) + { + ImplicitlyCaptured.DetachedFromVisualTree -= OnImplicitCaptureDetached; + ImplicitlyCaptured = GetNextCapture(e.Parent); + if (ImplicitlyCaptured != null) + ImplicitlyCaptured.DetachedFromVisualTree += OnImplicitCaptureDetached; + } + + public IInputElement Captured { get; private set; } + public IInputElement ImplicitlyCaptured { get; private set; } + public IInputElement GetEffectiveCapture() => Captured ?? ImplicitlyCaptured; + + public PointerType Type { get; } + public bool IsPrimary { get; } + public void Dispose() + { + if (ImplicitlyCaptured != null) + ImplicitlyCaptured.DetachedFromVisualTree -= OnImplicitCaptureDetached; + if (Captured != null) + Captured.DetachedFromVisualTree -= OnCaptureDetached; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs new file mode 100644 index 0000000000..baf7e7b3c4 --- /dev/null +++ b/src/Avalonia.Input/TouchDevice.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Input.Raw; +using Avalonia.VisualTree; + +namespace Avalonia.Input +{ + /// + /// Handles raw touch events + /// This class is supposed to be used on per-toplevel basis, don't event try to have a global instance + /// + public class TouchDevice : IInputDevice + { + Dictionary _pointers = new Dictionary(); + + static InputModifiers GetModifiers(InputModifiers modifiers, bool left) + { + var mask = (InputModifiers)0x7fffffff ^ InputModifiers.LeftMouseButton ^ InputModifiers.MiddleMouseButton ^ + InputModifiers.RightMouseButton; + modifiers &= mask; + if (left) + modifiers |= InputModifiers.LeftMouseButton; + return modifiers; + } + + public void ProcessRawEvent(RawInputEventArgs ev) + { + var args = (RawTouchEventArgs)ev; + if (!_pointers.TryGetValue(args.TouchPointId, out var pointer)) + { + if (args.Type == RawMouseEventType.TouchEnd) + return; + var hit = args.Root.InputHitTest(args.Position); + + _pointers[args.TouchPointId] = pointer = new Pointer(PointerIds.Next(), + PointerType.Touch, _pointers.Count == 0, hit); + } + + + var target = pointer.GetEffectiveCapture() ?? args.Root; + if (args.Type == RawMouseEventType.TouchBegin) + { + var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary); + target.RaiseEvent(new PointerPressedEventArgs(target, pointer, + args.Root, args.Position, new PointerPointProperties(modifiers), + modifiers)); + } + + if (args.Type == RawMouseEventType.TouchEnd) + { + _pointers.Remove(args.TouchPointId); + var modifiers = GetModifiers(args.InputModifiers, false); + using (pointer) + { + target.RaiseEvent(new PointerReleasedEventArgs(target, pointer, + args.Root, args.Position, new PointerPointProperties(modifiers), + modifiers, pointer.IsPrimary ? MouseButton.Left : MouseButton.None)); + } + } + + if (args.Type == RawMouseEventType.TouchUpdate) + { + var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary); + target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root, + args.Position, new PointerPointProperties(modifiers), modifiers)); + } + } + + } +} diff --git a/src/Avalonia.Input/TouchGestureRecognizers/TapGestureRecognizer.cs b/src/Avalonia.Input/TouchGestureRecognizers/TapGestureRecognizer.cs new file mode 100644 index 0000000000..4745981f96 --- /dev/null +++ b/src/Avalonia.Input/TouchGestureRecognizers/TapGestureRecognizer.cs @@ -0,0 +1,60 @@ +using System; +using Avalonia.Interactivity; + +namespace Avalonia.Input.TouchGestureRecognizers +{ + /* + public class TapGestureRecognizer : ITouchGestureRecognizer + { + long _started; + Point _startPoint; + const double Distance = 20; + const long MaxTapDuration = 500; + TouchGestureRecognizerResult ITouchGestureRecognizer.RecognizeGesture(IInputElement owner, TouchEventArgs args) + { + if (args.Route == RoutingStrategies.Tunnel) + return TouchGestureRecognizerResult.Continue; + + // Multi-touch sequence + if(args.Touches.Count > 1) + return TouchGestureRecognizerResult.Reject; + // Sequence started, save the start time + if(args.Type == TouchEventType.TouchBegin) + { + _started = args.Timestamp; + var pos = args.Touches[0].GetPosition(owner); + if (pos == null) + return TouchGestureRecognizerResult.Reject; + _startPoint = pos.Value; + return TouchGestureRecognizerResult.Continue; + } + + if(args.Type == TouchEventType.TouchEnd) + { + var pos = args.RemovedTouches[0].GetPosition(owner); + if (pos == null) + return TouchGestureRecognizerResult.Reject; + var endPoint = pos.Value; + + if(Math.Abs(endPoint.X - _startPoint.X) < Distance + && Math.Abs(endPoint.Y - _startPoint.Y) < Distance + && (args.Timestamp - _started) < MaxTapDuration) + { + ((Interactive)args.RemovedTouches[0].InitialTarget).RaiseEvent( + new RoutedEventArgs(Gestures.TappedEvent)); + return TouchGestureRecognizerResult.Accept; + } + else + return TouchGestureRecognizerResult.Reject; + } + return TouchGestureRecognizerResult.Continue; + } + + public void Cancel() + { + _started = 0; + _startPoint = default; + } + } + */ +} diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index ce03113169..cf5902eff7 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -28,6 +28,7 @@ namespace Avalonia.X11 public X11PlatformOptions Options { get; private set; } public void Initialize(X11PlatformOptions options) { + Options = options; XInitThreads(); Display = XOpenDisplay(IntPtr.Zero); DeferredDisplay = XOpenDisplay(IntPtr.Zero); @@ -66,7 +67,7 @@ namespace Avalonia.X11 GlxGlPlatformFeature.TryInitialize(Info); } - Options = options; + } public IntPtr DeferredDisplay { get; set; } @@ -96,6 +97,7 @@ namespace Avalonia public bool UseEGL { get; set; } public bool UseGpu { get; set; } = true; public string WmClass { get; set; } = Assembly.GetEntryAssembly()?.GetName()?.Name ?? "AvaloniaApplication"; + public bool? EnableMultiTouch { get; set; } } public static class AvaloniaX11PlatformExtensions { diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs index ee73ccc907..928c0a5d75 100644 --- a/src/Avalonia.X11/XI2Manager.cs +++ b/src/Avalonia.X11/XI2Manager.cs @@ -11,6 +11,7 @@ namespace Avalonia.X11 unsafe class XI2Manager { private X11Info _x11; + private bool _multitouch; private Dictionary _clients = new Dictionary(); class DeviceInfo { @@ -77,11 +78,14 @@ namespace Avalonia.X11 private PointerDeviceInfo _pointerDevice; private AvaloniaX11Platform _platform; + private readonly TouchDevice _touchDevice = new TouchDevice(); + public bool Init(AvaloniaX11Platform platform) { _platform = platform; _x11 = platform.Info; + _multitouch = platform.Options?.EnableMultiTouch ?? false; var devices =(XIDeviceInfo*) XIQueryDevice(_x11.Display, (int)XiPredefinedDeviceId.XIAllMasterDevices, out int num); for (var c = 0; c < num; c++) @@ -121,16 +125,23 @@ namespace Avalonia.X11 public XEventMask AddWindow(IntPtr xid, IXI2Client window) { _clients[xid] = window; - - XiSelectEvents(_x11.Display, xid, new Dictionary> + var events = new List { - [_pointerDevice.Id] = new List() + XiEventType.XI_Motion, + XiEventType.XI_ButtonPress, + XiEventType.XI_ButtonRelease + }; + + if (_multitouch) + events.AddRange(new[] { - XiEventType.XI_Motion, - XiEventType.XI_ButtonPress, - XiEventType.XI_ButtonRelease, - } - }); + XiEventType.XI_TouchBegin, + XiEventType.XI_TouchUpdate, + XiEventType.XI_TouchEnd + }); + + XiSelectEvents(_x11.Display, xid, + new Dictionary> {[_pointerDevice.Id] = events}); // We are taking over mouse input handling from here return XEventMask.PointerMotionMask @@ -154,8 +165,9 @@ namespace Avalonia.X11 _pointerDevice.Update(changed->Classes, changed->NumClasses); } - //TODO: this should only be used for non-touch devices - if (xev->evtype >= XiEventType.XI_ButtonPress && xev->evtype <= XiEventType.XI_Motion) + + if ((xev->evtype >= XiEventType.XI_ButtonPress && xev->evtype <= XiEventType.XI_Motion) + || (xev->evtype>=XiEventType.XI_TouchBegin&&xev->evtype<=XiEventType.XI_TouchEnd)) { var dev = (XIDeviceEvent*)xev; if (_clients.TryGetValue(dev->EventWindow, out var client)) @@ -165,6 +177,23 @@ namespace Avalonia.X11 void OnDeviceEvent(IXI2Client client, ParsedDeviceEvent ev) { + if (ev.Type == XiEventType.XI_TouchBegin + || ev.Type == XiEventType.XI_TouchUpdate + || ev.Type == XiEventType.XI_TouchEnd) + { + var type = ev.Type == XiEventType.XI_TouchBegin ? + RawMouseEventType.TouchBegin : + (ev.Type == XiEventType.XI_TouchUpdate ? + RawMouseEventType.TouchUpdate : + RawMouseEventType.TouchEnd); + client.ScheduleInput(new RawTouchEventArgs(_touchDevice, + ev.Timestamp, client.InputRoot, type, ev.Position, ev.Modifiers, ev.Detail)); + return; + } + + if (_multitouch && ev.Emulated) + return; + if (ev.Type == XiEventType.XI_Motion) { Vector scrollDelta = default; @@ -210,7 +239,7 @@ namespace Avalonia.X11 client.ScheduleInput(new RawMouseEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot, type.Value, ev.Position, ev.Modifiers)); } - + _pointerDevice.UpdateValuators(ev.Valuators); } } @@ -222,6 +251,8 @@ namespace Avalonia.X11 public ulong Timestamp { get; } public Point Position { get; } public int Button { get; set; } + public int Detail { get; set; } + public bool Emulated { get; set; } public Dictionary Valuators { get; } public ParsedDeviceEvent(XIDeviceEvent* ev) { @@ -258,6 +289,8 @@ namespace Avalonia.X11 Valuators[c] = *values++; if (Type == XiEventType.XI_ButtonPress || Type == XiEventType.XI_ButtonRelease) Button = ev->detail; + Detail = ev->detail; + Emulated = ev->flags.HasFlag(XiDeviceEventFlags.XIPointerEmulated); } } diff --git a/src/Avalonia.X11/XIStructs.cs b/src/Avalonia.X11/XIStructs.cs index ef49a72c43..4675ef47f2 100644 --- a/src/Avalonia.X11/XIStructs.cs +++ b/src/Avalonia.X11/XIStructs.cs @@ -230,13 +230,20 @@ namespace Avalonia.X11 public double root_y; public double event_x; public double event_y; - public int flags; + public XiDeviceEventFlags flags; public XIButtonState buttons; public XIValuatorState valuators; public XIModifierState mods; public XIModifierState group; } + [Flags] + public enum XiDeviceEventFlags : int + { + None = 0, + XIPointerEmulated = (1 << 16) + } + [StructLayout(LayoutKind.Sequential)] unsafe struct XIEvent { diff --git a/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs b/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs index 8b61f82c28..80a6e7a912 100644 --- a/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs +++ b/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs @@ -9,6 +9,8 @@ namespace Avalonia.Controls.UnitTests class TestPointer : IPointer { + public int Id { get; } = PointerIds.Next(); + public void Capture(IInputElement control) { Captured = control; diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs index 94a95b6db4..61e018dfa2 100644 --- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -12,6 +12,8 @@ namespace Avalonia.Controls.UnitTests.Platform { class FakePointer : IPointer { + public int Id { get; } = PointerIds.Next(); + public void Capture(IInputElement control) { Captured = control;