7 changed files with 445 additions and 4 deletions
@ -0,0 +1,141 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Raw; |
|||
using Avalonia.Threading; |
|||
using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods; |
|||
|
|||
namespace Avalonia.LinuxFramebuffer.Input.EvDev |
|||
{ |
|||
public class EvDevBackend : IInputBackend |
|||
{ |
|||
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; |
|||
|
|||
public EvDevBackend(EvDevDeviceDescription[] devices) |
|||
{ |
|||
_deviceDescriptions = devices; |
|||
} |
|||
|
|||
unsafe void InputThread() |
|||
{ |
|||
const int MaxEvents = 16; |
|||
var events = stackalloc epoll_event[MaxEvents]; |
|||
while (true) |
|||
{ |
|||
var eventCount = Math.Min(MaxEvents, epoll_wait(_epoll, events, MaxEvents, 1000)); |
|||
for (var c = 0; c < eventCount; c++) |
|||
{ |
|||
try |
|||
{ |
|||
var ev = events[c]; |
|||
var handler = _handlers[(int)ev.data.u32]; |
|||
handler.HandleEvents(); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
Console.Error.WriteLine(e.ToString()); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void OnRawEvent(RawInputEventArgs obj) |
|||
{ |
|||
lock (_lock) |
|||
{ |
|||
_inputQueue.Enqueue(obj); |
|||
TriggerQueueHandler(); |
|||
} |
|||
|
|||
} |
|||
|
|||
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) |
|||
{ |
|||
_onInput = onInput; |
|||
_epoll = epoll_create1(0); |
|||
for (var c = 0; c < _deviceDescriptions.Length; c++) |
|||
{ |
|||
var description = _deviceDescriptions[c]; |
|||
var dev = EvDevDevice.Open(description.Path); |
|||
EvDevDeviceHandler handler; |
|||
if (description is EvDevTouchScreenDeviceDescription touch) |
|||
handler = new EvDevSingleTouchScreen(dev, touch, info) { InputRoot = _inputRoot }; |
|||
else |
|||
throw new Exception("Unknown device description type " + description.GetType().FullName); |
|||
|
|||
handler.OnEvent += OnRawEvent; |
|||
_handlers.Add(handler); |
|||
|
|||
var ev = new epoll_event { events = EPOLLIN, data = { u32 = (uint)c } }; |
|||
epoll_ctl(_epoll, EPOLL_CTL_ADD, dev.Fd, ref ev); |
|||
} |
|||
|
|||
new Thread(InputThread) { IsBackground = true }.Start(); |
|||
} |
|||
|
|||
public void SetInputRoot(IInputRoot root) |
|||
{ |
|||
_inputRoot = root; |
|||
foreach (var h in _handlers) |
|||
h.InputRoot = root; |
|||
} |
|||
|
|||
|
|||
public static EvDevBackend CreateFromEnvironment() |
|||
{ |
|||
var env = Environment.GetEnvironmentVariables(); |
|||
var deviceDescriptions = new List<EvDevDeviceDescription>(); |
|||
foreach (string key in env.Keys) |
|||
{ |
|||
if (key.StartsWith("AVALONIA_EVDEV_DEVICE_")) |
|||
{ |
|||
var value = (string)env[key]; |
|||
deviceDescriptions.Add(EvDevDeviceDescription.ParseFromEnv(value)); |
|||
} |
|||
} |
|||
|
|||
if (deviceDescriptions.Count == 0) |
|||
throw new Exception( |
|||
"No device device description found, specify devices by adding AVALONIA_EVDEV_DEVICE_{name} environment variables"); |
|||
|
|||
return new EvDevBackend(deviceDescriptions.ToArray()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,65 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
namespace Avalonia.LinuxFramebuffer.Input.EvDev |
|||
{ |
|||
unsafe class EvDevDevice |
|||
{ |
|||
public int Fd { get; } |
|||
private IntPtr _dev; |
|||
public string Name { get; } |
|||
public List<EvType> EventTypes { get; private set; } = new List<EvType>(); |
|||
public input_absinfo? AbsX { get; } |
|||
public input_absinfo? AbsY { get; } |
|||
|
|||
public EvDevDevice(int fd, IntPtr dev) |
|||
{ |
|||
Fd = fd; |
|||
_dev = dev; |
|||
Name = Marshal.PtrToStringAnsi(NativeUnsafeMethods.libevdev_get_name(_dev)); |
|||
foreach (EvType type in Enum.GetValues(typeof(EvType))) |
|||
{ |
|||
if (NativeUnsafeMethods.libevdev_has_event_type(dev, type) != 0) |
|||
EventTypes.Add(type); |
|||
} |
|||
var ptr = NativeUnsafeMethods.libevdev_get_abs_info(dev, (int) AbsAxis.ABS_X); |
|||
if (ptr != null) |
|||
AbsX = *ptr; |
|||
ptr = NativeUnsafeMethods.libevdev_get_abs_info(dev, (int)AbsAxis.ABS_Y); |
|||
if (ptr != null) |
|||
AbsY = *ptr; |
|||
} |
|||
|
|||
public input_event? NextEvent() |
|||
{ |
|||
input_event ev; |
|||
if (NativeUnsafeMethods.libevdev_next_event(_dev, 2, out ev) == 0) |
|||
return ev; |
|||
return null; |
|||
} |
|||
|
|||
public static EvDevDevice Open(string device) |
|||
{ |
|||
var fd = NativeUnsafeMethods.open(device, 2048, 0); |
|||
if (fd <= 0) |
|||
throw new Exception($"Unable to open {device} code {Marshal.GetLastWin32Error()}"); |
|||
IntPtr dev; |
|||
var rc = NativeUnsafeMethods.libevdev_new_from_fd(fd, out dev); |
|||
if (rc < 0) |
|||
{ |
|||
NativeUnsafeMethods.close(fd); |
|||
throw new Exception($"Unable to initialize evdev for {device} code {Marshal.GetLastWin32Error()}"); |
|||
} |
|||
return new EvDevDevice(fd, dev); |
|||
} |
|||
} |
|||
|
|||
internal class EvDevAxisInfo |
|||
{ |
|||
public int Minimum { get; set; } |
|||
public int Maximum { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
using System; |
|||
using System.Linq; |
|||
|
|||
namespace Avalonia.LinuxFramebuffer.Input.EvDev |
|||
{ |
|||
public abstract class EvDevDeviceDescription |
|||
{ |
|||
protected internal EvDevDeviceDescription() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public string Path { get; set; } |
|||
|
|||
internal static EvDevDeviceDescription ParseFromEnv(string env) |
|||
{ |
|||
var formatEx = new ArgumentException( |
|||
"Invalid device format, expected `(path):type=(touchscreen):[calibration=m11,m12,m21,m22,m31,m32]"); |
|||
|
|||
|
|||
var items = env.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries); |
|||
if (items.Length < 2) |
|||
throw formatEx; |
|||
var path = items[0]; |
|||
var dic = items.Skip(1) |
|||
.Select(i => i.Split(new[] { '=' }, 2)) |
|||
.ToDictionary(x => x[0], x => x[1]); |
|||
|
|||
if (!dic.TryGetValue("type", out var type)) |
|||
throw formatEx; |
|||
|
|||
if (type == "touchscreen") |
|||
return EvDevTouchScreenDeviceDescription.ParseFromEnv(path, dic); |
|||
|
|||
throw formatEx; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,114 @@ |
|||
using System; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Raw; |
|||
|
|||
namespace Avalonia.LinuxFramebuffer.Input.EvDev |
|||
{ |
|||
internal class EvDevSingleTouchScreen : EvDevDeviceHandler |
|||
{ |
|||
private readonly IScreenInfoProvider _screenInfo; |
|||
private readonly int _width, _height; |
|||
private readonly Matrix _calibration; |
|||
private input_absinfo _axisX; |
|||
private input_absinfo _axisY; |
|||
private TouchDevice _device = new TouchDevice(); |
|||
|
|||
private int _currentX, _currentY; |
|||
private bool _hasMovement; |
|||
private bool? _pressAction; |
|||
|
|||
public EvDevSingleTouchScreen(EvDevDevice device, EvDevTouchScreenDeviceDescription description, |
|||
IScreenInfoProvider screenInfo) : base(device) |
|||
{ |
|||
if (device.AbsX == null || device.AbsY == null) |
|||
throw new ArgumentException("Device is not a touchscreen"); |
|||
_screenInfo = screenInfo; |
|||
|
|||
_calibration = description.CalibrationMatrix; |
|||
_axisX = device.AbsX.Value; |
|||
_axisY = device.AbsY.Value; |
|||
} |
|||
|
|||
protected override void HandleEvent(input_event ev) |
|||
{ |
|||
if (ev.Type == EvType.EV_ABS) |
|||
{ |
|||
if (ev.Axis == AbsAxis.ABS_X) |
|||
{ |
|||
_currentX = ev.value; |
|||
_hasMovement = true; |
|||
} |
|||
|
|||
if (ev.Axis == AbsAxis.ABS_Y) |
|||
{ |
|||
_currentY = ev.value; |
|||
_hasMovement = true; |
|||
} |
|||
} |
|||
|
|||
if (ev.Type == EvType.EV_KEY) |
|||
{ |
|||
if (ev.Key == EvKey.BTN_TOUCH) |
|||
{ |
|||
_pressAction = ev.value != 0; |
|||
} |
|||
} |
|||
|
|||
if (ev.Type == EvType.EV_SYN) |
|||
{ |
|||
if (_pressAction != null) |
|||
RaiseEvent(_pressAction == true ? RawPointerEventType.TouchBegin : RawPointerEventType.TouchEnd, |
|||
ev.Timestamp); |
|||
else if(_hasMovement) |
|||
RaiseEvent(RawPointerEventType.TouchUpdate, ev.Timestamp); |
|||
_hasMovement = false; |
|||
_pressAction = null; |
|||
} |
|||
} |
|||
|
|||
void RaiseEvent(RawPointerEventType type, ulong timestamp) |
|||
{ |
|||
var point = new Point(_currentX, _currentY); |
|||
|
|||
var touchWidth = _axisX.maximum - _axisX.minimum; |
|||
var touchHeight = _axisY.maximum - _axisY.minimum; |
|||
|
|||
var screenSize = _screenInfo.ScaledSize; |
|||
|
|||
// Normalize to 0-(max-min)
|
|||
point -= new Point(_axisX.minimum, _axisY.minimum); |
|||
|
|||
// Apply calibration matrix
|
|||
point *= _calibration; |
|||
|
|||
// Transform to display pixel grid
|
|||
point = new Point(point.X * screenSize.Width / touchWidth, point.Y * screenSize.Height / touchHeight); |
|||
|
|||
RaiseEvent(new RawTouchEventArgs(_device, timestamp, InputRoot, |
|||
type, point, RawInputModifiers.None, 1)); |
|||
} |
|||
} |
|||
|
|||
internal abstract class EvDevDeviceHandler |
|||
{ |
|||
public event Action<RawInputEventArgs> OnEvent; |
|||
public EvDevDeviceHandler(EvDevDevice device) |
|||
{ |
|||
Device = device; |
|||
} |
|||
|
|||
public EvDevDevice Device { get; } |
|||
public IInputRoot InputRoot { get; set; } |
|||
|
|||
public void HandleEvents() |
|||
{ |
|||
input_event? ev; |
|||
while ((ev = Device.NextEvent()) != null) |
|||
HandleEvent(ev.Value); |
|||
} |
|||
|
|||
protected void RaiseEvent(RawInputEventArgs ev) => OnEvent?.Invoke(ev); |
|||
|
|||
protected abstract void HandleEvent(input_event ev); |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.LinuxFramebuffer.Input.EvDev |
|||
{ |
|||
public sealed class EvDevTouchScreenDeviceDescription : EvDevDeviceDescription |
|||
{ |
|||
public Matrix CalibrationMatrix { get; set; } = Matrix.Identity; |
|||
|
|||
internal static EvDevTouchScreenDeviceDescription ParseFromEnv(string path, Dictionary<string, string> options) |
|||
{ |
|||
var calibrationMatrix = Matrix.Identity; |
|||
if (options.TryGetValue("calibration", out var calibration)) |
|||
calibrationMatrix = Matrix.Parse(calibration); |
|||
|
|||
return new EvDevTouchScreenDeviceDescription { Path = path, CalibrationMatrix = calibrationMatrix }; |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue