Browse Source

Added GetIntermediatePoints support for X11, libinput and evdev

Nikita Tsukanov 3 years ago
parent
commit
9c0964adf5

+ 217 - 3
samples/ControlCatalog/Pages/PointersPage.cs

@@ -1,15 +1,37 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
+using System.Reactive.Linq;
+using System.Runtime.InteropServices;
+using System.Threading;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Input;
+using Avalonia.Layout;
 using Avalonia.Media;
 using Avalonia.Media.Immutable;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
 
-namespace ControlCatalog.Pages
+namespace ControlCatalog.Pages;
+
+public class PointersPage : Decorator
 {
-    public class PointersPage : Control
+    public PointersPage()
+    {
+        Child = new TabControl
+        {
+            Items = new[]
+            {
+                new TabItem() { Header = "Contacts", Content = new PointerContactsTab() },
+                new TabItem() { Header = "IntermediatePoints", Content = new PointerIntermediatePointsTab() }
+            }
+        };
+    }
+    
+    
+    class PointerContactsTab : Control
     {
         class PointerInfo
         {
@@ -45,7 +67,7 @@ namespace ControlCatalog.Pages
         
         private Dictionary<IPointer, PointerInfo> _pointers = new Dictionary<IPointer, PointerInfo>();
 
-        public PointersPage()
+        public PointerContactsTab()
         {
             ClipToBounds = true;
         }
@@ -104,4 +126,196 @@ namespace ControlCatalog.Pages
             }
         }
     }
