Browse Source

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>
pull/20295/head
Krzysztof Krysiński 2 months ago
committed by GitHub
parent
commit
79be86d923
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      src/Avalonia.X11/X11Window.cs
  2. 302
      src/Avalonia.X11/XI2Manager.cs
  3. 8
      src/Avalonia.X11/XIStructs.cs
  4. 2
      src/Shared/RawEventGrouping.cs

4
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()

302
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<int, List<XiEventType>> {[_pointerDevice.Id] = events});
new Dictionary<int, List<XiEventType>> { [_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<int, double> Valuators { get; }
public static RawInputModifiers ParseButtonState(int len, byte* buttons)
private void SetPenSpecificValues(KeyValuePair<int, double> 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<int, double>();
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<int, double> 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<int, double>();
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; }
}
}

8
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
{

2
src/Shared/RawEventGrouping.cs

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

Loading…
Cancel
Save