Bläddra i källkod

Implemented TouchDevice and touch input support for X11 backend

Nikita Tsukanov 6 år sedan
förälder
incheckning
6827179476

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

+ 1 - 0
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();
         
         /// <summary>
         /// Gets the control that is currently capturing by the mouse, if any.

+ 60 - 0
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<IInputElement>().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;
+        }
+    }
+}

+ 70 - 0
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
+{
+    /// <summary>
+    /// Handles raw touch events
+    /// This class is supposed to be used on per-toplevel basis, don't event try to have a global instance
+    /// </summary>
+    public class TouchDevice : IInputDevice
+    {
+        Dictionary<long, Pointer> _pointers = new Dictionary<long, Pointer>();
+
+        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));
+            }
+        }
+        
+    }
+}

+ 60 - 0
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;
+        }
+    }
+    */
+}

+ 3 - 1
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
     {

+ 44 - 11
src/Avalonia.X11/XI2Manager.cs

@@ -11,6 +11,7 @@ namespace Avalonia.X11
     unsafe class XI2Manager
     {
         private X11Info _x11;
+        private bool _multitouch;
         private Dictionary<IntPtr, IXI2Client> _clients = new Dictionary<IntPtr, IXI2Client>();
         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<int, List<XiEventType>>
+            var events = new List<XiEventType>
             {
-                [_pointerDevice.Id] = new List<XiEventType>()
+                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<int, List<XiEventType>> {[_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<int, double> 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);
         }
     }
     

+ 8 - 1
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
     {

+ 2 - 0
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;

+ 2 - 0
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;