+
+    public class PointerIntermediatePointsTab : Decorator
+    {
+        public PointerIntermediatePointsTab()
+        {
+            this[TextBlock.ForegroundProperty] = Brushes.Black;
+            var slider = new Slider
+            {
+                Margin = new Thickness(5),
+                Minimum = 0,
+                Maximum = 500
+            };
+
+            var status = new TextBlock()
+            {
+                HorizontalAlignment = HorizontalAlignment.Left,
+                VerticalAlignment = VerticalAlignment.Top,
+            };
+            Child = new Grid
+            {
+                Children =
+                {
+                    new PointerCanvas(slider, status),
+                    new Border
+                    {
+                        Background = Brushes.LightYellow,
+                        Child = new StackPanel
+                        {
+                            Children =
+                            {
+                                new StackPanel
+                                {
+                                    Orientation = Orientation.Horizontal,
+                                    Children =
+                                    {
+                                        new TextBlock { Text = "Thread sleep:" },
+                                        new TextBlock()
+                                        {
+                                            [!TextBlock.TextProperty] =slider.GetObservable(Slider.ValueProperty)
+                                                .Select(x=>x.ToString()).ToBinding()
+                                        }
+                                    }
+                                },
+                                slider
+                            }
+                        },
+
+                        HorizontalAlignment = HorizontalAlignment.Right,
+                        VerticalAlignment = VerticalAlignment.Top,
+                        Width = 300,
+                        Height = 60
+                    },
+                    status
+                }
+            };
+        }
+
+        class PointerCanvas : Control
+        {
+            private readonly Slider _slider;
+            private readonly TextBlock _status;
+            private int _events;
+            private Stopwatch _stopwatch = Stopwatch.StartNew();
+            private Dictionary<int, PointerPoints> _pointers = new();
+            class PointerPoints
+            {
+                struct CanvasPoint
+                {
+                    public IBrush Brush;
+                    public Point Point;
+                    public double Radius;
+                }
+
+                readonly CanvasPoint[] _points = new CanvasPoint[1000];
+                int _index;
+                
+                public  void Render(DrawingContext context)
+                {
+                    
+                    CanvasPoint? prev = null;
+                    for (var c = 0; c < _points.Length; c++)
+                    {
+                        var i = (c + _index) % _points.Length;
+                        var pt = _points[i];
+                        if (prev.HasValue && prev.Value.Brush != null && pt.Brush != null)
+                            context.DrawLine(new Pen(Brushes.Black), prev.Value.Point, pt.Point);
+                        prev = pt;
+                        if (pt.Brush != null)
+                            context.DrawEllipse(pt.Brush, null, pt.Point, pt.Radius, pt.Radius);
+
+                    }
+
+                }
+
+                void AddPoint(Point pt, IBrush brush, double radius)
+                {
+                    _points[_index] = new CanvasPoint { Point = pt, Brush = brush, Radius = radius };
+                    _index = (_index + 1) % _points.Length;
+                }
+
+                public void HandleEvent(PointerEventArgs e, Visual v)
+                {
+                    e.Handled = true;
+                    if (e.RoutedEvent == PointerPressedEvent)
+                        AddPoint(e.GetPosition(v), Brushes.Green, 10);
+                    else if (e.RoutedEvent == PointerReleasedEvent)
+                        AddPoint(e.GetPosition(v), Brushes.Red, 10);
+                    else
+                    {
+                        var pts = e.GetIntermediatePoints(v);
+                        for (var c = 0; c < pts.Count; c++)
+                        {
+                            var pt = pts[c];
+                            AddPoint(pt.Position, c == pts.Count - 1 ? Brushes.Blue : Brushes.Black,
+                                c == pts.Count - 1 ? 5 : 2);
+                        }
+                    }
+                }
+            }
+            
+            public PointerCanvas(Slider slider, TextBlock status)
+            {
+                _slider = slider;
+                _status = status;
+                DispatcherTimer.Run(() =>
+                {
+                    if (_stopwatch.Elapsed.TotalSeconds > 1)
+                    {
+                        _status.Text = "Events per second: " + (_events / _stopwatch.Elapsed.TotalSeconds);
+                        _stopwatch.Restart();
+                        _events = 0;
+                    }
+
+                    return this.GetVisualRoot() != null;
+                }, TimeSpan.FromMilliseconds(10));
+            }
+
+
+            void HandleEvent(PointerEventArgs e)
+            {
+                _events++;
+                Thread.Sleep((int)_slider.Value);
+                InvalidateVisual();
+
+                if (e.RoutedEvent == PointerReleasedEvent && e.Pointer.Type == PointerType.Touch)
+                {
+                    _pointers.Remove(e.Pointer.Id);
+                    return;
+                }
+
+                if (!_pointers.TryGetValue(e.Pointer.Id, out var pt))
+                    _pointers[e.Pointer.Id] = pt = new PointerPoints();
+                pt.HandleEvent(e, this);
+                
+                
+            }
+            
+            public override void Render(DrawingContext context)
+            {
+                context.FillRectangle(Brushes.White, Bounds);
+                foreach(var pt in _pointers.Values)
+                    pt.Render(context);
+                base.Render(context);
+            }
+
+            protected override void OnPointerPressed(PointerPressedEventArgs e)
+            {
+                if (e.ClickCount == 2)
+                {
+                    _pointers.Clear();
+                    InvalidateVisual();
+                    return;
+                }
+                
+                HandleEvent(e);
+                base.OnPointerPressed(e);
+            }
+
+            protected override void OnPointerMoved(PointerEventArgs e)
+            {
+                HandleEvent(e);
+                base.OnPointerMoved(e);
+            }
+
+            protected override void OnPointerReleased(PointerReleasedEventArgs e)
+            {
+                HandleEvent(e);
+                base.OnPointerReleased(e);
+            }
+        }
+    
+    }
 }

+ 7 - 0
src/Avalonia.Base/Threading/Dispatcher.cs

@@ -74,6 +74,13 @@ namespace Avalonia.Threading
         /// </summary>
         /// <param name="minimumPriority"></param>
         public void RunJobs(DispatcherPriority minimumPriority) => _jobRunner.RunJobs(minimumPriority);
+        
+        /// <summary>
+        /// Use this method to check if there are more prioritized tasks
+        /// </summary>
+        /// <param name="minimumPriority"></param>
+        public bool HasJobsWithPriority(DispatcherPriority minimumPriority) =>
+            _jobRunner.HasJobsWithPriority(minimumPriority);
 
         /// <inheritdoc/>
         public Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal)

+ 15 - 0
src/Avalonia.Base/Threading/JobRunner.cs

