diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs new file mode 100644 index 0000000000..b3fc979fca --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs @@ -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 _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; + + 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 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(); + 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()); + } + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDevice.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDevice.cs new file mode 100644 index 0000000000..e7978a6e23 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDevice.cs @@ -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 EventTypes { get; private set; } = new List(); + 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; } + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDeviceDescription.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDeviceDescription.cs new file mode 100644 index 0000000000..659748bfbb --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevDeviceDescription.cs @@ -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; + } + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreen.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreen.cs new file mode 100644 index 0000000000..b69b151c3b --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreen.cs @@ -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 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); + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreenDeviceDescription.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreenDeviceDescription.cs new file mode 100644 index 0000000000..9d006cfc31 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreenDeviceDescription.cs @@ -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 options) + { + var calibrationMatrix = Matrix.Identity; + if (options.TryGetValue("calibration", out var calibration)) + calibrationMatrix = Matrix.Parse(calibration); + + return new EvDevTouchScreenDeviceDescription { Path = path, CalibrationMatrix = calibrationMatrix }; + } + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 3c8d3b5e06..a6b70069c1 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -8,12 +8,15 @@ using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.LinuxFramebuffer; +using Avalonia.LinuxFramebuffer.Input; +using Avalonia.LinuxFramebuffer.Input.EvDev; using Avalonia.LinuxFramebuffer.Input.LibInput; using Avalonia.LinuxFramebuffer.Output; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; +using JetBrains.Annotations; namespace Avalonia.LinuxFramebuffer { @@ -58,6 +61,7 @@ namespace Avalonia.LinuxFramebuffer class LinuxFramebufferLifetime : IControlledApplicationLifetime, ISingleViewApplicationLifetime { private readonly IOutputBackend _fb; + [CanBeNull] private readonly IInputBackend _inputBackend; private TopLevel _topLevel; private readonly CancellationTokenSource _cts = new CancellationTokenSource(); public CancellationToken Token => _cts.Token; @@ -67,6 +71,12 @@ namespace Avalonia.LinuxFramebuffer _fb = fb; } + public LinuxFramebufferLifetime(IOutputBackend fb, IInputBackend input) + { + _fb = fb; + _inputBackend = input; + } + public Control MainView { get => (Control)_topLevel?.Content; @@ -74,8 +84,16 @@ namespace Avalonia.LinuxFramebuffer { if (_topLevel == null) { + var inputBackend = _inputBackend; + if (inputBackend == null) + { + if (Environment.GetEnvironmentVariable("AVALONIA_USE_EVDEV") == "1") + inputBackend = EvDevBackend.CreateFromEnvironment(); + else + inputBackend = new LibInputBackend(); + } - var tl = new EmbeddableControlRoot(new FramebufferToplevelImpl(_fb, new LibInputBackend())); + var tl = new EmbeddableControlRoot(new FramebufferToplevelImpl(_fb, inputBackend)); tl.Prepare(); _topLevel = tl; _topLevel.Renderer.Start(); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs b/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs index 18db176bcd..f71d4e9753 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs @@ -50,6 +50,19 @@ namespace Avalonia.LinuxFramebuffer public static extern IntPtr libevdev_get_name(IntPtr dev); [DllImport("libevdev.so.2", EntryPoint = "libevdev_get_abs_info", SetLastError = true)] public static extern input_absinfo* libevdev_get_abs_info(IntPtr dev, int code); + + [DllImport("libc")] + public extern static int epoll_create1(int size); + + [DllImport("libc")] + public extern static int epoll_ctl(int epfd, int op, int fd, ref epoll_event __event); + + [DllImport("libc")] + public extern static int epoll_wait(int epfd, epoll_event* events, int maxevents, int timeout); + + public const int EPOLLIN = 1; + public const int EPOLL_CTL_ADD = 1; + public const int O_NONBLOCK = 2048; } [StructLayout(LayoutKind.Sequential)] @@ -190,9 +203,22 @@ namespace Avalonia.LinuxFramebuffer [StructLayout(LayoutKind.Sequential)] struct input_event { - private IntPtr crap1, crap2; - public ushort type, code; + private IntPtr timeval1, timeval2; + public ushort _type, _code; public int value; + public EvType Type => (EvType)_type; + public EvKey Key => (EvKey)_code; + public AbsAxis Axis => (AbsAxis)_code; + + public ulong Timestamp + { + get + { + var ms = (ulong)timeval2.ToInt64() / 1000; + var s = (ulong)timeval1.ToInt64() * 1000; + return s + ms; + } + } } [StructLayout(LayoutKind.Sequential)] @@ -249,7 +275,8 @@ namespace Avalonia.LinuxFramebuffer { BTN_LEFT = 0x110, BTN_RIGHT = 0x111, - BTN_MIDDLE = 0x112 + BTN_MIDDLE = 0x112, + BTN_TOUCH = 0x14a } [StructLayout(LayoutKind.Sequential)] @@ -263,4 +290,24 @@ namespace Avalonia.LinuxFramebuffer public __s32 resolution; } + + [StructLayout(LayoutKind.Explicit)] + struct epoll_data + { + [FieldOffset(0)] + public IntPtr ptr; + [FieldOffset(0)] + public int fd; + [FieldOffset(0)] + public uint u32; + [FieldOffset(0)] + public ulong u64; + } + + [StructLayout(LayoutKind.Sequential)] + struct epoll_event + { + public uint events; + public epoll_data data; + } }