diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 2f2542a150..ab59882962 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -44,6 +44,7 @@ namespace Avalonia.X11 private bool _triggeredExpose; private IInputRoot? _inputRoot; private readonly MouseDevice _mouse; + private readonly PenDevice _pen; private readonly TouchDevice _touch; private readonly IKeyboardDevice _keyboard; private readonly ITopLevelNativeMenuExporter? _nativeMenuExporter; @@ -97,6 +98,7 @@ namespace Avalonia.X11 _overrideRedirect = _popup || overrideRedirect; _x11 = platform.Info; _mouse = Avalonia.Input.MouseDevice.Primary; + _pen = new PenDevice(); _touch = new TouchDevice(); _keyboard = platform.KeyboardDevice; @@ -1072,6 +1074,7 @@ namespace Avalonia.X11 _platform.XI2?.OnWindowDestroyed(_handle); var handle = _handle; _handle = IntPtr.Zero; + _pen.Dispose(); _touch.Dispose(); if (!fromDestroyNotification) XDestroyWindow(_x11.Display, handle); @@ -1238,6 +1241,7 @@ namespace Avalonia.X11 } public IMouseDevice MouseDevice => _mouse; + public IPenDevice PenDevice => _pen; public TouchDevice TouchDevice => _touch; public IPopupImpl? CreatePopup() diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs index ce93c9d3c9..7b74f63501 100644 --- a/src/Avalonia.X11/XI2Manager.cs +++ b/src/Avalonia.X11/XI2Manager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; using Avalonia.Input; using Avalonia.Input.Raw; using static Avalonia.X11.XLib; @@ -16,7 +17,6 @@ namespace Avalonia.X11 XiEventType.XI_ButtonRelease, XiEventType.XI_Leave, XiEventType.XI_Enter, - }; private static readonly XiEventType[] MultiTouchEventTypes = new XiEventType[] @@ -35,13 +35,14 @@ namespace Avalonia.X11 public int Id { get; } public XIValuatorClassInfo[] Valuators { get; private set; } = []; public XIScrollClassInfo[] Scrollers { get; private set; } = []; + public DeviceInfo(XIDeviceInfo info) { Id = info.Deviceid; UpdateCore(info.Classes, info.NumClasses); } - public virtual void Update(XIAnyClassInfo** classes, int num) + public virtual void Update(XIAnyClassInfo** classes, int num, int? slaveId) { UpdateCore(classes, num); } @@ -66,7 +67,7 @@ namespace Avalonia.X11 { foreach (var v in valuators) { - if (Valuators.Length > v.Key) + if (Valuators.Length > v.Key) Valuators[v.Key].Value = v.Value; } } @@ -74,13 +75,16 @@ namespace Avalonia.X11 private class PointerDeviceInfo : DeviceInfo { + private string? _currentSlaveName = null; + private bool _currentSlaveIsEraser = false; + public PointerDeviceInfo(XIDeviceInfo info, X11Info x11Info) : base(info) { _x11 = x11Info; UpdateKnownValuator(); } - + private readonly X11Info _x11; private void UpdateKnownValuator() @@ -91,6 +95,9 @@ namespace Avalonia.X11 var touchMinorAtom = XInternAtom(_x11.Display, "Abs MT Touch Minor", false); var pressureAtom = XInternAtom(_x11.Display, "Abs MT Pressure", false); + var pressureAtomPen = XInternAtom(_x11.Display, "Abs Pressure", false); + var absTiltXAtom = XInternAtom(_x11.Display, "Abs Tilt X", false); + var absTiltYAtom = XInternAtom(_x11.Display, "Abs Tilt Y", false); PressureXIValuatorClassInfo = null; TouchMajorXIValuatorClassInfo = null; @@ -98,7 +105,8 @@ namespace Avalonia.X11 foreach (var xiValuatorClassInfo in Valuators) { - if (xiValuatorClassInfo.Label == pressureAtom) + if (xiValuatorClassInfo.Label == pressureAtom || + xiValuatorClassInfo.Label == pressureAtomPen) { PressureXIValuatorClassInfo = xiValuatorClassInfo; } @@ -110,15 +118,52 @@ namespace Avalonia.X11 { TouchMinorXIValuatorClassInfo = xiValuatorClassInfo; } + else if (xiValuatorClassInfo.Label == absTiltXAtom) + { + TiltXXIValuatorClassInfo = xiValuatorClassInfo; + } + else if (xiValuatorClassInfo.Label == absTiltYAtom) + { + TiltYXIValuatorClassInfo = xiValuatorClassInfo; + } } } - public override void Update(XIAnyClassInfo** classes, int num) + public override void Update(XIAnyClassInfo** classes, int num, int? slaveId) { - base.Update(classes, num); + base.Update(classes, num, slaveId); + + if (slaveId != null) + { + _currentSlaveName = null; + _currentSlaveIsEraser = false; + var devices = (XIDeviceInfo*)XIQueryDevice(_x11.Display, + (int)XiPredefinedDeviceId.XIAllDevices, out int deviceNum); + + for (var c = 0; c < deviceNum; c++) + { + if (devices[c].Deviceid == slaveId) + { + _currentSlaveName = Marshal.PtrToStringAnsi(devices[c].Name); + _currentSlaveIsEraser = + _currentSlaveName?.IndexOf("eraser", StringComparison.OrdinalIgnoreCase) >= 0; + break; + } + } + XIFreeDeviceInfo(devices); + } + UpdateKnownValuator(); } + public bool HasPressureValuator() + { + return PressureXIValuatorClassInfo is not null; + } + + public bool IsEraser => _currentSlaveIsEraser; + public string? Name => _currentSlaveName; + public bool HasScroll(ParsedDeviceEvent ev) { foreach (var val in ev.Valuators) @@ -127,7 +172,7 @@ namespace Avalonia.X11 return false; } - + public bool HasMotion(ParsedDeviceEvent ev) { foreach (var val in ev.Valuators) @@ -140,8 +185,10 @@ namespace Avalonia.X11 public XIValuatorClassInfo? PressureXIValuatorClassInfo { get; private set; } public XIValuatorClassInfo? TouchMajorXIValuatorClassInfo { get; private set; } public XIValuatorClassInfo? TouchMinorXIValuatorClassInfo { get; private set; } + public XIValuatorClassInfo? TiltXXIValuatorClassInfo { get; private set; } + public XIValuatorClassInfo? TiltYXIValuatorClassInfo { get; private set; } } - + private readonly PointerDeviceInfo _pointerDevice; private readonly AvaloniaX11Platform _platform; @@ -157,7 +204,7 @@ namespace Avalonia.X11 { var x11 = platform.Info; - var devices = (XIDeviceInfo*) XIQueryDevice(x11.Display, + var devices = (XIDeviceInfo*)XIQueryDevice(x11.Display, (int)XiPredefinedDeviceId.XIAllMasterDevices, out int num); PointerDeviceInfo? pointerDevice = null; @@ -202,8 +249,8 @@ namespace Avalonia.X11 events.AddRange(MultiTouchEventTypes); XiSelectEvents(_x11.Display, xid, - new Dictionary> {[_pointerDevice.Id] = events}); - + new Dictionary> { [_pointerDevice.Id] = events }); + // We are taking over mouse input handling from here return XEventMask.PointerMotionMask | XEventMask.ButtonMotionMask @@ -219,16 +266,15 @@ namespace Avalonia.X11 } public void OnWindowDestroyed(IntPtr xid) => _clients.Remove(xid); - + public void OnEvent(XIEvent* xev) { if (xev->evtype == XiEventType.XI_DeviceChanged) { var changed = (XIDeviceChangedEvent*)xev; - _pointerDevice.Update(changed->Classes, changed->NumClasses); + _pointerDevice.Update(changed->Classes, changed->NumClasses, changed->Reason == XiDeviceChangeReason.XISlaveSwitch ? changed->Sourceid : null); } - if ((xev->evtype >= XiEventType.XI_ButtonPress && xev->evtype <= XiEventType.XI_Motion) || (xev->evtype >= XiEventType.XI_TouchBegin && xev->evtype <= XiEventType.XI_TouchEnd)) { @@ -258,9 +304,9 @@ namespace Avalonia.X11 { foreach (var scroller in _pointerDevice.Scrollers) { - _pointerDevice.Valuators[scroller.Number].Value = 0; + _pointerDevice.Valuators[scroller.Number].Value = 0; } - + client.ScheduleXI2Input(new RawPointerEventArgs(client.MouseDevice, (ulong)ev.time.ToInt64(), client.InputRoot, RawPointerEventType.LeaveWindow, new Point(ev.event_x, ev.event_y), buttons)); @@ -270,8 +316,8 @@ namespace Avalonia.X11 private void OnDeviceEvent(IXI2Client client, ParsedDeviceEvent ev) { - if (ev.Type == XiEventType.XI_TouchBegin - || ev.Type == XiEventType.XI_TouchUpdate + if (ev.Type == XiEventType.XI_TouchBegin + || ev.Type == XiEventType.XI_TouchUpdate || ev.Type == XiEventType.XI_TouchEnd) { var type = ev.Type == XiEventType.XI_TouchBegin ? @@ -280,22 +326,20 @@ namespace Avalonia.X11 RawPointerEventType.TouchUpdate : RawPointerEventType.TouchEnd); - var rawPointerPoint = new RawPointerPoint() - { - Position = ev.Position - }; + var rawPointerPoint = new RawPointerPoint() { Position = ev.Position }; - if (_pointerDevice.PressureXIValuatorClassInfo is {} valuatorClassInfo) + if (_pointerDevice.PressureXIValuatorClassInfo is { } valuatorClassInfo) { if (ev.Valuators.TryGetValue(valuatorClassInfo.Number, out var pressureValue)) { // In our API we use range from 0.0 to 1.0. - var pressure = (pressureValue - valuatorClassInfo.Min) / (valuatorClassInfo.Max - valuatorClassInfo.Min); + var pressure = (pressureValue - valuatorClassInfo.Min) / + (valuatorClassInfo.Max - valuatorClassInfo.Min); rawPointerPoint.Pressure = (float)pressure; } } - if(_pointerDevice.TouchMajorXIValuatorClassInfo is {} touchMajorXIValuatorClassInfo) + if (_pointerDevice.TouchMajorXIValuatorClassInfo is { } touchMajorXIValuatorClassInfo) { double? touchMajor = null; double? touchMinor = null; @@ -310,18 +354,20 @@ namespace Avalonia.X11 // As https://www.kernel.org/doc/html/latest/input/multi-touch-protocol.html says, using `screenBounds.Width` is not accurate enough. touchMajor = (touchMajorValue - touchMajorXIValuatorClassInfo.Min) / - (touchMajorXIValuatorClassInfo.Max - touchMajorXIValuatorClassInfo.Min) * screenBounds.Width; + (touchMajorXIValuatorClassInfo.Max - touchMajorXIValuatorClassInfo.Min) * + screenBounds.Width; } } if (touchMajor != null) { - if(_pointerDevice.TouchMinorXIValuatorClassInfo is {} touchMinorXIValuatorClassInfo) + if (_pointerDevice.TouchMinorXIValuatorClassInfo is { } touchMinorXIValuatorClassInfo) { if (ev.Valuators.TryGetValue(touchMinorXIValuatorClassInfo.Number, out var touchMinorValue)) { touchMinor = (touchMinorValue - touchMinorXIValuatorClassInfo.Min) / - (touchMinorXIValuatorClassInfo.Max - touchMinorXIValuatorClassInfo.Min) * screenBounds.Height; + (touchMinorXIValuatorClassInfo.Max - touchMinorXIValuatorClassInfo.Min) * + screenBounds.Height; } } @@ -351,10 +397,17 @@ namespace Avalonia.X11 if (!client.IsEnabled || (_multitouch && ev.Emulated)) return; + + var eventModifiers = ev.Modifiers; + if (_pointerDevice.IsEraser) + eventModifiers |= RawInputModifiers.PenEraser; if (ev.Type == XiEventType.XI_Motion) { Vector scrollDelta = default; + var rawPointerPoint = new RawPointerPoint() { Position = ev.Position }; + IInputDevice device = _pointerDevice.HasPressureValuator() ? client.PenDevice : client.MouseDevice; + foreach (var v in ev.Valuators) { foreach (var scroller in _pointerDevice.Scrollers) @@ -374,15 +427,15 @@ namespace Avalonia.X11 } } - + SetPenSpecificValues(v, ref rawPointerPoint); } if (scrollDelta != default) - client.ScheduleXI2Input(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp, - client.InputRoot, ev.Position, scrollDelta, ev.Modifiers)); + client.ScheduleXI2Input(new RawMouseWheelEventArgs(device, ev.Timestamp, + client.InputRoot, ev.Position, scrollDelta, eventModifiers)); if (_pointerDevice.HasMotion(ev)) - client.ScheduleXI2Input(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, - RawPointerEventType.Move, ev.Position, ev.Modifiers)); + client.ScheduleXI2Input(new RawPointerEventArgs(device, ev.Timestamp, client.InputRoot, + RawPointerEventType.Move, rawPointerPoint, eventModifiers)); } if (ev.Type == XiEventType.XI_ButtonPress && ev.Button >= 4 && ev.Button <= 7 && !ev.Emulated) @@ -395,10 +448,10 @@ namespace Avalonia.X11 7 => new Vector(-1, 0), _ => (Vector?)null }; - + if (scrollDelta.HasValue) client.ScheduleXI2Input(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp, - client.InputRoot, ev.Position, scrollDelta.Value, ev.Modifiers)); + client.InputRoot, ev.Position, scrollDelta.Value, eventModifiers)); } if (ev.Type == XiEventType.XI_ButtonPress || ev.Type == XiEventType.XI_ButtonRelease) @@ -413,87 +466,134 @@ namespace Avalonia.X11 9 => down ? RawPointerEventType.XButton2Down : RawPointerEventType.XButton2Up, _ => (RawPointerEventType?)null }; + if (type.HasValue) - client.ScheduleXI2Input(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, - type.Value, ev.Position, ev.Modifiers)); + { + IInputDevice device = _pointerDevice.HasPressureValuator() ? client.PenDevice : client.MouseDevice; + var pointerPoint = new RawPointerPoint() { Position = ev.Position }; + + SetPenSpecificValues(ev, ref pointerPoint); + + client.ScheduleXI2Input(new RawPointerEventArgs(device, ev.Timestamp, client.InputRoot, + type.Value, pointerPoint, eventModifiers)); + } } - + _pointerDevice.UpdateValuators(ev.Valuators); } - } + + private void SetPenSpecificValues(ParsedDeviceEvent ev, ref RawPointerPoint pointerPoint) + { + foreach (var evValuator in ev.Valuators) + { + SetPenSpecificValues(evValuator, ref pointerPoint); + } + } - internal unsafe class ParsedDeviceEvent - { - public XiEventType Type { get; } - public RawInputModifiers Modifiers { get; } - public ulong Timestamp { get; } - public Point Position { get; } - public Point RootPosition { get; } - public int Button { get; set; } - public int Detail { get; set; } - public bool Emulated { get; set; } - public Dictionary Valuators { get; } - - public static RawInputModifiers ParseButtonState(int len, byte* buttons) + private void SetPenSpecificValues(KeyValuePair item, ref RawPointerPoint rawPointerPoint) { - RawInputModifiers rv = default; - if (len > 0) + if (_pointerDevice.PressureXIValuatorClassInfo is { } valuatorClassInfo) + { + if (item.Key == valuatorClassInfo.Number) + { + var pressure = (item.Value - valuatorClassInfo.Min) / + (valuatorClassInfo.Max - valuatorClassInfo.Min); + rawPointerPoint.Pressure = (float)pressure; + } + } + + if (_pointerDevice.TiltXXIValuatorClassInfo is { } tiltXValuatorClassInfo) + { + if (item.Key == tiltXValuatorClassInfo.Number) + { + rawPointerPoint.XTilt = (float)item.Value; + } + } + + if (_pointerDevice.TiltYXIValuatorClassInfo is { } tiltYValuatorClassInfo) { - if (XIMaskIsSet(buttons, 1)) - rv |= RawInputModifiers.LeftMouseButton; - if (XIMaskIsSet(buttons, 2)) - rv |= RawInputModifiers.MiddleMouseButton; - if (XIMaskIsSet(buttons, 3)) - rv |= RawInputModifiers.RightMouseButton; - if (len > 1) + if (item.Key == tiltYValuatorClassInfo.Number) { - if (XIMaskIsSet(buttons, 8)) - rv |= RawInputModifiers.XButton1MouseButton; - if (XIMaskIsSet(buttons, 9)) - rv |= RawInputModifiers.XButton2MouseButton; + rawPointerPoint.YTilt = (float)item.Value; } } - return rv; } - - public ParsedDeviceEvent(XIDeviceEvent* ev) + + internal unsafe class ParsedDeviceEvent { - Type = ev->evtype; - Timestamp = (ulong)ev->time.ToInt64(); - var state = (XModifierMask)ev->mods.Effective; - if (state.HasAllFlags(XModifierMask.ShiftMask)) - Modifiers |= RawInputModifiers.Shift; - if (state.HasAllFlags(XModifierMask.ControlMask)) - Modifiers |= RawInputModifiers.Control; - if (state.HasAllFlags(XModifierMask.Mod1Mask)) - Modifiers |= RawInputModifiers.Alt; - if (state.HasAllFlags(XModifierMask.Mod4Mask)) - Modifiers |= RawInputModifiers.Meta; - - Modifiers |= ParseButtonState(ev->buttons.MaskLen, ev->buttons.Mask); - - Valuators = new Dictionary(); - Position = new Point(ev->event_x, ev->event_y); - RootPosition = new Point(ev->root_x, ev->root_y); - var values = ev->valuators.Values; - if(ev->valuators.Mask != null) - for (var c = 0; c < ev->valuators.MaskLen * 8; c++) - if (XIMaskIsSet(ev->valuators.Mask, c)) - Valuators[c] = *values++; - - if (Type == XiEventType.XI_ButtonPress || Type == XiEventType.XI_ButtonRelease) - Button = ev->detail; - Detail = ev->detail; - Emulated = ev->flags.HasAllFlags(XiDeviceEventFlags.XIPointerEmulated); + public XiEventType Type { get; } + public RawInputModifiers Modifiers { get; } + public ulong Timestamp { get; } + public Point Position { get; } + public Point RootPosition { get; } + public int Button { get; set; } + public int Detail { get; set; } + public bool Emulated { get; set; } + public Dictionary Valuators { get; } + + public static RawInputModifiers ParseButtonState(int len, byte* buttons) + { + RawInputModifiers rv = default; + if (len > 0) + { + if (XIMaskIsSet(buttons, 1)) + rv |= RawInputModifiers.LeftMouseButton; + if (XIMaskIsSet(buttons, 2)) + rv |= RawInputModifiers.MiddleMouseButton; + if (XIMaskIsSet(buttons, 3)) + rv |= RawInputModifiers.RightMouseButton; + if (len > 1) + { + if (XIMaskIsSet(buttons, 8)) + rv |= RawInputModifiers.XButton1MouseButton; + if (XIMaskIsSet(buttons, 9)) + rv |= RawInputModifiers.XButton2MouseButton; + } + } + + return rv; + } + + public ParsedDeviceEvent(XIDeviceEvent* ev) + { + Type = ev->evtype; + Timestamp = (ulong)ev->time.ToInt64(); + var state = (XModifierMask)ev->mods.Effective; + if (state.HasAllFlags(XModifierMask.ShiftMask)) + Modifiers |= RawInputModifiers.Shift; + if (state.HasAllFlags(XModifierMask.ControlMask)) + Modifiers |= RawInputModifiers.Control; + if (state.HasAllFlags(XModifierMask.Mod1Mask)) + Modifiers |= RawInputModifiers.Alt; + if (state.HasAllFlags(XModifierMask.Mod4Mask)) + Modifiers |= RawInputModifiers.Meta; + + Modifiers |= ParseButtonState(ev->buttons.MaskLen, ev->buttons.Mask); + + Valuators = new Dictionary(); + Position = new Point(ev->event_x, ev->event_y); + RootPosition = new Point(ev->root_x, ev->root_y); + var values = ev->valuators.Values; + if (ev->valuators.Mask != null) + for (var c = 0; c < ev->valuators.MaskLen * 8; c++) + if (XIMaskIsSet(ev->valuators.Mask, c)) + Valuators[c] = *values++; + + if (Type == XiEventType.XI_ButtonPress || Type == XiEventType.XI_ButtonRelease) + Button = ev->detail; + Detail = ev->detail; + Emulated = ev->flags.HasAllFlags(XiDeviceEventFlags.XIPointerEmulated); + } } } internal interface IXI2Client { - bool IsEnabled { get; } - IInputRoot InputRoot { get; } - void ScheduleXI2Input(RawInputEventArgs args); - IMouseDevice MouseDevice { get; } - TouchDevice TouchDevice { get; } + bool IsEnabled { get; } + IInputRoot InputRoot { get; } + void ScheduleXI2Input(RawInputEventArgs args); + IMouseDevice MouseDevice { get; } + IPenDevice PenDevice { get; } + TouchDevice TouchDevice { get; } } } diff --git a/src/Avalonia.X11/XIStructs.cs b/src/Avalonia.X11/XIStructs.cs index 9a1ed01764..5bef1d0d99 100644 --- a/src/Avalonia.X11/XIStructs.cs +++ b/src/Avalonia.X11/XIStructs.cs @@ -205,7 +205,7 @@ namespace Avalonia.X11 public IntPtr Time; public int Deviceid; /* id of the device that changed */ public int Sourceid; /* Source for the new classes. */ - public int Reason; /* Reason for the change */ + public XiDeviceChangeReason Reason; /* Reason for the change */ public int NumClasses; public XIAnyClassInfo** Classes; /* same as in XIDeviceInfo */ } @@ -272,6 +272,12 @@ namespace Avalonia.X11 XIPointerEmulated = (1 << 16) } + internal enum XiDeviceChangeReason : int + { + XISlaveSwitch = 1, + XIDeviceChange = 2 + } + [StructLayout(LayoutKind.Sequential)] internal unsafe struct XIEvent { diff --git a/src/Shared/RawEventGrouping.cs b/src/Shared/RawEventGrouping.cs index cdcd5af142..936f7bf822 100644 --- a/src/Shared/RawEventGrouping.cs +++ b/src/Shared/RawEventGrouping.cs @@ -180,7 +180,7 @@ internal class RawEventGrouper : IDisposable { last.IntermediatePoints ??= new Lazy?>(s_getPooledListDelegate); - ((PooledList)last.IntermediatePoints.Value!).Add(new RawPointerPoint { Position = last.Position }); + ((PooledList)last.IntermediatePoints.Value!).Add(new RawPointerPoint { Position = last.Position, Pressure = last.Point.Pressure, ContactRect = last.Point.ContactRect, Twist = last.Point.Twist, XTilt = last.Point.XTilt, YTilt = last.Point.YTilt }); last.Position = current.Position; last.Timestamp = current.Timestamp; last.InputModifiers = current.InputModifiers;