@@ -121,6 +121,21 @@ namespace Avalonia.Threading
             return null;
         }
 
+        public bool HasJobsWithPriority(DispatcherPriority minimumPriority)
+        {
+            for (int c = (int)minimumPriority; c < (int)DispatcherPriority.MaxValue; c++)
+            {
+                var q = _queues[c];
+                lock (q)
+                {
+                    if (q.Count > 0)
+                        return true;
+                }
+            }
+
+            return false;
+        }
+
         private interface IJob
         {
             /// <summary>

+ 6 - 5
src/Avalonia.Input/MouseDevice.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using System.Linq;
 using System.Reactive.Linq;
 using Avalonia.Input.Raw;
@@ -159,7 +160,7 @@ namespace Avalonia.Input
                 case RawPointerEventType.XButton1Down:
                 case RawPointerEventType.XButton2Down:
                     if (ButtonCount(props) > 1)
-                        e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers);
+                        e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints);
                     else
                         e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position,
                             props, keyModifiers);
@@ -170,12 +171,12 @@ namespace Avalonia.Input
                 case RawPointerEventType.XButton1Up:
                 case RawPointerEventType.XButton2Up:
                     if (ButtonCount(props) != 0)
-                        e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers);
+                        e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints);
                     else
                         e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers);
                     break;
                 case RawPointerEventType.Move:
-                    e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers);
+                    e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints);
                     break;
                 case RawPointerEventType.Wheel:
                     e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers);
@@ -263,7 +264,7 @@ namespace Avalonia.Input
         }
 
         private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties,
-            KeyModifiers inputModifiers)
+            KeyModifiers inputModifiers, IReadOnlyList<Point>? intermediatePoints)
         {
             device = device ?? throw new ArgumentNullException(nameof(device));
             root = root ?? throw new ArgumentNullException(nameof(root));
@@ -283,7 +284,7 @@ namespace Avalonia.Input
             if (source is object)
             {
                 var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root,
-                    p, timestamp, properties, inputModifiers);
+                    p, timestamp, properties, inputModifiers, intermediatePoints);
 
                 source.RaiseEvent(e);
                 return e.Handled;

+ 41 - 3
src/Avalonia.Input/PointerEventArgs.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using Avalonia.Input.Raw;
 using Avalonia.Interactivity;
 using Avalonia.VisualTree;
@@ -10,6 +11,7 @@ namespace Avalonia.Input
         private readonly IVisual? _rootVisual;
         private readonly Point _rootVisualPosition;
         private readonly PointerPointProperties _properties;
+        private readonly IReadOnlyList<Point>? _previousPoints;
 
         public PointerEventArgs(RoutedEvent routedEvent,
             IInteractive? source,
@@ -28,6 +30,20 @@ namespace Avalonia.Input
             Timestamp = timestamp;
             KeyModifiers = modifiers;
         }
