From 05061f20d3ad71c536ce9b9bc27a33b167a26e92 Mon Sep 17 00:00:00 2001
From: Pyrdacor <trobt@web.de>
Date: Sun, 14 Jan 2024 21:56:52 +0100
Subject: [PATCH 01/21] start of adding touch input


From 5071a090bbc099f5654baf5d1444c2be816899be Mon Sep 17 00:00:00 2001
From: Pyrdacor <trobt@web.de>
Date: Sun, 14 Jan 2024 22:01:32 +0100
Subject: [PATCH 02/21] Added missing mouse update call for SDL backend

---
 src/Input/Silk.NET.Input.Sdl/SdlMouse.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/Input/Silk.NET.Input.Sdl/SdlMouse.cs b/src/Input/Silk.NET.Input.Sdl/SdlMouse.cs
index e6a9fb20fe..46a554a95f 100644
--- a/src/Input/Silk.NET.Input.Sdl/SdlMouse.cs
+++ b/src/Input/Silk.NET.Input.Sdl/SdlMouse.cs
@@ -63,6 +63,7 @@ public void Update()
                 _scrollWheels[0] = default;
             }
             _wheelChanged = false;
+            HandleUpdate();
         }
 
         public void DoEvent(Event @event)

From 53b77ef4d8aad71bf79438231f918134121f8a3b Mon Sep 17 00:00:00 2001
From: Pyrdacor <trobt@web.de>
Date: Sun, 14 Jan 2024 22:06:13 +0100
Subject: [PATCH 03/21] Added touch device interface and touch finger struct

---
 .../Interfaces/ITouchDevice.cs                | 43 +++++++++++++
 .../Structs/TouchFinger.cs                    | 62 +++++++++++++++++++
 2 files changed, 105 insertions(+)
 create mode 100644 src/Input/Silk.NET.Input.Common/Interfaces/ITouchDevice.cs
 create mode 100644 src/Input/Silk.NET.Input.Common/Structs/TouchFinger.cs

diff --git a/src/Input/Silk.NET.Input.Common/Interfaces/ITouchDevice.cs b/src/Input/Silk.NET.Input.Common/Interfaces/ITouchDevice.cs
new file mode 100644
index 0000000000..a75e60d4a1
--- /dev/null
+++ b/src/Input/Silk.NET.Input.Common/Interfaces/ITouchDevice.cs
@@ -0,0 +1,43 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+
+namespace Silk.NET.Input
+{
+    /// <summary>
+    /// An interface representing a touch device.
+    /// </summary>
+    public interface ITouchDevice : IInputDevice, IDisposable
+    {
+        /// <summary>
+        /// The known fingers this touch device has tracked.
+        /// </summary>
+        // ReSharper disable once ReturnTypeCanBeEnumerable.Global
+        IReadOnlyDictionary<long, TouchFinger> Fingers { get; }
+
+        /// <summary>
+        /// Checks if a specific finger is currently down on the touch surface.
+        /// </summary>
+        /// <param name="index">The finger index to check.</param>
+        /// <returns>Whether or not the finger is pressed down.</returns>
+        bool IsFingerDown(long index);
+
+        /// <summary>
+        /// Called when a finger touches the surface.
+        /// </summary>
+        event Action<ITouchDevice, TouchFinger>? FingerDown;
+
+        /// <summary>
+        /// Called when a finger is lifted from the surface.
+        /// </summary>
+        event Action<ITouchDevice, TouchFinger>? FingerUp;
+
+        /// <summary>
+        /// Called when the finger is moved while on the surface.
+        /// </summary>
+        event Action<ITouchDevice, TouchFinger, Vector2>? FingerMove;
+    }
+}
diff --git a/src/Input/Silk.NET.Input.Common/Structs/TouchFinger.cs b/src/Input/Silk.NET.Input.Common/Structs/TouchFinger.cs
new file mode 100644
index 0000000000..5499431dba
--- /dev/null
+++ b/src/Input/Silk.NET.Input.Common/Structs/TouchFinger.cs
@@ -0,0 +1,62 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Numerics;
+
+namespace Silk.NET.Input
+{
+    /// <summary>
+    /// Represents a touch finger.
+    /// </summary>
+    public readonly struct TouchFinger
+    {
+        /// <summary>
+        /// The index of the finger.
+        /// </summary>
+        public long Index { get; }
+
+        /// <summary>
+        /// The last known position of the finger.
+        /// </summary>
+        public Vector2 Position { get; }
+
+        /// <summary>
+        /// The last known normalized position of the finger.
+        /// </summary>
+        public Vector2 NormalizedPosition { get; }
+
+        /// <summary>
+        /// The last known speed of the finger.
+        /// </summary>
+        public Vector2 Speed { get; }
+
+        /// <summary>
+        /// The last known normalized speed of the finger.
+        /// </summary>
+        public Vector2 NormalizedSpeed { get; }
+
+        /// <summary>
+        /// Finger down on the touch surface.
+        /// </summary>
+        public bool Down { get; }
+
+        /// <summary>
+        /// Creates a new instance of the touch finger struct.
+        /// </summary>
+        /// <param name="index">The index of the finger.</param>
+        /// <param name="position">The position of the finger.</param>
+        /// <param name="normalizedPosition">The normalized position of the finger.</param>
+        /// <param name="speed">The speed of the finger.</param>
+        /// <param name="normalizedSpeed">The normalized speed of the finger.</param>
+        /// <param name="down">Boolean which is true if the finger is down.</param>
+        public TouchFinger(long index, Vector2 position, Vector2 normalizedPosition, Vector2 speed, Vector2 normalizedSpeed, bool down)
+        {
+            Index = index;
+            Position = position;
+            NormalizedPosition = normalizedPosition;
+            Speed = speed;
+            NormalizedSpeed = normalizedSpeed;
+            Down = down;
+        }
+    }
+}

From 952a717f2933546f581d50d3cdc7ca7ee79d8f28 Mon Sep 17 00:00:00 2001
From: Pyrdacor <trobt@web.de>
Date: Sun, 14 Jan 2024 22:20:52 +0100
Subject: [PATCH 04/21] Added gesture recognizer dummy and SDL touch device

---
 .../Interfaces/ITouchDevice.cs                |   5 +
 .../TouchGestureRecognizer.cs                 |  28 +++++
 .../Silk.NET.Input.Sdl/SdlTouchDevice.cs      | 115 ++++++++++++++++++
 3 files changed, 148 insertions(+)
 create mode 100644 src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
 create mode 100644 src/Input/Silk.NET.Input.Sdl/SdlTouchDevice.cs

diff --git a/src/Input/Silk.NET.Input.Common/Interfaces/ITouchDevice.cs b/src/Input/Silk.NET.Input.Common/Interfaces/ITouchDevice.cs
index a75e60d4a1..481525ae2a 100644
--- a/src/Input/Silk.NET.Input.Common/Interfaces/ITouchDevice.cs
+++ b/src/Input/Silk.NET.Input.Common/Interfaces/ITouchDevice.cs
@@ -18,6 +18,11 @@ public interface ITouchDevice : IInputDevice, IDisposable
         // ReSharper disable once ReturnTypeCanBeEnumerable.Global
         IReadOnlyDictionary<long, TouchFinger> Fingers { get; }
 
+        /// <summary>
+        /// A recognizer for gestures.
+        /// </summary>
+        TouchGestureRecognizer GestureRecognizer { get; }
+
         /// <summary>
         /// Checks if a specific finger is currently down on the touch surface.
         /// </summary>
diff --git a/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs b/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
new file mode 100644
index 0000000000..7560dc04c0
--- /dev/null
+++ b/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+namespace Silk.NET.Input
+{
+    /// <summary>
+    /// Touch gesture recognizer.
+    /// </summary>
+    public sealed class TouchGestureRecognizer
+    {
+        private readonly ITouchDevice _device;
+
+        internal TouchGestureRecognizer(ITouchDevice device)
+        {
+            _device = device;
+        }
+
+        /// <summary>
+        /// General gesture recognition update for time-based recognition aspects.
+        /// </summary>
+        public void Update()
+        {
+
+        }
+    }
+}
diff --git a/src/Input/Silk.NET.Input.Sdl/SdlTouchDevice.cs b/src/Input/Silk.NET.Input.Sdl/SdlTouchDevice.cs
new file mode 100644
index 0000000000..4a302af863
--- /dev/null
+++ b/src/Input/Silk.NET.Input.Sdl/SdlTouchDevice.cs
@@ -0,0 +1,115 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using Silk.NET.SDL;
+
+namespace Silk.NET.Input.Sdl
+{
+    internal class SdlTouchDevice : ITouchDevice, ISdlDevice
+    {
+        private readonly SdlInputContext _ctx;
+        private readonly Dictionary<long, TouchFinger> _fingers = new Dictionary<long, TouchFinger>();
+        private readonly Dictionary<long, DateTime> _fingerEventTimes = new Dictionary<long, DateTime>();
+
+        /// <summary>
+        /// Id of the touch device.
+        /// </summary>
+        public long TouchId { get; }
+
+        /// <summary>
+        /// Type of the touch device.
+        /// </summary>
+        public TouchDeviceType TouchDeviceType { get; }
+
+        public SdlTouchDevice(SdlInputContext ctx, long touchId, TouchDeviceType touchDeviceType, int index)
+        {
+            _ctx = ctx;
+            TouchId = touchId;
+            TouchDeviceType = touchDeviceType;
+            Index = index;
+            GestureRecognizer = new TouchGestureRecognizer(this);
+        }
+
+        public string Name => GetTouchDeviceName();
+        public int Index { get; }
+        public bool IsConnected { get; set; } = false;
+
+        public IReadOnlyDictionary<long, TouchFinger> Fingers => _fingers;
+
+        public TouchGestureRecognizer GestureRecognizer { get; }
+
+        public bool IsFingerDown(long index) => _fingers.TryGetValue(index, out var finger) && finger.Down;
+        public event Action<ITouchDevice, TouchFinger>? FingerDown;
+        public event Action<ITouchDevice, TouchFinger>? FingerUp;
+        public event Action<ITouchDevice, TouchFinger, Vector2>? FingerMove;
+
+        public void Update()
+        {
+            GestureRecognizer.Update();
+        }
+
+        public unsafe void DoEvent(Event @event)
+        {
+            var window = _ctx.Sdl.GetWindowFromID(@event.Tfinger.WindowID);
+            int windowWidth = 1;
+            int windowHeight = 1;
+            _ctx.Sdl.GetWindowSize(window, ref windowWidth, ref windowHeight);
+            Vector2 windowSize = new Vector2(windowWidth, windowHeight);
+            var normalizedPosition = new Vector2(@event.Tfinger.X, @event.Tfinger.Y);
+            var position = normalizedPosition * windowSize;
+
+            switch ((EventType) @event.Type)
+            {
+                case EventType.Fingerdown:
+                {
+                    var finger = new TouchFinger(@event.Tfinger.FingerId,
+                        position, normalizedPosition, Vector2.Zero, Vector2.Zero, true);
+                    FingerDown?.Invoke(this, finger);
+                    _fingers[finger.Index] = finger;
+                    _fingerEventTimes[finger.Index] = DateTime.Now;
+                    break;
+                }
+                case EventType.Fingerup:
+                {
+                    var finger = new TouchFinger(@event.Tfinger.FingerId,
+                        position, normalizedPosition, Vector2.Zero, Vector2.Zero, false);
+                    FingerUp?.Invoke(this, finger);
+                    _fingers.Remove(finger.Index);
+                    _fingerEventTimes.Remove(finger.Index);
+                    break;
+                }
+                case EventType.Fingermotion:
+                {
+                    var distance = new Vector2(@event.Tfinger.Dx, @event.Tfinger.Dy);
+                    var time = (DateTime.Now - _fingerEventTimes[@event.Tfinger.FingerId]).TotalSeconds;
+                    var normalizedSpeed = distance / (float) (time == 0.0 ? double.Epsilon : time);
+                    var speed = normalizedSpeed * windowSize;
+                    var finger = new TouchFinger(@event.Tfinger.FingerId,
+                        position, normalizedPosition, speed, normalizedSpeed,
+                        false);
+                    FingerMove?.Invoke(this, finger, distance);
+                    _fingers[finger.Index] = finger;
+                    _fingerEventTimes[finger.Index] = DateTime.Now;
+                    break;
+                }
+            }
+        }
+
+        public void Dispose()
+        {
+
+        }
+
+        private string GetTouchDeviceName()
+        {
+            try
+            {
+                return _ctx.Sdl.GetTouchNameS(Index);
+            }
+            catch
+            {
+                return "Silk.NET Touch Device (via SDL)";
+            }
+        }
+    }
+}

From 93d9fc1eb7cd966dc993d6030f045c4723c15291 Mon Sep 17 00:00:00 2001
From: Pyrdacor <trobt@web.de>
Date: Sun, 14 Jan 2024 22:26:24 +0100
Subject: [PATCH 05/21] Extended input contexts for touch devices

---
 .../Interfaces/IInputContext.cs               | 19 +++++++++
 .../InputContextImplementationBase.cs         |  2 +
 .../TouchGestureRecognizer.cs                 |  8 +++-
 .../Silk.NET.Input.Glfw/GlfwInputContext.cs   |  4 +-
 .../Silk.NET.Input.Sdl/SdlInputContext.cs     | 39 ++++++++++++++++---
 .../Silk.NET.Input.Sdl/SdlTouchDevice.cs      |  2 +-
 6 files changed, 66 insertions(+), 8 deletions(-)

diff --git a/src/Input/Silk.NET.Input.Common/Interfaces/IInputContext.cs b/src/Input/Silk.NET.Input.Common/Interfaces/IInputContext.cs
index 4f32ab9312..c4f20f3234 100644
--- a/src/Input/Silk.NET.Input.Common/Interfaces/IInputContext.cs
+++ b/src/Input/Silk.NET.Input.Common/Interfaces/IInputContext.cs
@@ -44,6 +44,25 @@ public interface IInputContext : IDisposable
         /// </remarks>
         IReadOnlyList<IMouse> Mice { get; }
 
