From 9c0964adf56ab573daed6be6c88ba480db2a0ec0 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 23 Jan 2022 01:41:48 +0300 Subject: [PATCH] Added GetIntermediatePoints support for X11, libinput and evdev --- samples/ControlCatalog/Pages/PointersPage.cs | 220 +++++++++++++++++- src/Avalonia.Base/Threading/Dispatcher.cs | 7 + src/Avalonia.Base/Threading/JobRunner.cs | 15 ++ src/Avalonia.Input/MouseDevice.cs | 11 +- src/Avalonia.Input/PointerEventArgs.cs | 44 +++- src/Avalonia.Input/Raw/RawInputEventArgs.cs | 2 +- src/Avalonia.Input/Raw/RawPointerEventArgs.cs | 9 +- src/Avalonia.Input/TouchDevice.cs | 2 +- src/Avalonia.X11/Avalonia.X11.csproj | 1 + src/Avalonia.X11/X11Window.cs | 40 +--- .../Avalonia.LinuxFramebuffer.csproj | 1 + .../Input/EvDev/EvDevBackend.cs | 38 +-- .../Input/LibInput/LibInputBackend.cs | 31 +-- .../Input/RawEventGroupingThreadingHelper.cs | 43 ++++ src/Shared/RawEventGrouping.cs | 129 ++++++++++ 15 files changed, 489 insertions(+), 104 deletions(-) create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Input/RawEventGroupingThreadingHelper.cs create mode 100644 src/Shared/RawEventGrouping.cs diff --git a/samples/ControlCatalog/Pages/PointersPage.cs b/samples/ControlCatalog/Pages/PointersPage.cs index fddc503a905..2901013cea3 100644 --- a/samples/ControlCatalog/Pages/PointersPage.cs +++ b/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 @@ class PointerInfo private Dictionary _pointers = new Dictionary(); - public PointersPage() + public PointerContactsTab() { ClipToBounds = true; } @@ -104,4 +126,196 @@ public override void Render(DrawingContext context) } } } + + 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 _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); + } + } + + } } diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index 908f4317760..49cee441d0e 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -74,6 +74,13 @@ public void RunJobs() /// /// public void RunJobs(DispatcherPriority minimumPriority) => _jobRunner.RunJobs(minimumPriority); + + /// + /// Use this method to check if there are more prioritized tasks + /// + /// + public bool HasJobsWithPriority(DispatcherPriority minimumPriority) => + _jobRunner.HasJobsWithPriority(minimumPriority); /// public Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal) diff --git a/src/Avalonia.Base/Threading/JobRunner.cs b/src/Avalonia.Base/Threading/JobRunner.cs index f2aef0414c6..4b304d44f6c 100644 --- a/src/Avalonia.Base/Threading/JobRunner.cs +++ b/src/Avalonia.Base/Threading/JobRunner.cs @@ -121,6 +121,21 @@ private void AddJob(IJob job) 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 { /// diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index a2c43013fd5..166af44d044 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/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 @@ private void ProcessRawEvent(RawPointerEventArgs e) 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 @@ private void ProcessRawEvent(RawPointerEventArgs e) 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 @@ private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, } private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, - KeyModifiers inputModifiers) + KeyModifiers inputModifiers, IReadOnlyList? intermediatePoints) { device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); @@ -283,7 +284,7 @@ private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Po 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; diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index 8c86cd46374..40495a2f0a1 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/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 @@ public class PointerEventArgs : RoutedEventArgs private readonly IVisual? _rootVisual; private readonly Point _rootVisualPosition; private readonly PointerPointProperties _properties; + private readonly IReadOnlyList? _previousPoints; public PointerEventArgs(RoutedEvent routedEvent, IInteractive? source, @@ -28,6 +30,20 @@ public PointerEventArgs(RoutedEvent routedEvent, Timestamp = timestamp; KeyModifiers = modifiers; } + + public PointerEventArgs(RoutedEvent routedEvent, + IInteractive? source, + IPointer pointer, + IVisual? rootVisual, Point rootVisualPosition, + ulong timestamp, + PointerPointProperties properties, + KeyModifiers modifiers, + IReadOnlyList? previousPoints) + : this(routedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers) + { + _previousPoints = previousPoints; + } + class EmulatedDevice : IPointerDevice { @@ -76,14 +92,16 @@ public InputModifiers InputModifiers 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 @@ public Point GetPosition(IVisual? relativeTo) public PointerPoint GetCurrentPoint(IVisual? relativeTo) => new PointerPoint(Pointer, GetPosition(relativeTo), _properties); + /// + /// Returns the PointerPoint associated with the current event + /// + /// The visual which coordinate system to use. Pass null for toplevel coordinate system + /// + public IReadOnlyList 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; + } + /// /// Returns the current pointer point properties /// diff --git a/src/Avalonia.Input/Raw/RawInputEventArgs.cs b/src/Avalonia.Input/Raw/RawInputEventArgs.cs index dcc5f27a79f..3a5ae1340f2 100644 --- a/src/Avalonia.Input/Raw/RawInputEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawInputEventArgs.cs @@ -51,6 +51,6 @@ public RawInputEventArgs(IInputDevice device, ulong timestamp, IInputRoot root) /// /// Gets the timestamp associated with the event. /// - public ulong Timestamp { get; private set; } + public ulong Timestamp { get; set; } } } diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index 62a1dd5d846..6cb8a10cf33 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Avalonia.Input.Raw { @@ -68,6 +69,12 @@ public RawPointerEventArgs( /// /// Gets the input modifiers. /// - public RawInputModifiers InputModifiers { get; private set; } + public RawInputModifiers InputModifiers { get; set; } + + /// + /// Points that were traversed by a pointer since the previous relevant event, + /// only valid for Move and TouchUpdate + /// + public IReadOnlyList? IntermediatePoints { get; set; } } } diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs index 0069aa79615..12ad182bf83 100644 --- a/src/Avalonia.Input/TouchDevice.cs +++ b/src/Avalonia.Input/TouchDevice.cs @@ -104,7 +104,7 @@ public void ProcessRawEvent(RawInputEventArgs ev) 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)); } diff --git a/src/Avalonia.X11/Avalonia.X11.csproj b/src/Avalonia.X11/Avalonia.X11.csproj index 9ba5c9d15f3..45a76bc3d68 100644 --- a/src/Avalonia.X11/Avalonia.X11.csproj +++ b/src/Avalonia.X11/Avalonia.X11.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index d745b4765b3..0f881eb91a4 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -49,15 +49,10 @@ unsafe partial class X11Window : IWindowImpl, IPopupImpl, IXI2Client, 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 _inputQueue = new Queue(); - private InputEventContainer _lastEvent; + private bool _useRenderWindow = false; public X11Window(AvaloniaX11Platform platform, IWindowImpl popupParent) { @@ -181,6 +176,8 @@ public X11Window(AvaloniaX11Platform platform, IWindowImpl popupParent) 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 @@ private void ScheduleInput(RawInputEventArgs args) 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 @@ public void Dispose() void Cleanup() { + if (_rawEventGrouper != null) + { + _rawEventGrouper.Dispose(); + _rawEventGrouper = null; + } + if (_transparencyHelper != null) { _transparencyHelper.Dispose(); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj b/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj index 6a2eb183850..5cebbb68298 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj +++ b/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj @@ -7,5 +7,6 @@ + diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs index b3fc979fcac..2eb10ae6662 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs @@ -13,15 +13,16 @@ public class EvDevBackend : IInputBackend private readonly EvDevDeviceDescription[] _deviceDescriptions; private readonly List _handlers = new List(); private int _epoll; - private Queue _inputQueue = new Queue(); private bool _isQueueHandlerTriggered; private object _lock = new object(); private Action _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 @@ unsafe void InputThread() 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 onInput) { diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index 432344955ac..15d42789d44 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -17,15 +17,15 @@ public class LibInputBackend : IInputBackend private TouchDevice _touch = new TouchDevice(); private MouseDevice _mouse = new MouseDevice(); private Point _mousePosition; - - private readonly Queue _inputQueue = new Queue(); + + private readonly RawEventGroupingThreadingHelper _inputQueue; private Action _onInput; private Dictionary _pointers = new Dictionary(); public LibInputBackend() { var ctx = libinput_path_create_context(); - + _inputQueue = new(e => _onInput?.Invoke(e)); new Thread(()=>InputThread(ctx)).Start(); } @@ -66,30 +66,7 @@ private unsafe void InputThread(IntPtr ctx) } } - 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) { diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/RawEventGroupingThreadingHelper.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/RawEventGroupingThreadingHelper.cs new file mode 100644 index 00000000000..f706f184612 --- /dev/null +++ b/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 _rawQueue = new(); + private readonly Action _queueHandler; + + public RawEventGroupingThreadingHelper(Action 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); +} \ No newline at end of file diff --git a/src/Shared/RawEventGrouping.cs b/src/Shared/RawEventGrouping.cs new file mode 100644 index 00000000000..25b4b41e566 --- /dev/null +++ b/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 _eventCallback; + private readonly Queue _inputQueue = new(); + private readonly Action _dispatchFromQueue; + readonly Dictionary _lastTouchPoints = new(); + RawInputEventArgs? _lastEvent; + + public RawEventGrouper(Action 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 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(); + ((PooledList)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(); + } +} +