+        
+        public PointerEventArgs(RoutedEvent routedEvent,
+            IInteractive? source,
+            IPointer pointer,
+            IVisual? rootVisual, Point rootVisualPosition,
+            ulong timestamp,
+            PointerPointProperties properties,
+            KeyModifiers modifiers,
+            IReadOnlyList<Point>? previousPoints)
+            : this(routedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers)
+        {
+            _previousPoints = previousPoints;
+        }
+        
 
         class EmulatedDevice : IPointerDevice
         {
@@ -76,14 +92,16 @@ namespace Avalonia.Input
         
         public KeyModifiers KeyModifiers { get; }
 
-        public Point GetPosition(IVisual? relativeTo)
+        private Point GetPosition(Point pt, IVisual? relativeTo)
         {
             if (_rootVisual == null)
                 return default;
             if (relativeTo == null)
-                return _rootVisualPosition;
-            return _rootVisualPosition * _rootVisual.TransformToVisual(relativeTo) ?? default;
+                return pt;
+            return pt * _rootVisual.TransformToVisual(relativeTo) ?? default;
         }
+        
+        public Point GetPosition(IVisual? relativeTo) => GetPosition(_rootVisualPosition, relativeTo);
 
         [Obsolete("Use GetCurrentPoint")]
         public PointerPoint GetPointerPoint(IVisual? relativeTo) => GetCurrentPoint(relativeTo);
@@ -96,6 +114,26 @@ namespace Avalonia.Input
         public PointerPoint GetCurrentPoint(IVisual? relativeTo)
             => new PointerPoint(Pointer, GetPosition(relativeTo), _properties);
 
+        /// <summary>
+        /// Returns the PointerPoint associated with the current event
+        /// </summary>
+        /// <param name="relativeTo">The visual which coordinate system to use. Pass null for toplevel coordinate system</param>
+        /// <returns></returns>
+        public IReadOnlyList<PointerPoint> GetIntermediatePoints(IVisual? relativeTo)
+        {
+            if (_previousPoints == null || _previousPoints.Count == 0)
+                return new[] { GetCurrentPoint(relativeTo) };
+            var points = new PointerPoint[_previousPoints.Count + 1];
+            for (var c = 0; c < _previousPoints.Count; c++)
+            {
+                var pt = _previousPoints[c];
+                points[c] = new PointerPoint(Pointer, GetPosition(pt, relativeTo), _properties);
+            }
+
+            points[points.Length - 1] = GetCurrentPoint(relativeTo);
+            return points;
+        }
+
         /// <summary>
         /// Returns the current pointer point properties
         /// </summary>

+ 1 - 1
src/Avalonia.Input/Raw/RawInputEventArgs.cs

@@ -51,6 +51,6 @@ namespace Avalonia.Input.Raw
         /// <summary>
         /// Gets the timestamp associated with the event.
         /// </summary>
-        public ulong Timestamp { get; private set; }
+        public ulong Timestamp { get; set; }
     }
 }

+ 8 - 1
src/Avalonia.Input/Raw/RawPointerEventArgs.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 
 namespace Avalonia.Input.Raw
 {
@@ -68,6 +69,12 @@ namespace Avalonia.Input.Raw
         /// <summary>
         /// Gets the input modifiers.
         /// </summary>
-        public RawInputModifiers InputModifiers { get; private set; }
+        public RawInputModifiers InputModifiers { get; set; }
+        
+        /// <summary>
+        /// Points that were traversed by a pointer since the previous relevant event,
+        /// only valid for Move and TouchUpdate
+        /// </summary>
+        public IReadOnlyList<Point>? IntermediatePoints { get; set; }
     }
 }

+ 1 - 1
src/Avalonia.Input/TouchDevice.cs

@@ -104,7 +104,7 @@ namespace Avalonia.Input
                 target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root,
                     args.Position, ev.Timestamp,
                     new PointerPointProperties(GetModifiers(args.InputModifiers, true), PointerUpdateKind.Other),
-                    GetKeyModifiers(args.InputModifiers)));
+                    GetKeyModifiers(args.InputModifiers), args.IntermediatePoints));
             }
 
 

+ 1 - 0
src/Avalonia.X11/Avalonia.X11.csproj

@@ -9,6 +9,7 @@
         <ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
         <ProjectReference Include="..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
         <ProjectReference Include="..\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj" />
+        <Compile Include="..\Shared\RawEventGrouping.cs" />
     </ItemGroup>
 
 </Project>

+ 12 - 28
src/Avalonia.X11/X11Window.cs

@@ -49,15 +49,10 @@ namespace Avalonia.X11
         private double? _scalingOverride;
         private bool _disabled;
         private TransparencyHelper _transparencyHelper;
-
+        private RawEventGrouper _rawEventGrouper;
         public object SyncRoot { get; } = new object();
 