+        /// <summary>
+        /// A list of all available touch devices.
+        /// </summary>
+        /// <remarks>
+        /// On some backends, this list may only contain 1 item. This is most likely because the underlying API doesn't
+        /// support multiple touch devices.
+        /// On some backends, this list might be empty. This is most likely because the underlying API doesn't
+        /// support touch devices.
+        /// </remarks>
+        IReadOnlyList<ITouchDevice> TouchDevices { get; }
+
+        /// <summary>
+        /// The primary touch device if any.
+        /// </summary>
+        /// <remarks>
+        /// On some backends this might just be an educated guess. Or might be null even though there are touch devices.
+        /// </remarks>
+        ITouchDevice? PrimaryTouchDevice { get; }
+
         /// <summary>
         /// A list of all other available input devices.
         /// </summary>
diff --git a/src/Input/Silk.NET.Input.Common/Internals/InputContextImplementationBase.cs b/src/Input/Silk.NET.Input.Common/Internals/InputContextImplementationBase.cs
index 1dd7721a8a..8f52530939 100644
--- a/src/Input/Silk.NET.Input.Common/Internals/InputContextImplementationBase.cs
+++ b/src/Input/Silk.NET.Input.Common/Internals/InputContextImplementationBase.cs
@@ -46,6 +46,8 @@ public void Dispose()
     public abstract IReadOnlyList<IJoystick> Joysticks { get; }
     public abstract IReadOnlyList<IKeyboard> Keyboards { get; }
     public abstract IReadOnlyList<IMouse> Mice { get; }
+    public abstract IReadOnlyList<ITouchDevice> TouchDevices { get; }
+    public abstract ITouchDevice? PrimaryTouchDevice { get; }
     public abstract IReadOnlyList<IInputDevice> OtherDevices { get; }
     public abstract event Action<IInputDevice, bool>? ConnectionChanged;
 }
diff --git a/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs b/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
index 7560dc04c0..9584d3b077 100644
--- a/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
+++ b/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
@@ -8,7 +8,7 @@ namespace Silk.NET.Input
     /// <summary>
     /// Touch gesture recognizer.
     /// </summary>
-    public sealed class TouchGestureRecognizer
+    public sealed class TouchGestureRecognizer : IDisposable
     {
         private readonly ITouchDevice _device;
 
@@ -17,6 +17,12 @@ internal TouchGestureRecognizer(ITouchDevice device)
             _device = device;
         }
 
+        /// <inheritdoc />
+        public void Dispose()
+        {
+
+        }
+
         /// <summary>
         /// General gesture recognition update for time-based recognition aspects.
         /// </summary>
diff --git a/src/Input/Silk.NET.Input.Glfw/GlfwInputContext.cs b/src/Input/Silk.NET.Input.Glfw/GlfwInputContext.cs
index 9e7a2f9f56..d5919a4b5b 100644
--- a/src/Input/Silk.NET.Input.Glfw/GlfwInputContext.cs
+++ b/src/Input/Silk.NET.Input.Glfw/GlfwInputContext.cs
@@ -7,7 +7,6 @@
 using Silk.NET.Input.Internals;
 using Silk.NET.Windowing;
 using Silk.NET.Windowing.Glfw;
-using Silk.NET.Windowing.Internals;
 
 namespace Silk.NET.Input.Glfw
 {
@@ -94,6 +93,9 @@ public override unsafe void CoreDispose()
         public override IReadOnlyList<IKeyboard> Keyboards { get; }
         public override IReadOnlyList<IMouse> Mice { get; }
         public override IReadOnlyList<IInputDevice> OtherDevices { get; } = Array.Empty<IInputDevice>();
+        public override IReadOnlyList<ITouchDevice> TouchDevices { get; } = Array.Empty<ITouchDevice>();
+        public override ITouchDevice? PrimaryTouchDevice { get; } = null;
+
         public override event Action<IInputDevice, bool>? ConnectionChanged;
     }
 }
diff --git a/src/Input/Silk.NET.Input.Sdl/SdlInputContext.cs b/src/Input/Silk.NET.Input.Sdl/SdlInputContext.cs
index ef9b429a43..9877595907 100644
--- a/src/Input/Silk.NET.Input.Sdl/SdlInputContext.cs
+++ b/src/Input/Silk.NET.Input.Sdl/SdlInputContext.cs
@@ -3,9 +3,9 @@
 
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using Silk.NET.Input.Internals;
 using Silk.NET.SDL;
-using Silk.NET.Windowing;
 using Silk.NET.Windowing.Sdl;
 
 namespace Silk.NET.Input.Sdl
@@ -32,6 +32,14 @@ public SdlInputContext(SdlView view) : base(view)
             );
             Keyboards = new IKeyboard[] {new SdlKeyboard(this)};
             Mice = new IMouse[] {new SdlMouse(this)};
+            int numTouchDevices = Sdl.GetNumTouchDevices();
+            SdlTouchDevices = new Dictionary<long, SdlTouchDevice>(numTouchDevices);
+            for (int i = 0; i < numTouchDevices; ++i)
+            {
+                long touchId = Sdl.GetTouchDevice(i);
+                SdlTouchDevices.Add(touchId, new SdlTouchDevice(this, touchId, Sdl.GetTouchDeviceType(touchId), i));
+            }
+            TouchDevices = new ReadOnlyCollectionListAdapter<SdlTouchDevice>(SdlTouchDevices.Values);
         }
 
         // Public properties
@@ -39,11 +47,14 @@ public SdlInputContext(SdlView view) : base(view)
         public override IReadOnlyList<IJoystick> Joysticks { get; }
         public override IReadOnlyList<IKeyboard> Keyboards { get; }
         public override IReadOnlyList<IMouse> Mice { get; }
+        public override IReadOnlyList<ITouchDevice> TouchDevices { get; }
+        public override ITouchDevice? PrimaryTouchDevice => TouchDevices.FirstOrDefault(td => td.IsConnected) ?? SdlTouchDevices.FirstOrDefault(td => td.Value.TouchId == SdlTouchDevices.Count).Value;
         public override IReadOnlyList<IInputDevice> OtherDevices { get; } = Array.Empty<IInputDevice>();
 
         // Implementation-specific properties
         public Dictionary<int, SdlGamepad> SdlGamepads { get; }
         public Dictionary<int, SdlJoystick> SdlJoysticks { get; }
+        public Dictionary<long, SdlTouchDevice> SdlTouchDevices { get; }
 
         public SDL.Sdl Sdl => _sdlView.Sdl;
         public override nint Handle => Window.Handle;
@@ -238,11 +249,25 @@ public override void ProcessEvents()
                     case EventType.Fingerdown:
                     case EventType.Fingerup:
                     case EventType.Fingermotion:
-                    case EventType.Dollargesture:
-                    case EventType.Dollarrecord:
-                    case EventType.Multigesture:
                     {
-                        // TODO touch input
+                        if (SdlTouchDevices.TryGetValue(@event.Tfinger.TouchId, out var td))
+                        {
+                            if (!td.IsConnected)
+                            {
+                                td.IsConnected = true;
+                                ConnectionChanged?.Invoke(td, true);
+                            }
+
+                            td.DoEvent(@event);
+                        }
+                        else
+                        {
+                            var touchId = @event.Tfinger.TouchId;
+                            var touchDevice = new SdlTouchDevice(this, touchId, Sdl.GetTouchDeviceType(touchId), SdlTouchDevices.Count) { IsConnected = true };
+                            SdlTouchDevices.Add(touchId, touchDevice);
+                            ConnectionChanged?.Invoke(touchDevice, true);
+                            touchDevice.DoEvent(@event);
+                        }
                         break;
                     }
                     default:
@@ -267,6 +292,10 @@ public override void ProcessEvents()
             {
                 gp.Update();
             }
+            foreach (var td in SdlTouchDevices.Values)
+            {
+                td.Update();
+            }
 
             // There's actually nowhere here that will raise an SDL error that we cause.
             // Sdl.ThrowError();
diff --git a/src/Input/Silk.NET.Input.Sdl/SdlTouchDevice.cs b/src/Input/Silk.NET.Input.Sdl/SdlTouchDevice.cs
index 4a302af863..0b91404f09 100644
--- a/src/Input/Silk.NET.Input.Sdl/SdlTouchDevice.cs
+++ b/src/Input/Silk.NET.Input.Sdl/SdlTouchDevice.cs
@@ -97,7 +97,7 @@ public unsafe void DoEvent(Event @event)
 
         public void Dispose()
         {
-
+            GestureRecognizer.Dispose();
         }
 
         private string GetTouchDeviceName()

From ee06cdc0f02195f3bbd0083f65d969daab5c91f4 Mon Sep 17 00:00:00 2001
From: Pyrdacor <trobt@web.de>
Date: Sun, 14 Jan 2024 22:30:24 +0100
Subject: [PATCH 06/21] Implemented touch gesture recognizer

---
 .../Enums/DoubleTapBehavior.cs                |  27 ++
 .../Silk.NET.Input.Common/Enums/Gesture.cs    |  54 +++
 .../Enums/MultiGestureHandling.cs             |  26 ++
 .../TouchGestureRecognizer.cs                 | 343 +++++++++++++++++-
 4 files changed, 447 insertions(+), 3 deletions(-)
 create mode 100644 src/Input/Silk.NET.Input.Common/Enums/DoubleTapBehavior.cs
 create mode 100644 src/Input/Silk.NET.Input.Common/Enums/Gesture.cs
 create mode 100644 src/Input/Silk.NET.Input.Common/Enums/MultiGestureHandling.cs

diff --git a/src/Input/Silk.NET.Input.Common/Enums/DoubleTapBehavior.cs b/src/Input/Silk.NET.Input.Common/Enums/DoubleTapBehavior.cs
new file mode 100644
index 0000000000..2ddb3493a1
--- /dev/null
+++ b/src/Input/Silk.NET.Input.Common/Enums/DoubleTapBehavior.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Silk.NET.Input
+{
+    /// <summary>
+    /// Controls the behavior of the double tap gesture tracking.
+    /// </summary>
+    public enum DoubleTapBehavior
+    {
+        /// <summary>
+        /// Always emit the first tap as a single tap.
+        /// </summary>
+        EmitFirstSingleTap,
+
+        /// <summary>
+        /// Always emit the first and second tap as single taps.
+        /// </summary>
+        EmitBothSingleTaps,
+
+        /// <summary>
+        /// Do not emit single taps and wait for the double tap time to elapse.
+        /// The first single tap is only emitted after the time has elapsed.
+        /// </summary>
+        WaitForDoubleTapTimeElapse
+    }
+}
diff --git a/src/Input/Silk.NET.Input.Common/Enums/Gesture.cs b/src/Input/Silk.NET.Input.Common/Enums/Gesture.cs
new file mode 100644
index 0000000000..aaadf937ac
--- /dev/null
+++ b/src/Input/Silk.NET.Input.Common/Enums/Gesture.cs
@@ -0,0 +1,54 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+namespace Silk.NET.Input
+{
+    /// <summary>
+    /// Controls the behavior of the double tap gesture tracking.
+    /// </summary>
+    [Flags]
+    public enum Gesture
+    {
+        /// <summary>
+        /// No gesture.
+        /// </summary>
+        None = 0,
+
+        /// <summary>
+        /// Tap gesture.
+        /// </summary>
+        Tap = 1,
+
+        /// <summary>
+        /// Double tap gesture.
+        /// </summary>
+        DoubleTap = 2,
+
+        /// <summary>
+        /// Swipe gesture.
+        /// </summary>
+        Swipe = 4,
+
+        /// <summary>
+        /// Long hold gesture.
+        /// </summary>
+        Hold = 8,
+
+        /// <summary>
+        /// Zoom gesture.
+        /// </summary>
+        Zoom = 16,
+
+        /// <summary>
+        /// Rotate gesture.
+        /// </summary>
+        Rotate = 32,
+
+        /// <summary>
+        /// All gestures.
+        /// </summary>
+        All = Tap | DoubleTap | Swipe | Hold | Zoom | Rotate
+    }
+}
diff --git a/src/Input/Silk.NET.Input.Common/Enums/MultiGestureHandling.cs b/src/Input/Silk.NET.Input.Common/Enums/MultiGestureHandling.cs
new file mode 100644
index 0000000000..420266ea52
--- /dev/null
+++ b/src/Input/Silk.NET.Input.Common/Enums/MultiGestureHandling.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Silk.NET.Input
+{
+    /// <summary>
+    /// Controls the behavior of how multiple two-finger gestures are handled.
+    /// </summary>
+    public enum MultiGestureHandling
+    {
+        /// <summary>
+        /// Only recognize the zoom gesture if both the zoom and the rotate gesture are performed.
+        /// </summary>
+        PrioritizeZoomGesture,
+
+        /// <summary>
+        /// Only recognize the rotate gesture if both the zoom and the rotate gesture are performed.
+        /// </summary>
+        PrioritizeRotateGesture,
+
+        /// <summary>
+        /// Recognize both gestures if they are performed (zoom and rotate).
+        /// </summary>
+        RecognizeBothGestures
+    }
+}
diff --git a/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs b/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
index 9584d3b077..e0f4f878dd 100644
--- a/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
+++ b/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
@@ -2,25 +2,246 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
+using System.ComponentModel;
+using System.Numerics;
 
 namespace Silk.NET.Input
 {
     /// <summary>
-    /// Touch gesture recognizer.
+    /// Touch gesture tracking.
     /// </summary>
     public sealed class TouchGestureRecognizer : IDisposable
     {
         private readonly ITouchDevice _device;
+        private long? _firstFingerIndex;
+        private DateTime? _firstFingerLastMoveTime;
+        private long? _secondFingerIndex;
+        private DateTime? _firstTapTime;
+        private Vector2? _firstTapPosition;
+        private Vector2? _firstTapNormalizedPosition;
+        private Vector2 _initialFingerDistance = Vector2.Zero;
+        private float _initialFingerAngle = 0.0f;
+        private bool _gestureHandled = false;
 
         internal TouchGestureRecognizer(ITouchDevice device)
         {
             _device = device;
+            device.FingerDown += Device_FingerDown;
+            device.FingerUp += Device_FingerUp;
+            device.FingerMove += Device_FingerMove;
+        }
+
+        private void Device_FingerDown(ITouchDevice touchDevice, TouchFinger finger)
+        {
+            if (_firstFingerIndex is null)
+            {
+                _gestureHandled = false;
+                _firstFingerIndex = finger.Index;
+                _firstFingerLastMoveTime = DateTime.Now;
+            }
+            else if (_secondFingerIndex is null)
+            {
+                _secondFingerIndex = finger.Index;
+                var firstFinger = _device.Fingers[_firstFingerIndex.Value];
+
+                _initialFingerDistance = finger.NormalizedPosition - firstFinger.NormalizedPosition;
+                _initialFingerAngle = (float)(Math.Atan2(finger.NormalizedPosition.Y - firstFinger.NormalizedPosition.Y,
+                    finger.NormalizedPosition.X - firstFinger.NormalizedPosition.X) * 180.0 / Math.PI);
+            }
+        }
+
+        private void Device_FingerUp(ITouchDevice touchDevice, TouchFinger finger)
+        {
+            if (_gestureHandled)
+            {
+                _firstFingerIndex = null;
+                _secondFingerIndex = null;
+                return;
+            }
+
+            if (finger.Index == _firstFingerIndex)
+            {
+                bool secondTap = _firstTapTime != null;
+                // Double tap is considered if it is tracked, this is the second tap and it was in time.
+                bool doubleTap = secondTap && TrackedGestures.HasFlag(Gesture.DoubleTap) &&
+                    (DateTime.Now - _firstTapTime!.Value).TotalMilliseconds <= DoubleTapTime;
+
+                if (doubleTap && _firstTapNormalizedPosition != null &&
+                    (Math.Abs(finger.NormalizedPosition.X - _firstTapNormalizedPosition.Value.X) > DoubleTapRange ||
+                     Math.Abs(finger.NormalizedPosition.Y - _firstTapNormalizedPosition.Value.Y) > DoubleTapRange))
+                {
+                    // Second tap was out of range
+                    doubleTap = false;
+                }
+
+                switch (DoubleTapBehavior)
+                {
+                    case DoubleTapBehavior.EmitFirstSingleTap:
+                        if (doubleTap)
+                        {
+                            DoubleTap?.Invoke(finger.Position);
+                        }
+                        else if (TrackedGestures.HasFlag(Gesture.Tap))
+                        {
+                            Tap?.Invoke(finger.Position);
+                        }
+                        break;
+                    case DoubleTapBehavior.EmitBothSingleTaps:
+                        if (TrackedGestures.HasFlag(Gesture.Tap))
+                        {
+                            Tap?.Invoke(finger.Position);
+                        }
+                        if (doubleTap)
+                        {
+                            DoubleTap?.Invoke(finger.Position);
+                        }
+                        break;
+                    case DoubleTapBehavior.WaitForDoubleTapTimeElapse:
+                        if (doubleTap)
+                        {
+                            DoubleTap?.Invoke(finger.Position);
+                        }
+                        else if (secondTap && TrackedGestures.HasFlag(Gesture.Tap))
+                        {
+                            Tap?.Invoke(_firstTapPosition ?? finger.Position);
+                            Tap?.Invoke(finger.Position);
+                        }
+                        break;
+                }
+
+                if (secondTap && (TrackedGestures & (Gesture.Tap | Gesture.DoubleTap)) != 0)
+                    _gestureHandled = true;
+
+                _firstTapTime = doubleTap ? null : DateTime.Now;
+                _firstTapPosition = doubleTap ? null : finger.Position;
+                _firstTapNormalizedPosition = doubleTap ? null : finger.NormalizedPosition;
+                _firstFingerIndex = null;
+                _secondFingerIndex = null;
+                _initialFingerDistance = Vector2.Zero;
+                _initialFingerAngle = 0.0f;
+            }
+            else if (finger.Index == _secondFingerIndex)
+            {
+                _secondFingerIndex = null;
+                _initialFingerDistance = Vector2.Zero;
+                _initialFingerAngle = 0.0f;
+                _gestureHandled = true;
+            }
+        }
+
+        private void Device_FingerMove(ITouchDevice touchDevice, TouchFinger finger, Vector2 delta)
+        {
+            if (finger.Index == _firstFingerIndex)
+            {
+                _firstFingerLastMoveTime = DateTime.Now;
+
+                if (!_gestureHandled &&
+                    TrackedGestures.HasFlag(Gesture.Swipe) &&
+                    _secondFingerIndex is null &&
+                    ((Math.Abs(finger.NormalizedSpeed.X) >= SwipeMinSpeed &&
+                    Math.Abs(finger.NormalizedSpeed.X) <= SwipeMaxSpeed) ||
+                    (Math.Abs(finger.NormalizedSpeed.Y) >= SwipeMinSpeed &&
+                    Math.Abs(finger.NormalizedSpeed.Y) <= SwipeMaxSpeed)))
+                {
+                    _gestureHandled = true;
+                    _firstFingerIndex = null;
+                    Swipe?.Invoke(finger.NormalizedSpeed);
+                    return;
+                }
+
+                if (_secondFingerIndex != null)
+                {
+                    CheckTwoFingerGestures();
+                }
+            }
+            else
+            {
+                if (_secondFingerIndex is null)
+                {
+                    _secondFingerIndex = finger.Index;
+                    var firstFinger = _device.Fingers[_firstFingerIndex.Value];
+
+                    _initialFingerDistance = finger.NormalizedPosition - firstFinger.NormalizedPosition;
+                    _initialFingerAngle = (float) (Math.Atan2(finger.NormalizedPosition.Y - firstFinger.NormalizedPosition.Y,
+                        finger.NormalizedPosition.X - firstFinger.NormalizedPosition.X) * 180.0 / Math.PI);
+                }
+
+                CheckTwoFingerGestures();
+            }
+        }
+
+        private void CheckTwoFingerGestures()
+        {
+            if (Zoom is null && Rotate is null)
+                return; // Don't bother
+
+            TouchFinger? firstFinger = _firstFingerIndex != null && _device.Fingers.TryGetValue(_firstFingerIndex.Value, out var finger) ? finger : null;
+            TouchFinger? secondFinger = _secondFingerIndex != null && _device.Fingers.TryGetValue(_secondFingerIndex.Value, out finger) ? finger : null;
+
+            if (firstFinger != null && secondFinger != null)
+            {
+                var multiGestureHandling = MultiGestureHandling;
+                if (Rotate is null)
+                    multiGestureHandling = MultiGestureHandling.PrioritizeZoomGesture;
+                else if (Zoom is null)
+                    multiGestureHandling = MultiGestureHandling.PrioritizeRotateGesture;
+                Action? zoomInvoker = null;
+
+                if (TrackedGestures.HasFlag(Gesture.Zoom))
+                {
+                    var fingerDistance = secondFinger.Value.NormalizedPosition - firstFinger.Value.NormalizedPosition;
+                    var distance = fingerDistance - _initialFingerDistance;
+
+                    if (Math.Abs(distance.X) >= ZoomDistanceThreshold || Math.Abs(distance.Y) >= ZoomDistanceThreshold)
+                    {
+                        zoomInvoker = () => Zoom?.Invoke(firstFinger.Value.Position, distance);
+
+                        if (multiGestureHandling == MultiGestureHandling.PrioritizeZoomGesture)
+                        {
+                            _gestureHandled = true;
+                            zoomInvoker();
+                            return;
+                        }
+                    }
+                }
+                if (TrackedGestures.HasFlag(Gesture.Rotate))
+                {
+                    var firstFingerPosition = firstFinger.Value.NormalizedPosition;
+                    var secondFingerPosition = secondFinger.Value.NormalizedPosition;
+                    var fingerAngle = (float)(Math.Atan2(secondFingerPosition.Y - firstFingerPosition.Y,
+                            secondFingerPosition.X - firstFingerPosition.X) * 180.0 / Math.PI);
+                    var angle = CalculateAngleDifference(fingerAngle, _initialFingerAngle);
+
+                    if (Math.Abs(angle) >= RotateAngleThreshold)
+                    {
+                        _gestureHandled = true;
+
+                        if (zoomInvoker != null && multiGestureHandling == MultiGestureHandling.RecognizeBothGestures)
+                            zoomInvoker();
+
+                        Rotate?.Invoke(firstFinger.Value.Position, angle);
+                    }
+                }
+            }
+        }
+
+        private static float CalculateAngleDifference(float angle1, float angle2)
+        {
+            // Ensure angles are in the range [0, 360)
+            angle1 = (angle1 + 360.0f) % 360.0f;
+            angle2 = (angle2 + 360.0f) % 360.0f;
+
+            // Calculate the signed angle difference
+            // This is always in the range (-360, 360)
+            return angle2 - angle1;
         }
 
         /// <inheritdoc />
         public void Dispose()
         {
-
+            _device.FingerDown -= Device_FingerDown;
+            _device.FingerUp -= Device_FingerUp;
+            _device.FingerMove -= Device_FingerMove;
         }
 
         /// <summary>
@@ -28,7 +249,123 @@ public void Dispose()
         /// </summary>
         public void Update()
         {
-
+            if (TrackedGestures.HasFlag(Gesture.Hold) &&
+                _firstFingerIndex != null && _firstFingerLastMoveTime != null && _secondFingerIndex is null &&
+                (DateTime.Now - _firstFingerLastMoveTime.Value).TotalMilliseconds >= HoldTime &&
+                _device.Fingers.TryGetValue(_firstFingerIndex.Value, out var finger))
+            {
+                _gestureHandled = true;
+                Hold?.Invoke(finger.Position);
+                _firstFingerIndex = null;
+            }
         }
+
+        /// <summary>
+        /// The tracked gestures.
+        /// </summary>
+        [DefaultValue(Gesture.All)]
+        public Gesture TrackedGestures { get; set; } = Gesture.All;
+
+        /// <summary>
+        /// THe behavior to handle multiple two-finger gestures.
+        /// </summary>
+        [DefaultValue(MultiGestureHandling.RecognizeBothGestures)]
+        public MultiGestureHandling MultiGestureHandling { get; set; } = MultiGestureHandling.RecognizeBothGestures;
+
+        /// <summary>
+        /// The behavior for tracking double taps.
+        /// </summary>
+        [DefaultValue(DoubleTapBehavior.EmitFirstSingleTap)]
+        public DoubleTapBehavior DoubleTapBehavior { get; set; } = DoubleTapBehavior.EmitFirstSingleTap;
+
+        /// <summary>
+        /// The maximum time in milliseconds between two
+        /// consecutive taps to count as a double tap.
+        /// </summary>
+        [DefaultValue(500)]
+        public int DoubleTapTime { get; set; } = 500;
+
+        /// <summary>
+        /// The maximum distance in normalized distance (0..1) between two
+        /// consecutive taps to count as a double tap.
+        /// </summary>
+        [DefaultValue(8)]
+        public float DoubleTapRange { get; set; } = 0.05f;
+
+        /// <summary>
+        /// The minimum finger speed in normalized distance (0..1) per second to count as a swipe gesture.
+        /// </summary>
+        public float SwipeMinSpeed { get; set; } = 0.15f; // TODO
+
+        /// <summary>
+        /// The maximum finger speed in normalized distance (0..1) per second to count as a swipe gesture.
+        /// </summary>
+        public float SwipeMaxSpeed { get; set; } = 1000.0f; // TODO
+
+        /// <summary>
+        /// The minimum time in milliseconds a finger must be pressed on the surface to count as a hold gesture.
+        /// </summary>
+        [DefaultValue(1000)]
+        public int HoldTime { get; set; } = 1000;
+
+        /// <summary>
+        /// Distance threshold as a normalized value (0..1) for zoom gesture tracking.
+        /// </summary>
+        public float ZoomDistanceThreshold { get; set; } = 0.15f;
+
+        /// <summary>
+        /// Angle threshold in degrees for rotate gesture tracking.
+        /// </summary>
+        public int RotateAngleThreshold { get; set; } = 12; // TODO
+
+        /// <summary>
+        /// Tap gesture.
+        /// </summary>
+        /// <remarks>
+        /// The event argument gives the finger position of the tap.
+        /// </remarks>
+        public event Action<Vector2>? Tap;
+
+        /// <summary>
+        /// Double tap gesture.
+        /// </summary>
+        /// <remarks>
+        /// The event argument gives the finger position of the second tap.
+        /// </remarks>
+        public event Action<Vector2>? DoubleTap;
+
+        /// <summary>
+        /// Swipe gesture.
+        /// </summary>
+        /// <remarks>
+        /// The event argument gives the swipe direction as a normalized 2D vector.
+        /// </remarks>
+        public event Action<Vector2>? Swipe;
+
+        /// <summary>
+        /// Long hold gesture.
+        /// </summary>
+        /// <remarks>
+        /// The event argument gives the finger position at the end of the hold.
+        /// </remarks>
+        public event Action<Vector2>? Hold;
+
+        /// <summary>
+        /// Zoom gesture.
+        /// </summary>
+        /// <remarks>
+        /// The first event argument gives the first finger position and the second
+        /// event argument the delta of the distance of the two fingers in relation to the last zoom event.
+        /// </remarks>
+        public event Action<Vector2, Vector2>? Zoom;
+
+        /// <summary>
+        /// Rotate gesture.
+        /// </summary>
+        /// <remarks>
+        /// The first event argument gives the first finger position and the second
+        /// event argument gives the angle delta in degress in relation to the last rotate event.
+        /// </remarks>
+        public event Action<Vector2, float>? Rotate;
     }
 }

From 0d7ffef4fd30cb35703a29e2ffa135eb3571293e Mon Sep 17 00:00:00 2001
From: Pyrdacor <trobt@web.de>
Date: Sun, 14 Jan 2024 22:40:38 +0100
Subject: [PATCH 07/21] Some gesture recognizer improvements/adjustments

---
 .../TouchGestureRecognizer.cs                    | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs b/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
