Browse Source

Merge pull request #7413 from AvaloniaUI/feature/intermediate-points

Added GetIntermediatePoints support for X11, libinput and evdev
# Conflicts:
#	src/Avalonia.Base/Threading/JobRunner.cs
release/0.10.13
Max Katz 4 years ago
committed by Dan Walmsley
parent
commit
8b3b65496f
  1. 220
      samples/ControlCatalog/Pages/PointersPage.cs
  2. 7
      src/Avalonia.Base/Threading/Dispatcher.cs
  3. 17
      src/Avalonia.Base/Threading/JobRunner.cs
  4. 11
      src/Avalonia.Input/MouseDevice.cs
  5. 44
      src/Avalonia.Input/PointerEventArgs.cs
  6. 2
      src/Avalonia.Input/Raw/RawInputEventArgs.cs
  7. 9
      src/Avalonia.Input/Raw/RawPointerEventArgs.cs
  8. 2
      src/Avalonia.Input/TouchDevice.cs
  9. 1
      src/Avalonia.X11/Avalonia.X11.csproj
  10. 39
      src/Avalonia.X11/X11Window.cs
  11. 1
      src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj
  12. 38
      src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs
  13. 31
      src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs
  14. 43
      src/Linux/Avalonia.LinuxFramebuffer/Input/RawEventGroupingThreadingHelper.cs
  15. 129
      src/Shared/RawEventGrouping.cs

220
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
src/Avalonia.Base/Threading/Dispatcher.cs

@ -78,6 +78,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)

17
src/Avalonia.Base/Threading/JobRunner.cs

@ -109,7 +109,22 @@ 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>

11
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;

44
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>

2
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; }
}
}

9
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; }
}
}

2
src/Avalonia.Input/TouchDevice.cs

@ -107,7 +107,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
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>

39
src/Avalonia.X11/X11Window.cs

@ -50,13 +50,7 @@ namespace Avalonia.X11
private double? _scalingOverride;
private bool _disabled;
private TransparencyHelper _transparencyHelper;
class InputEventContainer
{
public RawInputEventArgs Event;
}
private readonly Queue<InputEventContainer> _inputQueue = new Queue<InputEventContainer>();
private InputEventContainer _lastEvent;
private RawEventGrouper _rawEventGrouper;
private bool _useRenderWindow = false;
public X11Window(AvaloniaX11Platform platform, IWindowImpl popupParent)
{
@ -180,6 +174,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);
@ -721,33 +717,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);
}
@ -775,6 +752,12 @@ namespace Avalonia.X11
void Cleanup()
{
if (_rawEventGrouper != null)
{
_rawEventGrouper.Dispose();
_rawEventGrouper = null;
}
if (_transparencyHelper != null)
{
_transparencyHelper.Dispose();

1
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>

38
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)
{

31
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
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
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();
}
}
Loading…
Cancel
Save