-        class InputEventContainer
-        {
-            public RawInputEventArgs Event;
-        }
-        private readonly Queue<InputEventContainer> _inputQueue = new Queue<InputEventContainer>();
-        private InputEventContainer _lastEvent;
+       
         private bool _useRenderWindow = false;
         public X11Window(AvaloniaX11Platform platform, IWindowImpl popupParent)
         {
@@ -181,6 +176,8 @@ namespace Avalonia.X11
             UpdateMotifHints();
             UpdateSizeHints(null);
 
+            _rawEventGrouper = new RawEventGrouper(e => Input?.Invoke(e));
+            
             _transparencyHelper = new TransparencyHelper(_x11, _handle, platform.Globals);
             _transparencyHelper.SetTransparencyRequest(WindowTransparencyLevel.None);
 
@@ -735,33 +732,14 @@ namespace Avalonia.X11
             if (args is RawDragEvent drag)
                 drag.Location = drag.Location / RenderScaling;
             
-            _lastEvent = new InputEventContainer() {Event = args};
-            _inputQueue.Enqueue(_lastEvent);
-            if (_inputQueue.Count == 1)
-            {
-                Dispatcher.UIThread.Post(() =>
-                {
-                    while (_inputQueue.Count > 0)
-                    {
-                        Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
-                        var ev = _inputQueue.Dequeue();
-                        Input?.Invoke(ev.Event);
-                    }
-                }, DispatcherPriority.Input);
-            }
+            _rawEventGrouper.HandleEvent(args);
         }
         
         void MouseEvent(RawPointerEventType type, ref XEvent ev, XModifierMask mods)
         {
             var mev = new RawPointerEventArgs(
                 _mouse, (ulong)ev.ButtonEvent.time.ToInt64(), _inputRoot,
-                type, new Point(ev.ButtonEvent.x, ev.ButtonEvent.y), TranslateModifiers(mods)); 
-            if(type == RawPointerEventType.Move && _inputQueue.Count>0 && _lastEvent.Event is RawPointerEventArgs ma)
-                if (ma.Type == RawPointerEventType.Move)
-                {
-                    _lastEvent.Event = mev;
-                    return;
-                }
+                type, new Point(ev.ButtonEvent.x, ev.ButtonEvent.y), TranslateModifiers(mods));
             ScheduleInput(mev, ref ev);
         }
 