index e0f4f878dd..101fda0c54 100644
--- a/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
+++ b/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
@@ -63,7 +63,7 @@ private void Device_FingerUp(ITouchDevice touchDevice, TouchFinger finger)
             {
                 bool secondTap = _firstTapTime != null;
                 // Double tap is considered if it is tracked, this is the second tap and it was in time.
-                bool doubleTap = secondTap && TrackedGestures.HasFlag(Gesture.DoubleTap) &&
+                bool doubleTap = secondTap && DoubleTap != null && TrackedGestures.HasFlag(Gesture.DoubleTap) &&
                     (DateTime.Now - _firstTapTime!.Value).TotalMilliseconds <= DoubleTapTime;
 
                 if (doubleTap && _firstTapNormalizedPosition != null &&
@@ -156,7 +156,7 @@ _secondFingerIndex is null &&
             }
             else
             {
-                if (_secondFingerIndex is null)
+                if (_firstFingerIndex != null && _secondFingerIndex is null)
                 {
                     _secondFingerIndex = finger.Index;
                     var firstFinger = _device.Fingers[_firstFingerIndex.Value];
@@ -289,18 +289,20 @@ public void Update()
         /// The maximum distance in normalized distance (0..1) between two
         /// consecutive taps to count as a double tap.
         /// </summary>
-        [DefaultValue(8)]
+        [DefaultValue(0.05f)]
         public float DoubleTapRange { get; set; } = 0.05f;
 
         /// <summary>
         /// The minimum finger speed in normalized distance (0..1) per second to count as a swipe gesture.
         /// </summary>
-        public float SwipeMinSpeed { get; set; } = 0.15f; // TODO
+        [DefaultValue(0.15f)]
+        public float SwipeMinSpeed { get; set; } = 0.15f;
 
         /// <summary>
         /// The maximum finger speed in normalized distance (0..1) per second to count as a swipe gesture.
         /// </summary>
-        public float SwipeMaxSpeed { get; set; } = 1000.0f; // TODO
+        [DefaultValue(1000.0f)]
+        public float SwipeMaxSpeed { get; set; } = 1000.0f;
 
         /// <summary>
         /// The minimum time in milliseconds a finger must be pressed on the surface to count as a hold gesture.
@@ -311,12 +313,14 @@ public void Update()
         /// <summary>
         /// Distance threshold as a normalized value (0..1) for zoom gesture tracking.
         /// </summary>
+        [DefaultValue(0.15f)]
         public float ZoomDistanceThreshold { get; set; } = 0.15f;
 
         /// <summary>
         /// Angle threshold in degrees for rotate gesture tracking.
         /// </summary>
-        public int RotateAngleThreshold { get; set; } = 12; // TODO
+        [DefaultValue(9.0f)]
+        public float RotateAngleThreshold { get; set; } = 9.0f;
 
         /// <summary>
         /// Tap gesture.

From ce5fcbbbfdf2a89879c28c13a162f704d0560cf4 Mon Sep 17 00:00:00 2001
From: Pyrdacor <trobt@web.de>
Date: Sun, 14 Jan 2024 23:46:53 +0100
Subject: [PATCH 08/21] Gesture recognizer adjustments

---
 .../TouchGestureRecognizer.cs                 | 34 ++++++++++++-------
 1 file changed, 21 insertions(+), 13 deletions(-)

diff --git a/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs b/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
index 101fda0c54..3e945f9dd9 100644
--- a/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
+++ b/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
@@ -20,6 +20,7 @@ public sealed class TouchGestureRecognizer : IDisposable
         private Vector2? _firstTapPosition;
         private Vector2? _firstTapNormalizedPosition;
         private Vector2 _initialFingerDistance = Vector2.Zero;
+        private Vector2 _initialNormalizedFingerDistance = Vector2.Zero;
         private float _initialFingerAngle = 0.0f;
         private bool _gestureHandled = false;
 
@@ -44,7 +45,8 @@ private void Device_FingerDown(ITouchDevice touchDevice, TouchFinger finger)
                 _secondFingerIndex = finger.Index;
                 var firstFinger = _device.Fingers[_firstFingerIndex.Value];
 
-                _initialFingerDistance = finger.NormalizedPosition - firstFinger.NormalizedPosition;
+                _initialFingerDistance = finger.Position - firstFinger.Position;
+                _initialNormalizedFingerDistance = finger.NormalizedPosition - firstFinger.NormalizedPosition;
                 _initialFingerAngle = (float)(Math.Atan2(finger.NormalizedPosition.Y - firstFinger.NormalizedPosition.Y,
                     finger.NormalizedPosition.X - firstFinger.NormalizedPosition.X) * 180.0 / Math.PI);
             }
@@ -118,12 +120,14 @@ private void Device_FingerUp(ITouchDevice touchDevice, TouchFinger finger)
                 _firstFingerIndex = null;
                 _secondFingerIndex = null;
                 _initialFingerDistance = Vector2.Zero;
+                _initialNormalizedFingerDistance = Vector2.Zero;
                 _initialFingerAngle = 0.0f;
             }
             else if (finger.Index == _secondFingerIndex)
             {
                 _secondFingerIndex = null;
                 _initialFingerDistance = Vector2.Zero;
+                _initialNormalizedFingerDistance = Vector2.Zero;
                 _initialFingerAngle = 0.0f;
                 _gestureHandled = true;
             }
@@ -161,7 +165,8 @@ _secondFingerIndex is null &&
                     _secondFingerIndex = finger.Index;
                     var firstFinger = _device.Fingers[_firstFingerIndex.Value];
 
-                    _initialFingerDistance = finger.NormalizedPosition - firstFinger.NormalizedPosition;
+                    _initialFingerDistance = finger.Position - firstFinger.Position;
+                    _initialNormalizedFingerDistance = finger.NormalizedPosition - firstFinger.NormalizedPosition;
                     _initialFingerAngle = (float) (Math.Atan2(finger.NormalizedPosition.Y - firstFinger.NormalizedPosition.Y,
                         finger.NormalizedPosition.X - firstFinger.NormalizedPosition.X) * 180.0 / Math.PI);
                 }
@@ -189,12 +194,15 @@ private void CheckTwoFingerGestures()
 
                 if (TrackedGestures.HasFlag(Gesture.Zoom))
                 {
-                    var fingerDistance = secondFinger.Value.NormalizedPosition - firstFinger.Value.NormalizedPosition;
-                    var distance = fingerDistance - _initialFingerDistance;
+                    var normalizedFingerDistance = secondFinger.Value.NormalizedPosition - firstFinger.Value.NormalizedPosition;
+                    var normalizedDistance = normalizedFingerDistance - _initialNormalizedFingerDistance;
 
-                    if (Math.Abs(distance.X) >= ZoomDistanceThreshold || Math.Abs(distance.Y) >= ZoomDistanceThreshold)
+                    if (Math.Abs(normalizedDistance.X) >= ZoomDistanceThreshold || Math.Abs(normalizedDistance.Y) >= ZoomDistanceThreshold)
                     {
-                        zoomInvoker = () => Zoom?.Invoke(firstFinger.Value.Position, distance);
+                        var firstFingerPosition = firstFinger.Value.Position;
+                        var fingerDistance = secondFinger.Value.Position - firstFingerPosition - _initialFingerDistance;
+
+                        zoomInvoker = () => Zoom?.Invoke(firstFingerPosition, fingerDistance);
 
                         if (multiGestureHandling == MultiGestureHandling.PrioritizeZoomGesture)
                         {
@@ -326,7 +334,7 @@ public void Update()
         /// Tap gesture.
         /// </summary>
         /// <remarks>
-        /// The event argument gives the finger position of the tap.
+        /// The event argument gives the finger position of the tap in pixel coordinates.
         /// </remarks>
         public event Action<Vector2>? Tap;
 
@@ -334,7 +342,7 @@ public void Update()
         /// Double tap gesture.
         /// </summary>
         /// <remarks>
-        /// The event argument gives the finger position of the second tap.
+        /// The event argument gives the finger position of the second tap in pixel coordinates.
         /// </remarks>
         public event Action<Vector2>? DoubleTap;
 
@@ -350,7 +358,7 @@ public void Update()
         /// Long hold gesture.
         /// </summary>
         /// <remarks>
-        /// The event argument gives the finger position at the end of the hold.
+        /// The event argument gives the finger position at the end of the hold in pixel coordinates.
         /// </remarks>
         public event Action<Vector2>? Hold;
 
@@ -358,8 +366,8 @@ public void Update()
         /// Zoom gesture.
         /// </summary>
         /// <remarks>
-        /// The first event argument gives the first finger position and the second
-        /// event argument the delta of the distance of the two fingers in relation to the last zoom event.
+        /// The first event argument gives the first finger position in pixel coordinates and the second
+        /// event argument the total distance change in pixels of the two fingers in relation to the initial finger distance.
         /// </remarks>
         public event Action<Vector2, Vector2>? Zoom;
 
@@ -367,8 +375,8 @@ public void Update()
         /// Rotate gesture.
         /// </summary>
         /// <remarks>
-        /// The first event argument gives the first finger position and the second
-        /// event argument gives the angle delta in degress in relation to the last rotate event.
+        /// The first event argument gives the first finger position in pixel coordinates and the second
+        /// event argument gives the total angle delta in degress in relation to the initial finger angle.
         /// </remarks>
         public event Action<Vector2, float>? Rotate;
     }

From 0a662c5241f9ff1777b6ecbc01b4e3895286e133 Mon Sep 17 00:00:00 2001
From: Pyrdacor <trobt@web.de>
Date: Mon, 15 Jan 2024 00:05:34 +0100
Subject: [PATCH 09/21] Some more gesture recognizer adjustments

---
 src/Input/Silk.NET.Input.Common/Enums/Gesture.cs |  2 +-
 .../TouchGestureRecognizer.cs                    | 16 +++++++++-------
 2 files changed, 10 insertions(+), 8 deletions(-)

diff --git a/src/Input/Silk.NET.Input.Common/Enums/Gesture.cs b/src/Input/Silk.NET.Input.Common/Enums/Gesture.cs
index aaadf937ac..df887190bb 100644
--- a/src/Input/Silk.NET.Input.Common/Enums/Gesture.cs
+++ b/src/Input/Silk.NET.Input.Common/Enums/Gesture.cs
@@ -6,7 +6,7 @@
 namespace Silk.NET.Input
 {
     /// <summary>
-    /// Controls the behavior of the double tap gesture tracking.
+    /// Recognizable touch gesture.
     /// </summary>
     [Flags]
     public enum Gesture
diff --git a/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs b/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
index 3e945f9dd9..35df467a7f 100644
--- a/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
+++ b/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
@@ -140,6 +140,7 @@ private void Device_FingerMove(ITouchDevice touchDevice, TouchFinger finger, Vec
                 _firstFingerLastMoveTime = DateTime.Now;
 
                 if (!_gestureHandled &&
+                    Swipe != null &&
                     TrackedGestures.HasFlag(Gesture.Swipe) &&
                     _secondFingerIndex is null &&
                     ((Math.Abs(finger.NormalizedSpeed.X) >= SwipeMinSpeed &&
@@ -149,7 +150,7 @@ _secondFingerIndex is null &&
                 {
                     _gestureHandled = true;
                     _firstFingerIndex = null;
-                    Swipe?.Invoke(finger.NormalizedSpeed);
+                    Swipe(finger.NormalizedSpeed);
                     return;
                 }
 
@@ -192,7 +193,7 @@ private void CheckTwoFingerGestures()
                     multiGestureHandling = MultiGestureHandling.PrioritizeRotateGesture;
                 Action? zoomInvoker = null;
 
-                if (TrackedGestures.HasFlag(Gesture.Zoom))
+                if (Zoom != null && TrackedGestures.HasFlag(Gesture.Zoom))
                 {
                     var normalizedFingerDistance = secondFinger.Value.NormalizedPosition - firstFinger.Value.NormalizedPosition;
                     var normalizedDistance = normalizedFingerDistance - _initialNormalizedFingerDistance;
@@ -202,7 +203,7 @@ private void CheckTwoFingerGestures()
                         var firstFingerPosition = firstFinger.Value.Position;
                         var fingerDistance = secondFinger.Value.Position - firstFingerPosition - _initialFingerDistance;
 
-                        zoomInvoker = () => Zoom?.Invoke(firstFingerPosition, fingerDistance);
+                        zoomInvoker = () => Zoom(firstFingerPosition, fingerDistance);
 
                         if (multiGestureHandling == MultiGestureHandling.PrioritizeZoomGesture)
                         {
@@ -212,7 +213,7 @@ private void CheckTwoFingerGestures()
                         }
                     }
                 }
-                if (TrackedGestures.HasFlag(Gesture.Rotate))
+                if (Rotate != null && TrackedGestures.HasFlag(Gesture.Rotate))
                 {
                     var firstFingerPosition = firstFinger.Value.NormalizedPosition;
                     var secondFingerPosition = secondFinger.Value.NormalizedPosition;
@@ -227,7 +228,7 @@ private void CheckTwoFingerGestures()
                         if (zoomInvoker != null && multiGestureHandling == MultiGestureHandling.RecognizeBothGestures)
                             zoomInvoker();
 
-                        Rotate?.Invoke(firstFinger.Value.Position, angle);
+                        Rotate(firstFinger.Value.Position, angle);
                     }
                 }
             }
@@ -257,13 +258,14 @@ public void Dispose()
         /// </summary>
         public void Update()
         {
-            if (TrackedGestures.HasFlag(Gesture.Hold) &&
+            if (Hold != null &&
+                TrackedGestures.HasFlag(Gesture.Hold) &&
                 _firstFingerIndex != null && _firstFingerLastMoveTime != null && _secondFingerIndex is null &&
                 (DateTime.Now - _firstFingerLastMoveTime.Value).TotalMilliseconds >= HoldTime &&
                 _device.Fingers.TryGetValue(_firstFingerIndex.Value, out var finger))
             {
                 _gestureHandled = true;
-                Hold?.Invoke(finger.Position);
+                Hold(finger.Position);
                 _firstFingerIndex = null;
             }
         }

From 1eeef2413315db2a4325d8538bafbefd2e69967c Mon Sep 17 00:00:00 2001
From: Pyrdacor <trobt@web.de>
Date: Mon, 15 Jan 2024 00:05:52 +0100
Subject: [PATCH 10/21] Extended AndroidInputDemo for touch

---
 .../AndroidInputDemo/AndroidManifest.xml      |  11 +-
 .../AndroidInputDemo/MainActivity.cs          | 135 +++++++++++++++++-
 2 files changed, 139 insertions(+), 7 deletions(-)

diff --git a/examples/CSharp/OpenGL Demos/AndroidInputDemo/AndroidManifest.xml b/examples/CSharp/OpenGL Demos/AndroidInputDemo/AndroidManifest.xml
index c4f3a7d809..dba50ab426 100644
--- a/examples/CSharp/OpenGL Demos/AndroidInputDemo/AndroidManifest.xml	
+++ b/examples/CSharp/OpenGL Demos/AndroidInputDemo/AndroidManifest.xml	
@@ -1,8 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android" 
-          android:versionCode="1" 
-          android:versionName="1.0" 
-          package="com.companyname.AndroidInputDemo">
-  <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true">
-  </application>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.AndroidInputDemo">
+	<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
+	<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
+	<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true"></application>
+	<uses-sdk />
 </manifest>
\ No newline at end of file
diff --git a/examples/CSharp/OpenGL Demos/AndroidInputDemo/MainActivity.cs b/examples/CSharp/OpenGL Demos/AndroidInputDemo/MainActivity.cs
index 145cd47cb6..1ed19e4ead 100644
--- a/examples/CSharp/OpenGL Demos/AndroidInputDemo/MainActivity.cs	
+++ b/examples/CSharp/OpenGL Demos/AndroidInputDemo/MainActivity.cs	
@@ -34,6 +34,10 @@ public class MainActivity : SilkActivity
         private static int counter = 0;
 
         private static IInputContext input;
+        private static ITouchDevice currentTouchDevice;
+        private static Gesture trackedGestures = Gesture.All;
+        private static DoubleTapBehavior doubleTapBehavior = DoubleTapBehavior.EmitFirstSingleTap;
+        private static MultiGestureHandling multiGestureHandling = MultiGestureHandling.RecognizeBothGestures;
 
         /// <summary>
         /// This is where the application starts.
@@ -69,6 +73,15 @@ private unsafe static void OnLoad()
 
             input.Keyboards[0].KeyChar += KeyChar;
 
+            if (input.PrimaryTouchDevice != null)
+                SetupTouchDevice(input.PrimaryTouchDevice);
+
+            input.ConnectionChanged += (device, connected) =>
+            {
+                if (connected && device is ITouchDevice touchDevice)
+                    SetupTouchDevice(touchDevice);
+            };
+
             Vbo = new BufferObject<float>(Gl, Vertices, BufferTargetARB.ArrayBuffer);
             Vao = new VertexArrayObject<float, uint>(Gl, Vbo, null);
 
@@ -82,12 +95,96 @@ private unsafe static void OnLoad()
                 1.0f, 2.0f);
         }
 
+        private static void GestureRecognizer_Rotate(Vector2 position, float angle)
+        {
+            DebugLog($"Rotate gesture at {position} with rotation {angle} degree");
+        }
+
+        private static void GestureRecognizer_Zoom(Vector2 position, Vector2 zoom)
+        {
+            DebugLog($"Zoom gesture at {position} with zoom {zoom}");
+        }
+
+        private static void GestureRecognizer_Hold(Vector2 position)
+        {
+            Gl.ClearColor(1.0f, 0.0f, 0.0f, 1.0f);
+            DebugLog($"Hold gesture at {position}");
+        }
+
+        private static void GestureRecognizer_Swipe(Vector2 direction)
+        {
+            DebugLog($"Swipe gesture with direction {direction}");
+        }
+
+        private static void GestureRecognizer_DoubleTap(Vector2 position)
+        {
+            Gl.ClearColor(0.0f, 0.0f, 1.0f, 1.0f);
+            DebugLog($"Double Tap gesture at {position}");
+        }
+
+        private static void GestureRecognizer_Tap(Vector2 position)
+        {
+            Gl.ClearColor(0.0f, 1.0f, 0.0f, 1.0f);
+            DebugLog($"Tap gesture at {position}");
+        }
+
         private static void KeyChar(IKeyboard arg1, char arg2)
         {
             if (arg2 == 'c')
                 Array.Clear(Vertices);
             if (arg2 == 'k')
                 input.Keyboards[0].EndInput();
+            if (arg2 == 't')
+                ToggleGesture(Gesture.Tap);
+            if (arg2 == 'd')
+                ToggleGesture(Gesture.DoubleTap);
+            if (arg2 == 's')
+                ToggleGesture(Gesture.Swipe);
+            if (arg2 == 'h')
+                ToggleGesture(Gesture.Hold);
+            if (arg2 == 'z')
+                ToggleGesture(Gesture.Zoom);
+            if (arg2 == 'r')
+                ToggleGesture(Gesture.Rotate);
+            if (arg2 == '0')
+                SetDoubleTapBehavior(DoubleTapBehavior.WaitForDoubleTapTimeElapse);
+            if (arg2 == '1')
+                SetDoubleTapBehavior(DoubleTapBehavior.EmitFirstSingleTap);
+            if (arg2 == '2')
+                SetDoubleTapBehavior(DoubleTapBehavior.EmitBothSingleTaps);
+            if (arg2 == '7')
+                SetMultiGestureHandling(MultiGestureHandling.RecognizeBothGestures);
+            if (arg2 == '8')
+                SetMultiGestureHandling(MultiGestureHandling.PrioritizeZoomGesture);
+            if (arg2 == '9')
+                SetMultiGestureHandling(MultiGestureHandling.PrioritizeRotateGesture);
+        }
+
+        private static void ToggleGesture(Gesture gesture)
+        {
+            if (trackedGestures.HasFlag(gesture))
+                trackedGestures &= ~gesture;
+            else
+                trackedGestures |= gesture;
+
+            if (currentTouchDevice?.GestureRecognizer != null)
+                currentTouchDevice.GestureRecognizer.TrackedGestures = trackedGestures;
+        }
+
+        private static void SetDoubleTapBehavior(DoubleTapBehavior doubleTapBehavior)
+        {
+            MainActivity.doubleTapBehavior = doubleTapBehavior;
+
+            if (currentTouchDevice?.GestureRecognizer != null)
+                currentTouchDevice.GestureRecognizer.DoubleTapBehavior = doubleTapBehavior;
+        }
+
+        private static void SetMultiGestureHandling(MultiGestureHandling multiGestureHandling)
+        {
+            MainActivity.multiGestureHandling = multiGestureHandling;
+
+            if (currentTouchDevice?.GestureRecognizer != null)
+                currentTouchDevice.GestureRecognizer.MultiGestureHandling = multiGestureHandling;
         }
 
         private static void MouseDown(IMouse arg1, MouseButton arg2)
@@ -131,5 +228,41 @@ private static void OnClose()
             Vao.Dispose();
             Shader.Dispose();
         }
+
+        private static void SetupTouchDevice(ITouchDevice touchDevice)
+        {
+            if (currentTouchDevice == touchDevice)
+                return;
+
+            TouchGestureRecognizer gestureRecognizer;
+
+            if (currentTouchDevice != null)
+            {
+                gestureRecognizer = currentTouchDevice.GestureRecognizer;
+                gestureRecognizer.Tap -= GestureRecognizer_Tap;
+                gestureRecognizer.DoubleTap -= GestureRecognizer_DoubleTap;
+                gestureRecognizer.Swipe -= GestureRecognizer_Swipe;
+                gestureRecognizer.Hold -= GestureRecognizer_Hold;
+                gestureRecognizer.Zoom -= GestureRecognizer_Zoom;
+                gestureRecognizer.Rotate -= GestureRecognizer_Rotate;
+            }
+
+            currentTouchDevice = touchDevice;
+            gestureRecognizer = currentTouchDevice.GestureRecognizer;
+            gestureRecognizer.TrackedGestures = trackedGestures;
+            gestureRecognizer.DoubleTapBehavior = doubleTapBehavior;
+            gestureRecognizer.MultiGestureHandling = multiGestureHandling;
+            gestureRecognizer.Tap += GestureRecognizer_Tap;
+            gestureRecognizer.DoubleTap += GestureRecognizer_DoubleTap;
+            gestureRecognizer.Swipe += GestureRecognizer_Swipe;
+            gestureRecognizer.Hold += GestureRecognizer_Hold;
+            gestureRecognizer.Zoom += GestureRecognizer_Zoom;
+            gestureRecognizer.Rotate += GestureRecognizer_Rotate;
+        }
+
+        private static void DebugLog(string message)
+        {
+            Android.Util.Log.Debug(nameof(AndroidInputDemo), message);
+        }
     }
-}
\ No newline at end of file
+}

