Added basic Pen support for X11 (#19910)

* Added surface dispose to DrawingSurfaceDemoBase

* Added pen support for x11

* Fixed intermediate points defaulting properties

* Detect the current device being an eraser

* Formatting fixes

* Removed spaces

---------

Co-authored-by: flabbet <flabbet@fedora>
Co-authored-by: Nikita Tsukanov <keks9n@gmail.com>
This commit is contained in:
Krzysztof Krysiński
2025-12-16 11:12:00 +01:00
committed by GitHub
parent 7a340a6579
commit 79be86d923
4 changed files with 216 additions and 106 deletions
+4
View File
@@ -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()
+122 -22
View File
@@ -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);
}
@@ -74,6 +75,9 @@ 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;
@@ -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)
@@ -140,6 +185,8 @@ 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;
@@ -225,10 +272,9 @@ namespace Avalonia.X11
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))
{
@@ -280,17 +326,15 @@ 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 (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;
}
}
@@ -310,7 +354,8 @@ 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;
}
}
@@ -321,7 +366,8 @@ namespace Avalonia.X11
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;
}
}
@@ -352,9 +398,16 @@ 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)
@@ -398,7 +451,7 @@ namespace Avalonia.X11
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,13 +466,57 @@ 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);
}
}
private void SetPenSpecificValues(KeyValuePair<int, double> item, ref RawPointerPoint rawPointerPoint)
{
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 (item.Key == tiltYValuatorClassInfo.Number)
{
rawPointerPoint.YTilt = (float)item.Value;
}
}
}
internal unsafe class ParsedDeviceEvent
@@ -453,6 +550,7 @@ namespace Avalonia.X11
rv |= RawInputModifiers.XButton2MouseButton;
}
}
return rv;
}
@@ -487,6 +585,7 @@ namespace Avalonia.X11
Emulated = ev->flags.HasAllFlags(XiDeviceEventFlags.XIPointerEmulated);
}
}
}
internal interface IXI2Client
{
@@ -494,6 +593,7 @@ namespace Avalonia.X11
IInputRoot InputRoot { get; }
void ScheduleXI2Input(RawInputEventArgs args);
IMouseDevice MouseDevice { get; }
IPenDevice PenDevice { get; }
TouchDevice TouchDevice { get; }
}
}
+7 -1
View File
@@ -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
{
+1 -1
View File
@@ -180,7 +180,7 @@ internal class RawEventGrouper : IDisposable
{
last.IntermediatePoints ??= new Lazy<IReadOnlyList<RawPointerPoint>?>(s_getPooledListDelegate);
((PooledList<RawPointerPoint>)last.IntermediatePoints.Value!).Add(new RawPointerPoint { Position = last.Position });
((PooledList<RawPointerPoint>)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;