@@ -789,6 +767,12 @@ namespace Avalonia.X11
 
         void Cleanup()
         {
+            if (_rawEventGrouper != null)
+            {
+                _rawEventGrouper.Dispose();
+                _rawEventGrouper = null;
+            }
+            
             if (_transparencyHelper != null)
             {
                 _transparencyHelper.Dispose();

+ 1 - 0
src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj

@@ -7,5 +7,6 @@
   <ItemGroup>
     <ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
     <ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
+    <Compile Include="..\..\Shared\RawEventGrouping.cs" />
   </ItemGroup>  
 </Project>

+ 3 - 35
src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs

@@ -13,15 +13,16 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev
         private readonly EvDevDeviceDescription[] _deviceDescriptions;
         private readonly List<EvDevDeviceHandler> _handlers = new List<EvDevDeviceHandler>();
         private int _epoll;
-        private Queue<RawInputEventArgs> _inputQueue = new Queue<RawInputEventArgs>();
         private bool _isQueueHandlerTriggered;
         private object _lock = new object();
         private Action<RawInputEventArgs> _onInput;
         private IInputRoot _inputRoot;
+        private RawEventGroupingThreadingHelper _inputQueue;
 
         public EvDevBackend(EvDevDeviceDescription[] devices)
         {
             _deviceDescriptions = devices;
+            _inputQueue = new RawEventGroupingThreadingHelper(e => _onInput?.Invoke(e));
         }
         
         unsafe void InputThread()
@@ -49,42 +50,9 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev
 
         private void OnRawEvent(RawInputEventArgs obj)
         {
-            lock (_lock)
-            {
-                _inputQueue.Enqueue(obj);
-                TriggerQueueHandler();
-            }
-                
+            _inputQueue.OnEvent(obj);
         }
         
-        void TriggerQueueHandler()
-        {
-            if (_isQueueHandlerTriggered)
-                return;
-            _isQueueHandlerTriggered = true;
-            Dispatcher.UIThread.Post(InputQueueHandler, DispatcherPriority.Input);
-
-        }
-        
-        void InputQueueHandler()
-        {
-            RawInputEventArgs ev;
-            lock (_lock)
-            {
-                _isQueueHandlerTriggered = false;
-                if(_inputQueue.Count == 0)
-                    return;
-                ev = _inputQueue.Dequeue();
-            }
-
-            _onInput?.Invoke(ev);
-
-            lock (_lock)
-            {
-                if (_inputQueue.Count > 0)
-                    TriggerQueueHandler();
-            }
-        }
         
         public void Initialize(IScreenInfoProvider info, Action<RawInputEventArgs> onInput)
         {

+ 4 - 27
src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs

@@ -17,15 +17,15 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput
         private TouchDevice _touch = new TouchDevice();
         private MouseDevice _mouse = new MouseDevice();
         private Point _mousePosition;
-        
-        private readonly Queue<RawInputEventArgs> _inputQueue = new Queue<RawInputEventArgs>();
+
+        private readonly RawEventGroupingThreadingHelper _inputQueue;
         private Action<RawInputEventArgs> _onInput;
         private Dictionary<int, Point> _pointers = new Dictionary<int, Point>();
 
         public LibInputBackend()
         {
             var ctx = libinput_path_create_context();
-            
+            _inputQueue = new(e => _onInput?.Invoke(e));
             new Thread(()=>InputThread(ctx)).Start();
         }
 
@@ -66,30 +66,7 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput
             }
         }
 
-        private void ScheduleInput(RawInputEventArgs ev)
-        {
-            lock (_inputQueue)
-            {
-                _inputQueue.Enqueue(ev);
-                if (_inputQueue.Count == 1)
-                {
-                    Dispatcher.UIThread.Post(() =>
-                    {
-                        while (true)
-                        {
-                            Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
-                            RawInputEventArgs dequeuedEvent = null;
-                            lock(_inputQueue)
-                                if (_inputQueue.Count != 0)
-                                    dequeuedEvent = _inputQueue.Dequeue();
-                            if (dequeuedEvent == null)
-                                return;
-                            _onInput?.Invoke(dequeuedEvent);
-                        }
-                    }, DispatcherPriority.Input);
-                }
-            }
-        }
+        private void ScheduleInput(RawInputEventArgs ev) => _inputQueue.OnEvent(ev);
 
         private void HandleTouch(IntPtr ev, LibInputEventType type)
         {

+ 43 - 0
src/Linux/Avalonia.LinuxFramebuffer/Input/RawEventGroupingThreadingHelper.cs

@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Input.Raw;
+using Avalonia.Threading;
+
+namespace Avalonia.LinuxFramebuffer.Input;
+
+internal class RawEventGroupingThreadingHelper : IDisposable
+{
+    private readonly RawEventGrouper _grouper;
+    private readonly Queue<RawInputEventArgs> _rawQueue = new();
+    private readonly Action _queueHandler;
+
+    public RawEventGroupingThreadingHelper(Action<RawInputEventArgs> eventCallback)
+    {
+        _grouper = new RawEventGrouper(eventCallback);
+        _queueHandler = QueueHandler;
+    }
+
+    private void QueueHandler()
+    {
+        lock (_rawQueue)
+        {
+            while (_rawQueue.Count > 0)
+                _grouper.HandleEvent(_rawQueue.Dequeue());
+        }
+    }
+
+    public void OnEvent(RawInputEventArgs args)
+    {
+        lock (_rawQueue)
+        {
+            _rawQueue.Enqueue(args);
+            if (_rawQueue.Count == 1)
+            {
+                Dispatcher.UIThread.Post(_queueHandler, DispatcherPriority.Input);
+            }
+        }
+    }
+
+    public void Dispose() =>
+        Dispatcher.UIThread.Post(() => _grouper.Dispose(), DispatcherPriority.Input + 1);
+}

+ 129 - 0
src/Shared/RawEventGrouping.cs

@@ -0,0 +1,129 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using Avalonia.Collections.Pooled;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Threading;
+using JetBrains.Annotations;
+
+namespace Avalonia;
+
+/*
+  This helper maintains an input queue for backends that handle input asynchronously.
+  While doing that it groups Move and TouchUpdate events so we could provide GetIntermediatePoints API
+ */
+
+internal class RawEventGrouper : IDisposable
+{
+    private readonly Action<RawInputEventArgs> _eventCallback;
+    private readonly Queue<RawInputEventArgs> _inputQueue = new();
+    private readonly Action _dispatchFromQueue;
+    readonly Dictionary<long, RawTouchEventArgs> _lastTouchPoints = new();
+    RawInputEventArgs? _lastEvent;
+
+    public RawEventGrouper(Action<RawInputEventArgs> eventCallback)
+    {
+        _eventCallback = eventCallback;
+        _dispatchFromQueue = DispatchFromQueue;
+    }
+    
+    private void AddToQueue(RawInputEventArgs args)
+    {
+        _lastEvent = args;
+        _inputQueue.Enqueue(args);
+        if (_inputQueue.Count == 1)
+            Dispatcher.UIThread.Post(_dispatchFromQueue, DispatcherPriority.Input);
+    }
+
+    private void DispatchFromQueue()
+    {
+        while (true)
+        {
+            if(_inputQueue.Count == 0)
+                return;
+
+            var ev = _inputQueue.Dequeue();
+
+            if (_lastEvent == ev) 
+                _lastEvent = null;
+            
+            if (ev is RawTouchEventArgs { Type: RawPointerEventType.TouchUpdate } touchUpdate)
+                _lastTouchPoints.Remove(touchUpdate.TouchPointId);
+
+            _eventCallback?.Invoke(ev);
+
+            if (ev is RawPointerEventArgs { IntermediatePoints: PooledList<Point> list }) 
+                list.Dispose();
+
+            if (Dispatcher.UIThread.HasJobsWithPriority(DispatcherPriority.Input + 1))
+            {
+                Dispatcher.UIThread.Post(_dispatchFromQueue, DispatcherPriority.Input);
+                return;
+            }
+        }
+    }
+    
+    public void HandleEvent(RawInputEventArgs args)
+    {
+        /*
+         Try to update already enqueued events if
+         1) they are still not handled (_lastEvent and _lastTouchPoints shouldn't contain said event in that case)
+         2) previous event belongs to the same "event block", events in the same block:
+           - belong from the same device
+           - are pointer move events (Move/TouchUpdate)
+           - have the same type
+           - have same modifiers
+           
+         Even if nothing is updated and the event is actually enqueued, we need to update the relevant tracking info  
+        */
+        if (
+            args is RawPointerEventArgs pointerEvent
+            && _lastEvent != null 
+            && _lastEvent.Device == args.Device 
+            && _lastEvent is RawPointerEventArgs lastPointerEvent
+            && lastPointerEvent.InputModifiers == pointerEvent.InputModifiers
+            && lastPointerEvent.Type == pointerEvent.Type 
+            && lastPointerEvent.Type is RawPointerEventType.Move or RawPointerEventType.TouchUpdate)
+        {
+            if (args is RawTouchEventArgs touchEvent)
+            {
+                if (_lastTouchPoints.TryGetValue(touchEvent.TouchPointId, out var lastTouchEvent))
+                    MergeEvents(lastTouchEvent, touchEvent);
+                else
+                {
+                    _lastTouchPoints[touchEvent.TouchPointId] = touchEvent;
+                    AddToQueue(touchEvent);
+                }
+            }
+            else
+                MergeEvents(lastPointerEvent, pointerEvent);
+
+            return;
+        }
+        else
+        {
+            _lastTouchPoints.Clear();
+            if (args is RawTouchEventArgs { Type: RawPointerEventType.TouchUpdate } touchEvent)
+                _lastTouchPoints[touchEvent.TouchPointId] = touchEvent;
+        }
+        AddToQueue(args);
+    }
+
+    private static void MergeEvents(RawPointerEventArgs last, RawPointerEventArgs current)
+    {
+        last.IntermediatePoints ??= new PooledList<Point>();
+        ((PooledList<Point>)last.IntermediatePoints).Add(last.Position);
+        last.Position = current.Position;
+        last.Timestamp = current.Timestamp;
+        last.InputModifiers = current.InputModifiers;
+    }
+
+    public void Dispose()
+    {
+        _inputQueue.Clear();
+        _lastEvent = null;
+        _lastTouchPoints.Clear();
+    }
+}
+