From 79588da0dffcd9bef9b8ff0890ab6eaf0dc693d3 Mon Sep 17 00:00:00 2001
From: Pyrdacor <trobt@web.de>
Date: Mon, 15 Jan 2024 00:22:53 +0100
Subject: [PATCH 11/21] Improved touch device disposing

---
 src/Input/Silk.NET.Input.Common/Interfaces/ITouchDevice.cs | 2 +-
 src/Input/Silk.NET.Input.Sdl/SdlInputContext.cs            | 5 +++++
 src/Input/Silk.NET.Input.Sdl/SdlTouchDevice.cs             | 2 +-
 3 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/src/Input/Silk.NET.Input.Common/Interfaces/ITouchDevice.cs b/src/Input/Silk.NET.Input.Common/Interfaces/ITouchDevice.cs
index 481525ae2a..fac285b615 100644
--- a/src/Input/Silk.NET.Input.Common/Interfaces/ITouchDevice.cs
+++ b/src/Input/Silk.NET.Input.Common/Interfaces/ITouchDevice.cs
@@ -10,7 +10,7 @@ namespace Silk.NET.Input
     /// <summary>
     /// An interface representing a touch device.
     /// </summary>
-    public interface ITouchDevice : IInputDevice, IDisposable
+    public interface ITouchDevice : IInputDevice
     {
         /// <summary>
         /// The known fingers this touch device has tracked.
diff --git a/src/Input/Silk.NET.Input.Sdl/SdlInputContext.cs b/src/Input/Silk.NET.Input.Sdl/SdlInputContext.cs
index 9877595907..58c50eb270 100644
--- a/src/Input/Silk.NET.Input.Sdl/SdlInputContext.cs
+++ b/src/Input/Silk.NET.Input.Sdl/SdlInputContext.cs
@@ -342,6 +342,11 @@ public override void CoreDispose()
             {
                 joy.Dispose();
             }
+
+            foreach (var td in SdlTouchDevices.Values)
+            {
+                td.Dispose();
+            }
         }
 
         public void ChangeConnection(IInputDevice device, bool connected)
diff --git a/src/Input/Silk.NET.Input.Sdl/SdlTouchDevice.cs b/src/Input/Silk.NET.Input.Sdl/SdlTouchDevice.cs
index 0b91404f09..2ae3f1f898 100644
--- a/src/Input/Silk.NET.Input.Sdl/SdlTouchDevice.cs
+++ b/src/Input/Silk.NET.Input.Sdl/SdlTouchDevice.cs
@@ -5,7 +5,7 @@
 
 namespace Silk.NET.Input.Sdl
 {
-    internal class SdlTouchDevice : ITouchDevice, ISdlDevice
+    internal class SdlTouchDevice : ITouchDevice, ISdlDevice, IDisposable
     {
         private readonly SdlInputContext _ctx;
         private readonly Dictionary<long, TouchFinger> _fingers = new Dictionary<long, TouchFinger>();

From bffb083814a7ce43058c433610871714134c46b7 Mon Sep 17 00:00:00 2001
From: Pyrdacor <trobt@web.de>
Date: Mon, 15 Jan 2024 00:23:34 +0100
Subject: [PATCH 12/21] Updated public api

---
 .../netcoreapp3.1/PublicAPI.Unshipped.txt     | 64 +++++++++++++++++++
 1 file changed, 64 insertions(+)

diff --git a/src/Input/Silk.NET.Input.Common/PublicAPI/netcoreapp3.1/PublicAPI.Unshipped.txt b/src/Input/Silk.NET.Input.Common/PublicAPI/netcoreapp3.1/PublicAPI.Unshipped.txt
index 862d9e4729..0cae6b2980 100644
--- a/src/Input/Silk.NET.Input.Common/PublicAPI/netcoreapp3.1/PublicAPI.Unshipped.txt
+++ b/src/Input/Silk.NET.Input.Common/PublicAPI/netcoreapp3.1/PublicAPI.Unshipped.txt
@@ -1,4 +1,68 @@
 #nullable enable
+Silk.NET.Input.DoubleTapBehavior
+Silk.NET.Input.DoubleTapBehavior.EmitBothSingleTaps = 1 -> Silk.NET.Input.DoubleTapBehavior
+Silk.NET.Input.DoubleTapBehavior.EmitFirstSingleTap = 0 -> Silk.NET.Input.DoubleTapBehavior
+Silk.NET.Input.DoubleTapBehavior.WaitForDoubleTapTimeElapse = 2 -> Silk.NET.Input.DoubleTapBehavior
+Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.All = Silk.NET.Input.Gesture.Tap | Silk.NET.Input.Gesture.DoubleTap | Silk.NET.Input.Gesture.Swipe | Silk.NET.Input.Gesture.Hold | Silk.NET.Input.Gesture.Zoom | Silk.NET.Input.Gesture.Rotate -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.DoubleTap = 2 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.Hold = 8 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.None = 0 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.Rotate = 32 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.Swipe = 4 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.Tap = 1 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.Zoom = 16 -> Silk.NET.Input.Gesture
+Silk.NET.Input.IInputContext.PrimaryTouchDevice.get -> Silk.NET.Input.ITouchDevice?
+Silk.NET.Input.IInputContext.TouchDevices.get -> System.Collections.Generic.IReadOnlyList<Silk.NET.Input.ITouchDevice!>!
+Silk.NET.Input.ITouchDevice
+Silk.NET.Input.ITouchDevice.FingerDown -> System.Action<Silk.NET.Input.ITouchDevice!, Silk.NET.Input.TouchFinger>?
+Silk.NET.Input.ITouchDevice.FingerMove -> System.Action<Silk.NET.Input.ITouchDevice!, Silk.NET.Input.TouchFinger, System.Numerics.Vector2>?
+Silk.NET.Input.ITouchDevice.Fingers.get -> System.Collections.Generic.IReadOnlyDictionary<long, Silk.NET.Input.TouchFinger>!
+Silk.NET.Input.ITouchDevice.FingerUp -> System.Action<Silk.NET.Input.ITouchDevice!, Silk.NET.Input.TouchFinger>?
+Silk.NET.Input.ITouchDevice.GestureRecognizer.get -> Silk.NET.Input.TouchGestureRecognizer!
+Silk.NET.Input.ITouchDevice.IsFingerDown(long index) -> bool
+Silk.NET.Input.MultiGestureHandling
+Silk.NET.Input.MultiGestureHandling.PrioritizeRotateGesture = 1 -> Silk.NET.Input.MultiGestureHandling
+Silk.NET.Input.MultiGestureHandling.PrioritizeZoomGesture = 0 -> Silk.NET.Input.MultiGestureHandling
+Silk.NET.Input.MultiGestureHandling.RecognizeBothGestures = 2 -> Silk.NET.Input.MultiGestureHandling
+Silk.NET.Input.TouchFinger
+Silk.NET.Input.TouchFinger.Down.get -> bool
+Silk.NET.Input.TouchFinger.Index.get -> long
+Silk.NET.Input.TouchFinger.NormalizedPosition.get -> System.Numerics.Vector2
+Silk.NET.Input.TouchFinger.NormalizedSpeed.get -> System.Numerics.Vector2
+Silk.NET.Input.TouchFinger.Position.get -> System.Numerics.Vector2
+Silk.NET.Input.TouchFinger.Speed.get -> System.Numerics.Vector2
+Silk.NET.Input.TouchFinger.TouchFinger() -> void
+Silk.NET.Input.TouchFinger.TouchFinger(long index, System.Numerics.Vector2 position, System.Numerics.Vector2 normalizedPosition, System.Numerics.Vector2 speed, System.Numerics.Vector2 normalizedSpeed, bool down) -> void
+Silk.NET.Input.TouchGestureRecognizer
+Silk.NET.Input.TouchGestureRecognizer.Dispose() -> void
+Silk.NET.Input.TouchGestureRecognizer.DoubleTap -> System.Action<System.Numerics.Vector2>?
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapBehavior.get -> Silk.NET.Input.DoubleTapBehavior
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapBehavior.set -> void
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapRange.get -> float
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapRange.set -> void
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapTime.get -> int
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapTime.set -> void
+Silk.NET.Input.TouchGestureRecognizer.Hold -> System.Action<System.Numerics.Vector2>?
+Silk.NET.Input.TouchGestureRecognizer.HoldTime.get -> int
+Silk.NET.Input.TouchGestureRecognizer.HoldTime.set -> void
+Silk.NET.Input.TouchGestureRecognizer.MultiGestureHandling.get -> Silk.NET.Input.MultiGestureHandling
+Silk.NET.Input.TouchGestureRecognizer.MultiGestureHandling.set -> void
+Silk.NET.Input.TouchGestureRecognizer.Rotate -> System.Action<System.Numerics.Vector2, float>?
+Silk.NET.Input.TouchGestureRecognizer.RotateAngleThreshold.get -> float
+Silk.NET.Input.TouchGestureRecognizer.RotateAngleThreshold.set -> void
+Silk.NET.Input.TouchGestureRecognizer.Swipe -> System.Action<System.Numerics.Vector2>?
+Silk.NET.Input.TouchGestureRecognizer.SwipeMaxSpeed.get -> float
+Silk.NET.Input.TouchGestureRecognizer.SwipeMaxSpeed.set -> void
+Silk.NET.Input.TouchGestureRecognizer.SwipeMinSpeed.get -> float
+Silk.NET.Input.TouchGestureRecognizer.SwipeMinSpeed.set -> void
+Silk.NET.Input.TouchGestureRecognizer.Tap -> System.Action<System.Numerics.Vector2>?
+Silk.NET.Input.TouchGestureRecognizer.TrackedGestures.get -> Silk.NET.Input.Gesture
+Silk.NET.Input.TouchGestureRecognizer.TrackedGestures.set -> void
+Silk.NET.Input.TouchGestureRecognizer.Update() -> void
+Silk.NET.Input.TouchGestureRecognizer.Zoom -> System.Action<System.Numerics.Vector2, System.Numerics.Vector2>?
+Silk.NET.Input.TouchGestureRecognizer.ZoomDistanceThreshold.get -> float
+Silk.NET.Input.TouchGestureRecognizer.ZoomDistanceThreshold.set -> void
 static Silk.NET.Input.GamepadExtensions.LeftThumbstick(this Silk.NET.Input.IGamepad! gamepad) -> Silk.NET.Input.Thumbstick
 static Silk.NET.Input.GamepadExtensions.LeftThumbstickButton(this Silk.NET.Input.IGamepad! gamepad) -> Silk.NET.Input.Button
 static Silk.NET.Input.GamepadExtensions.RightThumbstick(this Silk.NET.Input.IGamepad! gamepad) -> Silk.NET.Input.Thumbstick

From 82a8e0dae01f8c65f1fb53f19a99ff041ec3bf87 Mon Sep 17 00:00:00 2001
From: Pyrdacor <trobt@web.de>
Date: Mon, 15 Jan 2024 00:26:20 +0100
Subject: [PATCH 13/21] Added value range info for normalized touch finger
 props

---
 src/Input/Silk.NET.Input.Common/Structs/TouchFinger.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Input/Silk.NET.Input.Common/Structs/TouchFinger.cs b/src/Input/Silk.NET.Input.Common/Structs/TouchFinger.cs
index 5499431dba..810b3c8e0b 100644
--- a/src/Input/Silk.NET.Input.Common/Structs/TouchFinger.cs
+++ b/src/Input/Silk.NET.Input.Common/Structs/TouchFinger.cs
@@ -21,7 +21,7 @@ public readonly struct TouchFinger
         public Vector2 Position { get; }
 
         /// <summary>
-        /// The last known normalized position of the finger.
+        /// The last known normalized position (0..1) of the finger.
         /// </summary>
         public Vector2 NormalizedPosition { get; }
 
@@ -31,7 +31,7 @@ public readonly struct TouchFinger
         public Vector2 Speed { get; }
 
         /// <summary>
-        /// The last known normalized speed of the finger.
+        /// The last known normalized speed (-1..1) of the finger.
         /// </summary>
         public Vector2 NormalizedSpeed { get; }
 

From 8570b0bb622f2a2d64da583c2ec0fa5d53d52c20 Mon Sep 17 00:00:00 2001
From: Pyrdacor <trobt@web.de>
Date: Mon, 15 Jan 2024 00:40:23 +0100
Subject: [PATCH 14/21] Fixed wrong finger down value

---
 src/Input/Silk.NET.Input.Sdl/SdlTouchDevice.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Input/Silk.NET.Input.Sdl/SdlTouchDevice.cs b/src/Input/Silk.NET.Input.Sdl/SdlTouchDevice.cs
index 2ae3f1f898..eb20c48f54 100644
--- a/src/Input/Silk.NET.Input.Sdl/SdlTouchDevice.cs
+++ b/src/Input/Silk.NET.Input.Sdl/SdlTouchDevice.cs
@@ -86,7 +86,7 @@ public unsafe void DoEvent(Event @event)
                     var speed = normalizedSpeed * windowSize;
                     var finger = new TouchFinger(@event.Tfinger.FingerId,
                         position, normalizedPosition, speed, normalizedSpeed,
-                        false);
+                        true);
                     FingerMove?.Invoke(this, finger, distance);
                     _fingers[finger.Index] = finger;
                     _fingerEventTimes[finger.Index] = DateTime.Now;

From dc017dd07643adb85baff19df063244bcc3edb64 Mon Sep 17 00:00:00 2001
From: Pyrdacor <trobt@web.de>
Date: Mon, 15 Jan 2024 00:42:46 +0100
Subject: [PATCH 15/21] Fixed finger move distance and added remark

---
 src/Input/Silk.NET.Input.Common/Interfaces/ITouchDevice.cs | 4 ++++
 src/Input/Silk.NET.Input.Sdl/SdlTouchDevice.cs             | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/Input/Silk.NET.Input.Common/Interfaces/ITouchDevice.cs b/src/Input/Silk.NET.Input.Common/Interfaces/ITouchDevice.cs
index fac285b615..2e75d8342b 100644
--- a/src/Input/Silk.NET.Input.Common/Interfaces/ITouchDevice.cs
+++ b/src/Input/Silk.NET.Input.Common/Interfaces/ITouchDevice.cs
@@ -43,6 +43,10 @@ public interface ITouchDevice : IInputDevice
         /// <summary>
         /// Called when the finger is moved while on the surface.
         /// </summary>
+        /// <remarks>
+        /// The last event argument gives the distance in pixels
+        /// the finger has moved since the last event.
+        /// </remarks>
         event Action<ITouchDevice, TouchFinger, Vector2>? FingerMove;
     }
 }
diff --git a/src/Input/Silk.NET.Input.Sdl/SdlTouchDevice.cs b/src/Input/Silk.NET.Input.Sdl/SdlTouchDevice.cs
index eb20c48f54..cf75594fc3 100644
--- a/src/Input/Silk.NET.Input.Sdl/SdlTouchDevice.cs
+++ b/src/Input/Silk.NET.Input.Sdl/SdlTouchDevice.cs
@@ -87,7 +87,7 @@ public unsafe void DoEvent(Event @event)
                     var finger = new TouchFinger(@event.Tfinger.FingerId,
                         position, normalizedPosition, speed, normalizedSpeed,
                         true);
-                    FingerMove?.Invoke(this, finger, distance);
+                    FingerMove?.Invoke(this, finger, distance * windowSize);
                     _fingers[finger.Index] = finger;
                     _fingerEventTimes[finger.Index] = DateTime.Now;
                     break;

From 6779df938ea80c436a11e4bd40c35e1d868f24a7 Mon Sep 17 00:00:00 2001
From: Pyrdacor <trobt@web.de>
Date: Mon, 15 Jan 2024 01:03:41 +0100
Subject: [PATCH 16/21] Added double tap elapse handling for 3rd double tap
 behavior

---
 .../TouchGestureRecognizer.cs                 | 24 ++++++++++++++++++-
 1 file changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs b/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
index 35df467a7f..7b423485ec 100644
--- a/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
+++ b/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
@@ -258,7 +258,29 @@ public void Dispose()
         /// </summary>
         public void Update()
         {
-            if (Hold != null &&
+            if (DoubleTap != null &&
+                DoubleTapBehavior == DoubleTapBehavior.WaitForDoubleTapTimeElapse &&
+                _firstFingerIndex is null &&
+                _firstTapTime != null &&
+                (DateTime.Now - _firstTapTime.Value).TotalMilliseconds >= DoubleTapTime)
+            {
+                _gestureHandled = true;
+
+                if (Tap != null && TrackedGestures.HasFlag(Gesture.Tap) && _firstTapPosition != null)
+                {
+                    Tap(_firstTapPosition.Value);
+                }
+
+                _firstTapTime = null;
+                _firstTapPosition = null;
+                _firstTapNormalizedPosition = null;
+                _firstFingerIndex = null;
+                _secondFingerIndex = null;
+                _initialFingerDistance = Vector2.Zero;
+                _initialNormalizedFingerDistance = Vector2.Zero;
+                _initialFingerAngle = 0.0f;
+            }
+            else if (Hold != null &&
                 TrackedGestures.HasFlag(Gesture.Hold) &&
                 _firstFingerIndex != null && _firstFingerLastMoveTime != null && _secondFingerIndex is null &&
                 (DateTime.Now - _firstFingerLastMoveTime.Value).TotalMilliseconds >= HoldTime &&

From cff6e21aae25ac7ebad7f649e0bad0773411f1cb Mon Sep 17 00:00:00 2001
From: Pyrdacor <trobt@web.de>
Date: Mon, 15 Jan 2024 01:14:59 +0100
Subject: [PATCH 17/21] Zoom distance is now given in absolute values

---
 src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs b/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
index 7b423485ec..f390755242 100644
--- a/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
+++ b/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
@@ -203,7 +203,7 @@ private void CheckTwoFingerGestures()
                         var firstFingerPosition = firstFinger.Value.Position;
                         var fingerDistance = secondFinger.Value.Position - firstFingerPosition - _initialFingerDistance;
 
-                        zoomInvoker = () => Zoom(firstFingerPosition, fingerDistance);
+                        zoomInvoker = () => Zoom(firstFingerPosition, new Vector2(Math.Abs(fingerDistance.X), Math.Abs(fingerDistance.Y))));
 
                         if (multiGestureHandling == MultiGestureHandling.PrioritizeZoomGesture)
                         {

From 079884133828802fd0649e7d9d8809229a30ec21 Mon Sep 17 00:00:00 2001
From: Pyrdacor <trobt@web.de>
Date: Mon, 15 Jan 2024 01:26:56 +0100
Subject: [PATCH 18/21] Zoom gesture now works with a single distance value

---
 .../PublicAPI/net5.0/PublicAPI.Unshipped.txt  | 66 +++++++++++++++++++
 .../TouchGestureRecognizer.cs                 | 45 +++++++------
 2 files changed, 92 insertions(+), 19 deletions(-)

diff --git a/src/Input/Silk.NET.Input.Common/PublicAPI/net5.0/PublicAPI.Unshipped.txt b/src/Input/Silk.NET.Input.Common/PublicAPI/net5.0/PublicAPI.Unshipped.txt
index 862d9e4729..bf305d02f8 100644
--- a/src/Input/Silk.NET.Input.Common/PublicAPI/net5.0/PublicAPI.Unshipped.txt
+++ b/src/Input/Silk.NET.Input.Common/PublicAPI/net5.0/PublicAPI.Unshipped.txt
@@ -1,4 +1,70 @@
 #nullable enable
+Silk.NET.Input.DoubleTapBehavior
+Silk.NET.Input.DoubleTapBehavior.EmitBothSingleTaps = 1 -> Silk.NET.Input.DoubleTapBehavior
+Silk.NET.Input.DoubleTapBehavior.EmitFirstSingleTap = 0 -> Silk.NET.Input.DoubleTapBehavior
+Silk.NET.Input.DoubleTapBehavior.WaitForDoubleTapTimeElapse = 2 -> Silk.NET.Input.DoubleTapBehavior
+Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.All = Silk.NET.Input.Gesture.Tap | Silk.NET.Input.Gesture.DoubleTap | Silk.NET.Input.Gesture.Swipe | Silk.NET.Input.Gesture.Hold | Silk.NET.Input.Gesture.Zoom | Silk.NET.Input.Gesture.Rotate -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.DoubleTap = 2 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.Hold = 8 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.None = 0 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.Rotate = 32 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.Swipe = 4 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.Tap = 1 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.Zoom = 16 -> Silk.NET.Input.Gesture
+Silk.NET.Input.IInputContext.PrimaryTouchDevice.get -> Silk.NET.Input.ITouchDevice?
+Silk.NET.Input.IInputContext.TouchDevices.get -> System.Collections.Generic.IReadOnlyList<Silk.NET.Input.ITouchDevice!>!
+Silk.NET.Input.ITouchDevice
+Silk.NET.Input.ITouchDevice.FingerDown -> System.Action<Silk.NET.Input.ITouchDevice!, Silk.NET.Input.TouchFinger>?
+Silk.NET.Input.ITouchDevice.FingerMove -> System.Action<Silk.NET.Input.ITouchDevice!, Silk.NET.Input.TouchFinger, System.Numerics.Vector2>?
+Silk.NET.Input.ITouchDevice.Fingers.get -> System.Collections.Generic.IReadOnlyDictionary<long, Silk.NET.Input.TouchFinger>!
+Silk.NET.Input.ITouchDevice.FingerUp -> System.Action<Silk.NET.Input.ITouchDevice!, Silk.NET.Input.TouchFinger>?
+Silk.NET.Input.ITouchDevice.GestureRecognizer.get -> Silk.NET.Input.TouchGestureRecognizer!
+Silk.NET.Input.ITouchDevice.IsFingerDown(long index) -> bool
+Silk.NET.Input.MultiGestureHandling
+Silk.NET.Input.MultiGestureHandling.PrioritizeRotateGesture = 1 -> Silk.NET.Input.MultiGestureHandling
+Silk.NET.Input.MultiGestureHandling.PrioritizeZoomGesture = 0 -> Silk.NET.Input.MultiGestureHandling
+Silk.NET.Input.MultiGestureHandling.RecognizeBothGestures = 2 -> Silk.NET.Input.MultiGestureHandling
+Silk.NET.Input.TouchFinger
+Silk.NET.Input.TouchFinger.Down.get -> bool
+Silk.NET.Input.TouchFinger.Index.get -> long
+Silk.NET.Input.TouchFinger.NormalizedPosition.get -> System.Numerics.Vector2
+Silk.NET.Input.TouchFinger.NormalizedSpeed.get -> System.Numerics.Vector2
+Silk.NET.Input.TouchFinger.Position.get -> System.Numerics.Vector2
+Silk.NET.Input.TouchFinger.Speed.get -> System.Numerics.Vector2
+Silk.NET.Input.TouchFinger.TouchFinger() -> void
+Silk.NET.Input.TouchFinger.TouchFinger(long index, System.Numerics.Vector2 position, System.Numerics.Vector2 normalizedPosition, System.Numerics.Vector2 speed, System.Numerics.Vector2 normalizedSpeed, bool down) -> void
+Silk.NET.Input.TouchGestureRecognizer
+Silk.NET.Input.TouchGestureRecognizer.Dispose() -> void
+Silk.NET.Input.TouchGestureRecognizer.DoubleTap -> System.Action<System.Numerics.Vector2>?
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapBehavior.get -> Silk.NET.Input.DoubleTapBehavior
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapBehavior.set -> void
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapRange.get -> float
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapRange.set -> void
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapTime.get -> int
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapTime.set -> void
+Silk.NET.Input.TouchGestureRecognizer.Hold -> System.Action<System.Numerics.Vector2>?
+Silk.NET.Input.TouchGestureRecognizer.HoldTime.get -> int
+Silk.NET.Input.TouchGestureRecognizer.HoldTime.set -> void
+Silk.NET.Input.TouchGestureRecognizer.MultiGestureHandling.get -> Silk.NET.Input.MultiGestureHandling
+Silk.NET.Input.TouchGestureRecognizer.MultiGestureHandling.set -> void
+Silk.NET.Input.TouchGestureRecognizer.Rotate -> System.Action<System.Numerics.Vector2, float>?
+Silk.NET.Input.TouchGestureRecognizer.RotateAngleThreshold.get -> float
+Silk.NET.Input.TouchGestureRecognizer.RotateAngleThreshold.set -> void
+Silk.NET.Input.TouchGestureRecognizer.Swipe -> System.Action<System.Numerics.Vector2>?
+Silk.NET.Input.TouchGestureRecognizer.SwipeMaxSpeed.get -> float
+Silk.NET.Input.TouchGestureRecognizer.SwipeMaxSpeed.set -> void
+Silk.NET.Input.TouchGestureRecognizer.SwipeMinSpeed.get -> float
+Silk.NET.Input.TouchGestureRecognizer.SwipeMinSpeed.set -> void
+Silk.NET.Input.TouchGestureRecognizer.Tap -> System.Action<System.Numerics.Vector2>?
+Silk.NET.Input.TouchGestureRecognizer.TrackedGestures.get -> Silk.NET.Input.Gesture
+Silk.NET.Input.TouchGestureRecognizer.TrackedGestures.set -> void
+Silk.NET.Input.TouchGestureRecognizer.Update() -> void
+Silk.NET.Input.TouchGestureRecognizer.Zoom -> System.Action<System.Numerics.Vector2, float>?
+Silk.NET.Input.TouchGestureRecognizer.ZoomInDistanceThreshold.get -> float
+Silk.NET.Input.TouchGestureRecognizer.ZoomInDistanceThreshold.set -> void
+Silk.NET.Input.TouchGestureRecognizer.ZoomOutDistanceThreshold.get -> float
+Silk.NET.Input.TouchGestureRecognizer.ZoomOutDistanceThreshold.set -> void
 static Silk.NET.Input.GamepadExtensions.LeftThumbstick(this Silk.NET.Input.IGamepad! gamepad) -> Silk.NET.Input.Thumbstick
 static Silk.NET.Input.GamepadExtensions.LeftThumbstickButton(this Silk.NET.Input.IGamepad! gamepad) -> Silk.NET.Input.Button
 static Silk.NET.Input.GamepadExtensions.RightThumbstick(this Silk.NET.Input.IGamepad! gamepad) -> Silk.NET.Input.Thumbstick
diff --git a/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs b/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
index f390755242..9cda15de85 100644
--- a/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
+++ b/src/Input/Silk.NET.Input.Common/TouchGestureRecognizer.cs
@@ -19,8 +19,8 @@ public sealed class TouchGestureRecognizer : IDisposable
         private DateTime? _firstTapTime;
         private Vector2? _firstTapPosition;
         private Vector2? _firstTapNormalizedPosition;
-        private Vector2 _initialFingerDistance = Vector2.Zero;
-        private Vector2 _initialNormalizedFingerDistance = Vector2.Zero;
+        private float _initialFingerDistance = 0.0f;
+        private float _initialNormalizedFingerDistance = 0.0f;
         private float _initialFingerAngle = 0.0f;
         private bool _gestureHandled = false;
 
@@ -45,8 +45,8 @@ private void Device_FingerDown(ITouchDevice touchDevice, TouchFinger finger)
                 _secondFingerIndex = finger.Index;
                 var firstFinger = _device.Fingers[_firstFingerIndex.Value];
 
-                _initialFingerDistance = finger.Position - firstFinger.Position;
-                _initialNormalizedFingerDistance = finger.NormalizedPosition - firstFinger.NormalizedPosition;
+                _initialFingerDistance = (finger.Position - firstFinger.Position).Length();
+                _initialNormalizedFingerDistance = (finger.NormalizedPosition - firstFinger.NormalizedPosition).Length();
                 _initialFingerAngle = (float)(Math.Atan2(finger.NormalizedPosition.Y - firstFinger.NormalizedPosition.Y,
                     finger.NormalizedPosition.X - firstFinger.NormalizedPosition.X) * 180.0 / Math.PI);
             }
@@ -119,15 +119,15 @@ private void Device_FingerUp(ITouchDevice touchDevice, TouchFinger finger)
                 _firstTapNormalizedPosition = doubleTap ? null : finger.NormalizedPosition;
                 _firstFingerIndex = null;
                 _secondFingerIndex = null;
-                _initialFingerDistance = Vector2.Zero;
-                _initialNormalizedFingerDistance = Vector2.Zero;
+                _initialFingerDistance = 0.0f;
+                _initialNormalizedFingerDistance = 0.0f;
                 _initialFingerAngle = 0.0f;
             }
             else if (finger.Index == _secondFingerIndex)
             {
                 _secondFingerIndex = null;
-                _initialFingerDistance = Vector2.Zero;
-                _initialNormalizedFingerDistance = Vector2.Zero;
+                _initialFingerDistance = 0.0f;
+                _initialNormalizedFingerDistance = 0.0f;
                 _initialFingerAngle = 0.0f;
                 _gestureHandled = true;
             }
@@ -166,8 +166,8 @@ _secondFingerIndex is null &&
                     _secondFingerIndex = finger.Index;
                     var firstFinger = _device.Fingers[_firstFingerIndex.Value];
 
-                    _initialFingerDistance = finger.Position - firstFinger.Position;
-                    _initialNormalizedFingerDistance = finger.NormalizedPosition - firstFinger.NormalizedPosition;
+                    _initialFingerDistance = (finger.Position - firstFinger.Position).Length();
+                    _initialNormalizedFingerDistance = (finger.NormalizedPosition - firstFinger.NormalizedPosition).Length();
                     _initialFingerAngle = (float) (Math.Atan2(finger.NormalizedPosition.Y - firstFinger.NormalizedPosition.Y,
                         finger.NormalizedPosition.X - firstFinger.NormalizedPosition.X) * 180.0 / Math.PI);
                 }
@@ -195,15 +195,16 @@ private void CheckTwoFingerGestures()
 
                 if (Zoom != null && TrackedGestures.HasFlag(Gesture.Zoom))
                 {
-                    var normalizedFingerDistance = secondFinger.Value.NormalizedPosition - firstFinger.Value.NormalizedPosition;
+                    var normalizedFingerDistance = (secondFinger.Value.NormalizedPosition - firstFinger.Value.NormalizedPosition).Length();
                     var normalizedDistance = normalizedFingerDistance - _initialNormalizedFingerDistance;
 
-                    if (Math.Abs(normalizedDistance.X) >= ZoomDistanceThreshold || Math.Abs(normalizedDistance.Y) >= ZoomDistanceThreshold)
+                    if ((normalizedDistance >= 0.0f && normalizedDistance >= ZoomInDistanceThreshold) ||
+                        (normalizedDistance < 0.0f && -normalizedDistance >= ZoomOutDistanceThreshold))
                     {
                         var firstFingerPosition = firstFinger.Value.Position;
-                        var fingerDistance = secondFinger.Value.Position - firstFingerPosition - _initialFingerDistance;
+                        var fingerDistance = (secondFinger.Value.Position - firstFingerPosition).Length() - _initialFingerDistance;
 
-                        zoomInvoker = () => Zoom(firstFingerPosition, new Vector2(Math.Abs(fingerDistance.X), Math.Abs(fingerDistance.Y))));
+                        zoomInvoker = () => Zoom(firstFingerPosition, fingerDistance);
 
                         if (multiGestureHandling == MultiGestureHandling.PrioritizeZoomGesture)
                         {
@@ -276,8 +277,8 @@ _firstFingerIndex is null &&
                 _firstTapNormalizedPosition = null;
                 _firstFingerIndex = null;
                 _secondFingerIndex = null;
-                _initialFingerDistance = Vector2.Zero;
-                _initialNormalizedFingerDistance = Vector2.Zero;
+                _initialFingerDistance = 0.0f;
+                _initialNormalizedFingerDistance = 0.0f;
                 _initialFingerAngle = 0.0f;
             }
             else if (Hold != null &&
@@ -343,10 +344,16 @@ _firstFingerIndex is null &&
         public int HoldTime { get; set; } = 1000;
 
         /// <summary>
-        /// Distance threshold as a normalized value (0..1) for zoom gesture tracking.
+        /// Distance threshold as a normalized value (0..1) for zoom in gesture tracking.
         /// </summary>
         [DefaultValue(0.15f)]
-        public float ZoomDistanceThreshold { get; set; } = 0.15f;
+        public float ZoomInDistanceThreshold { get; set; } = 0.15f;
+
+        /// <summary>
+        /// Distance threshold as a normalized value (0..1) for zoom out in gesture tracking.
+        /// </summary>
+        [DefaultValue(0.15f)]
+        public float ZoomOutDistanceThreshold { get; set; } = 0.1f;
 
         /// <summary>
         /// Angle threshold in degrees for rotate gesture tracking.
@@ -393,7 +400,7 @@ _firstFingerIndex is null &&
         /// The first event argument gives the first finger position in pixel coordinates and the second
         /// event argument the total distance change in pixels of the two fingers in relation to the initial finger distance.
         /// </remarks>
-        public event Action<Vector2, Vector2>? Zoom;
+        public event Action<Vector2, float>? Zoom;
 
         /// <summary>
         /// Rotate gesture.

From 8c8581e0622e2d1ee939f3c2b4c8bf15d3302ce2 Mon Sep 17 00:00:00 2001
From: Pyrdacor <trobt@web.de>
Date: Mon, 15 Jan 2024 01:30:30 +0100
Subject: [PATCH 19/21] Updated demo project for last commit change

---
 examples/CSharp/OpenGL Demos/AndroidInputDemo/MainActivity.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/examples/CSharp/OpenGL Demos/AndroidInputDemo/MainActivity.cs b/examples/CSharp/OpenGL Demos/AndroidInputDemo/MainActivity.cs
index 1ed19e4ead..ddba6dfdd7 100644
--- a/examples/CSharp/OpenGL Demos/AndroidInputDemo/MainActivity.cs	
+++ b/examples/CSharp/OpenGL Demos/AndroidInputDemo/MainActivity.cs	
@@ -100,7 +100,7 @@ private static void GestureRecognizer_Rotate(Vector2 position, float angle)
             DebugLog($"Rotate gesture at {position} with rotation {angle} degree");
         }
 
-        private static void GestureRecognizer_Zoom(Vector2 position, Vector2 zoom)
+        private static void GestureRecognizer_Zoom(Vector2 position, float zoom)
         {
             DebugLog($"Zoom gesture at {position} with zoom {zoom}");
         }

From 2f0042a43c519cba3719e2e4a206cb7466cbdd6a Mon Sep 17 00:00:00 2001
From: Pyrdacor <trobt@web.de>
Date: Mon, 15 Jan 2024 17:10:05 +0100
Subject: [PATCH 20/21] Added stuff to the public API for the 10th time or so
 ...

---
 .../netstandard2.1/PublicAPI.Unshipped.txt    | 66 +++++++++++++++++++
 1 file changed, 66 insertions(+)

diff --git a/src/Input/Silk.NET.Input.Common/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt b/src/Input/Silk.NET.Input.Common/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt
index 862d9e4729..bf305d02f8 100644
--- a/src/Input/Silk.NET.Input.Common/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt
+++ b/src/Input/Silk.NET.Input.Common/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt
@@ -1,4 +1,70 @@
 #nullable enable
+Silk.NET.Input.DoubleTapBehavior
+Silk.NET.Input.DoubleTapBehavior.EmitBothSingleTaps = 1 -> Silk.NET.Input.DoubleTapBehavior
+Silk.NET.Input.DoubleTapBehavior.EmitFirstSingleTap = 0 -> Silk.NET.Input.DoubleTapBehavior
+Silk.NET.Input.DoubleTapBehavior.WaitForDoubleTapTimeElapse = 2 -> Silk.NET.Input.DoubleTapBehavior
+Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.All = Silk.NET.Input.Gesture.Tap | Silk.NET.Input.Gesture.DoubleTap | Silk.NET.Input.Gesture.Swipe | Silk.NET.Input.Gesture.Hold | Silk.NET.Input.Gesture.Zoom | Silk.NET.Input.Gesture.Rotate -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.DoubleTap = 2 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.Hold = 8 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.None = 0 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.Rotate = 32 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.Swipe = 4 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.Tap = 1 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.Zoom = 16 -> Silk.NET.Input.Gesture
+Silk.NET.Input.IInputContext.PrimaryTouchDevice.get -> Silk.NET.Input.ITouchDevice?
+Silk.NET.Input.IInputContext.TouchDevices.get -> System.Collections.Generic.IReadOnlyList<Silk.NET.Input.ITouchDevice!>!
+Silk.NET.Input.ITouchDevice
+Silk.NET.Input.ITouchDevice.FingerDown -> System.Action<Silk.NET.Input.ITouchDevice!, Silk.NET.Input.TouchFinger>?
+Silk.NET.Input.ITouchDevice.FingerMove -> System.Action<Silk.NET.Input.ITouchDevice!, Silk.NET.Input.TouchFinger, System.Numerics.Vector2>?
+Silk.NET.Input.ITouchDevice.Fingers.get -> System.Collections.Generic.IReadOnlyDictionary<long, Silk.NET.Input.TouchFinger>!
+Silk.NET.Input.ITouchDevice.FingerUp -> System.Action<Silk.NET.Input.ITouchDevice!, Silk.NET.Input.TouchFinger>?
+Silk.NET.Input.ITouchDevice.GestureRecognizer.get -> Silk.NET.Input.TouchGestureRecognizer!
+Silk.NET.Input.ITouchDevice.IsFingerDown(long index) -> bool
+Silk.NET.Input.MultiGestureHandling
+Silk.NET.Input.MultiGestureHandling.PrioritizeRotateGesture = 1 -> Silk.NET.Input.MultiGestureHandling
+Silk.NET.Input.MultiGestureHandling.PrioritizeZoomGesture = 0 -> Silk.NET.Input.MultiGestureHandling
+Silk.NET.Input.MultiGestureHandling.RecognizeBothGestures = 2 -> Silk.NET.Input.MultiGestureHandling
+Silk.NET.Input.TouchFinger
+Silk.NET.Input.TouchFinger.Down.get -> bool
+Silk.NET.Input.TouchFinger.Index.get -> long
+Silk.NET.Input.TouchFinger.NormalizedPosition.get -> System.Numerics.Vector2
+Silk.NET.Input.TouchFinger.NormalizedSpeed.get -> System.Numerics.Vector2
+Silk.NET.Input.TouchFinger.Position.get -> System.Numerics.Vector2
+Silk.NET.Input.TouchFinger.Speed.get -> System.Numerics.Vector2
+Silk.NET.Input.TouchFinger.TouchFinger() -> void
+Silk.NET.Input.TouchFinger.TouchFinger(long index, System.Numerics.Vector2 position, System.Numerics.Vector2 normalizedPosition, System.Numerics.Vector2 speed, System.Numerics.Vector2 normalizedSpeed, bool down) -> void
+Silk.NET.Input.TouchGestureRecognizer
+Silk.NET.Input.TouchGestureRecognizer.Dispose() -> void
+Silk.NET.Input.TouchGestureRecognizer.DoubleTap -> System.Action<System.Numerics.Vector2>?
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapBehavior.get -> Silk.NET.Input.DoubleTapBehavior
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapBehavior.set -> void
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapRange.get -> float
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapRange.set -> void
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapTime.get -> int
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapTime.set -> void
+Silk.NET.Input.TouchGestureRecognizer.Hold -> System.Action<System.Numerics.Vector2>?
+Silk.NET.Input.TouchGestureRecognizer.HoldTime.get -> int
+Silk.NET.Input.TouchGestureRecognizer.HoldTime.set -> void
+Silk.NET.Input.TouchGestureRecognizer.MultiGestureHandling.get -> Silk.NET.Input.MultiGestureHandling
+Silk.NET.Input.TouchGestureRecognizer.MultiGestureHandling.set -> void
+Silk.NET.Input.TouchGestureRecognizer.Rotate -> System.Action<System.Numerics.Vector2, float>?
+Silk.NET.Input.TouchGestureRecognizer.RotateAngleThreshold.get -> float
+Silk.NET.Input.TouchGestureRecognizer.RotateAngleThreshold.set -> void
+Silk.NET.Input.TouchGestureRecognizer.Swipe -> System.Action<System.Numerics.Vector2>?
+Silk.NET.Input.TouchGestureRecognizer.SwipeMaxSpeed.get -> float
+Silk.NET.Input.TouchGestureRecognizer.SwipeMaxSpeed.set -> void
+Silk.NET.Input.TouchGestureRecognizer.SwipeMinSpeed.get -> float
+Silk.NET.Input.TouchGestureRecognizer.SwipeMinSpeed.set -> void
+Silk.NET.Input.TouchGestureRecognizer.Tap -> System.Action<System.Numerics.Vector2>?
+Silk.NET.Input.TouchGestureRecognizer.TrackedGestures.get -> Silk.NET.Input.Gesture
+Silk.NET.Input.TouchGestureRecognizer.TrackedGestures.set -> void
+Silk.NET.Input.TouchGestureRecognizer.Update() -> void
+Silk.NET.Input.TouchGestureRecognizer.Zoom -> System.Action<System.Numerics.Vector2, float>?
+Silk.NET.Input.TouchGestureRecognizer.ZoomInDistanceThreshold.get -> float
+Silk.NET.Input.TouchGestureRecognizer.ZoomInDistanceThreshold.set -> void
+Silk.NET.Input.TouchGestureRecognizer.ZoomOutDistanceThreshold.get -> float
+Silk.NET.Input.TouchGestureRecognizer.ZoomOutDistanceThreshold.set -> void
 static Silk.NET.Input.GamepadExtensions.LeftThumbstick(this Silk.NET.Input.IGamepad! gamepad) -> Silk.NET.Input.Thumbstick
 static Silk.NET.Input.GamepadExtensions.LeftThumbstickButton(this Silk.NET.Input.IGamepad! gamepad) -> Silk.NET.Input.Button
 static Silk.NET.Input.GamepadExtensions.RightThumbstick(this Silk.NET.Input.IGamepad! gamepad) -> Silk.NET.Input.Thumbstick

From d968f4f9437cccffada68e70fde545808141570e Mon Sep 17 00:00:00 2001
From: Pyrdacor <trobt@web.de>
Date: Mon, 22 Jan 2024 11:40:24 +0100
Subject: [PATCH 21/21] Updated net 2.0 public API definition

---
 .../netstandard2.0/PublicAPI.Unshipped.txt    | 64 +++++++++++++++++++
 1 file changed, 64 insertions(+)

diff --git a/src/Input/Silk.NET.Input.Common/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Input/Silk.NET.Input.Common/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
index 862d9e4729..0cae6b2980 100644
--- a/src/Input/Silk.NET.Input.Common/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
+++ b/src/Input/Silk.NET.Input.Common/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
@@ -1,4 +1,68 @@
 #nullable enable
+Silk.NET.Input.DoubleTapBehavior
+Silk.NET.Input.DoubleTapBehavior.EmitBothSingleTaps = 1 -> Silk.NET.Input.DoubleTapBehavior
+Silk.NET.Input.DoubleTapBehavior.EmitFirstSingleTap = 0 -> Silk.NET.Input.DoubleTapBehavior
+Silk.NET.Input.DoubleTapBehavior.WaitForDoubleTapTimeElapse = 2 -> Silk.NET.Input.DoubleTapBehavior
+Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.All = Silk.NET.Input.Gesture.Tap | Silk.NET.Input.Gesture.DoubleTap | Silk.NET.Input.Gesture.Swipe | Silk.NET.Input.Gesture.Hold | Silk.NET.Input.Gesture.Zoom | Silk.NET.Input.Gesture.Rotate -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.DoubleTap = 2 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.Hold = 8 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.None = 0 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.Rotate = 32 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.Swipe = 4 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.Tap = 1 -> Silk.NET.Input.Gesture
+Silk.NET.Input.Gesture.Zoom = 16 -> Silk.NET.Input.Gesture
+Silk.NET.Input.IInputContext.PrimaryTouchDevice.get -> Silk.NET.Input.ITouchDevice?
+Silk.NET.Input.IInputContext.TouchDevices.get -> System.Collections.Generic.IReadOnlyList<Silk.NET.Input.ITouchDevice!>!
+Silk.NET.Input.ITouchDevice
+Silk.NET.Input.ITouchDevice.FingerDown -> System.Action<Silk.NET.Input.ITouchDevice!, Silk.NET.Input.TouchFinger>?
+Silk.NET.Input.ITouchDevice.FingerMove -> System.Action<Silk.NET.Input.ITouchDevice!, Silk.NET.Input.TouchFinger, System.Numerics.Vector2>?
+Silk.NET.Input.ITouchDevice.Fingers.get -> System.Collections.Generic.IReadOnlyDictionary<long, Silk.NET.Input.TouchFinger>!
+Silk.NET.Input.ITouchDevice.FingerUp -> System.Action<Silk.NET.Input.ITouchDevice!, Silk.NET.Input.TouchFinger>?
+Silk.NET.Input.ITouchDevice.GestureRecognizer.get -> Silk.NET.Input.TouchGestureRecognizer!
+Silk.NET.Input.ITouchDevice.IsFingerDown(long index) -> bool
+Silk.NET.Input.MultiGestureHandling
+Silk.NET.Input.MultiGestureHandling.PrioritizeRotateGesture = 1 -> Silk.NET.Input.MultiGestureHandling
+Silk.NET.Input.MultiGestureHandling.PrioritizeZoomGesture = 0 -> Silk.NET.Input.MultiGestureHandling
+Silk.NET.Input.MultiGestureHandling.RecognizeBothGestures = 2 -> Silk.NET.Input.MultiGestureHandling
+Silk.NET.Input.TouchFinger
+Silk.NET.Input.TouchFinger.Down.get -> bool
+Silk.NET.Input.TouchFinger.Index.get -> long
+Silk.NET.Input.TouchFinger.NormalizedPosition.get -> System.Numerics.Vector2
+Silk.NET.Input.TouchFinger.NormalizedSpeed.get -> System.Numerics.Vector2
+Silk.NET.Input.TouchFinger.Position.get -> System.Numerics.Vector2
+Silk.NET.Input.TouchFinger.Speed.get -> System.Numerics.Vector2
+Silk.NET.Input.TouchFinger.TouchFinger() -> void
+Silk.NET.Input.TouchFinger.TouchFinger(long index, System.Numerics.Vector2 position, System.Numerics.Vector2 normalizedPosition, System.Numerics.Vector2 speed, System.Numerics.Vector2 normalizedSpeed, bool down) -> void
+Silk.NET.Input.TouchGestureRecognizer
+Silk.NET.Input.TouchGestureRecognizer.Dispose() -> void
+Silk.NET.Input.TouchGestureRecognizer.DoubleTap -> System.Action<System.Numerics.Vector2>?
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapBehavior.get -> Silk.NET.Input.DoubleTapBehavior
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapBehavior.set -> void
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapRange.get -> float
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapRange.set -> void
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapTime.get -> int
+Silk.NET.Input.TouchGestureRecognizer.DoubleTapTime.set -> void
+Silk.NET.Input.TouchGestureRecognizer.Hold -> System.Action<System.Numerics.Vector2>?
+Silk.NET.Input.TouchGestureRecognizer.HoldTime.get -> int
+Silk.NET.Input.TouchGestureRecognizer.HoldTime.set -> void
+Silk.NET.Input.TouchGestureRecognizer.MultiGestureHandling.get -> Silk.NET.Input.MultiGestureHandling
+Silk.NET.Input.TouchGestureRecognizer.MultiGestureHandling.set -> void
+Silk.NET.Input.TouchGestureRecognizer.Rotate -> System.Action<System.Numerics.Vector2, float>?
+Silk.NET.Input.TouchGestureRecognizer.RotateAngleThreshold.get -> float
+Silk.NET.Input.TouchGestureRecognizer.RotateAngleThreshold.set -> void
+Silk.NET.Input.TouchGestureRecognizer.Swipe -> System.Action<System.Numerics.Vector2>?
+Silk.NET.Input.TouchGestureRecognizer.SwipeMaxSpeed.get -> float
+Silk.NET.Input.TouchGestureRecognizer.SwipeMaxSpeed.set -> void
+Silk.NET.Input.TouchGestureRecognizer.SwipeMinSpeed.get -> float
+Silk.NET.Input.TouchGestureRecognizer.SwipeMinSpeed.set -> void
+Silk.NET.Input.TouchGestureRecognizer.Tap -> System.Action<System.Numerics.Vector2>?
+Silk.NET.Input.TouchGestureRecognizer.TrackedGestures.get -> Silk.NET.Input.Gesture
+Silk.NET.Input.TouchGestureRecognizer.TrackedGestures.set -> void
+Silk.NET.Input.TouchGestureRecognizer.Update() -> void
+Silk.NET.Input.TouchGestureRecognizer.Zoom -> System.Action<System.Numerics.Vector2, System.Numerics.Vector2>?
+Silk.NET.Input.TouchGestureRecognizer.ZoomDistanceThreshold.get -> float
+Silk.NET.Input.TouchGestureRecognizer.ZoomDistanceThreshold.set -> void
 static Silk.NET.Input.GamepadExtensions.LeftThumbstick(this Silk.NET.Input.IGamepad! gamepad) -> Silk.NET.Input.Thumbstick
 static Silk.NET.Input.GamepadExtensions.LeftThumbstickButton(this Silk.NET.Input.IGamepad! gamepad) -> Silk.NET.Input.Button
 static Silk.NET.Input.GamepadExtensions.RightThumbstick(this Silk.NET.Input.IGamepad! gamepad) -> Silk.NET.Input.Thumbstick