diff --git a/src/Avalonia.Base/Input/PenDevice.cs b/src/Avalonia.Base/Input/PenDevice.cs
index f8cb713e73..c98e69de30 100644
--- a/src/Avalonia.Base/Input/PenDevice.cs
+++ b/src/Avalonia.Base/Input/PenDevice.cs
@@ -52,7 +52,7 @@ namespace Avalonia.Input
}
var props = new PointerPointProperties(e.InputModifiers, e.Type.ToUpdateKind(),
- e.Point.Twist, e.Point.Pressure, e.Point.XTilt, e.Point.YTilt);
+ e.Point.Twist, e.Point.Pressure, e.Point.XTilt, e.Point.YTilt, e.Point.ContactRect);
var keyModifiers = e.InputModifiers.ToKeyModifiers();
bool shouldReleasePointer = false;
diff --git a/src/Avalonia.Base/Input/PointerPoint.cs b/src/Avalonia.Base/Input/PointerPoint.cs
index f5310934d1..5ae7005e5f 100644
--- a/src/Avalonia.Base/Input/PointerPoint.cs
+++ b/src/Avalonia.Base/Input/PointerPoint.cs
@@ -35,6 +35,11 @@ namespace Avalonia.Input
///
public record struct PointerPointProperties
{
+ ///
+ /// Gets the bounding rectangle of the contact area (typically from touch input).
+ ///
+ public Rect ContactRect { get; }
+
///
/// Gets a value that indicates whether the pointer input was triggered by the primary action mode of an input device.
///
@@ -155,17 +160,22 @@ namespace Avalonia.Input
}
public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind,
- float twist, float pressure, float xTilt, float yTilt
- ) : this (modifiers, kind)
+ float twist, float pressure, float xTilt, float yTilt) : this(modifiers, kind, twist, pressure, xTilt, yTilt, default)
+ {
+ }
+
+ public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind,
+ float twist, float pressure, float xTilt, float yTilt, Rect contactRect) : this(modifiers, kind)
{
Twist = twist;
Pressure = pressure;
XTilt = xTilt;
YTilt = yTilt;
+ ContactRect = contactRect;
}
internal PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind, RawPointerPoint rawPoint)
- : this(modifiers, kind, rawPoint.Twist, rawPoint.Pressure, rawPoint.XTilt, rawPoint.YTilt)
+ : this(modifiers, kind, rawPoint.Twist, rawPoint.Pressure, rawPoint.XTilt, rawPoint.YTilt, rawPoint.ContactRect)
{
}
diff --git a/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
index 1a13d37112..1d5d0a7822 100644
--- a/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
+++ b/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
@@ -143,6 +143,14 @@ namespace Avalonia.Input.Raw
///
public float YTilt { get; set; }
+ ///
+ public Rect ContactRect
+ {
+ get => _contactRect ?? new Rect(Position, new Size());
+ set => _contactRect = value;
+ }
+
+ private Rect? _contactRect;
public RawPointerPoint()
{
diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs
index 582608045f..64ff98e7c7 100644
--- a/src/Avalonia.X11/X11Platform.cs
+++ b/src/Avalonia.X11/X11Platform.cs
@@ -26,9 +26,9 @@ namespace Avalonia.X11
{
private Lazy _keyboardDevice = new Lazy(() => new KeyboardDevice());
public KeyboardDevice KeyboardDevice => _keyboardDevice.Value;
- public Dictionary Windows =
+ public Dictionary Windows { get; } =
new Dictionary();
- public XI2Manager XI2;
+ public XI2Manager XI2 { get; private set; }
public X11Info Info { get; private set; }
public X11Screens X11Screens { get; private set; }
public Compositor Compositor { get; private set; }
diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs
index e293e6f135..d105fbaaad 100644
--- a/src/Avalonia.X11/XI2Manager.cs
+++ b/src/Avalonia.X11/XI2Manager.cs
@@ -1,8 +1,12 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
+using System.Runtime.InteropServices;
+
using Avalonia.Input;
using Avalonia.Input.Raw;
+using Avalonia.Platform;
using static Avalonia.X11.XLib;
namespace Avalonia.X11
@@ -97,13 +101,15 @@ namespace Avalonia.X11
private AvaloniaX11Platform _platform;
private XIValuatorClassInfo? _pressureXIValuatorClassInfo;
+ private XIValuatorClassInfo? _touchMajorXIValuatorClassInfo;
+ private XIValuatorClassInfo? _touchMinorXIValuatorClassInfo;
public bool Init(AvaloniaX11Platform platform)
{
_platform = platform;
_x11 = platform.Info;
_multitouch = platform.Options?.EnableMultiTouch ?? true;
- var devices =(XIDeviceInfo*) XIQueryDevice(_x11.Display,
+ var devices = (XIDeviceInfo*) XIQueryDevice(_x11.Display,
(int)XiPredefinedDeviceId.XIAllMasterDevices, out int num);
for (var c = 0; c < num; c++)
{
@@ -118,8 +124,31 @@ namespace Avalonia.X11
if (_multitouch)
{
+ // ABS_MT_TOUCH_MAJOR ABS_MT_TOUCH_MINOR
+ // https://www.kernel.org/doc/html/latest/input/multi-touch-protocol.html
+ var touchMajorAtom = XInternAtom(_x11.Display, "Abs MT Touch Major", false);
+ var touchMinorAtom = XInternAtom(_x11.Display, "Abs MT Touch Minor", false);
+
var pressureAtom = XInternAtom(_x11.Display, "Abs MT Pressure", false);
- _pressureXIValuatorClassInfo = _pointerDevice.Valuators.FirstOrDefault(t => t.Label == pressureAtom);
+
+ var pressureXIValuatorClassInfo = _pointerDevice.Valuators.FirstOrDefault(t => t.Label == pressureAtom);
+ if (pressureXIValuatorClassInfo.Label == pressureAtom)
+ {
+ // Why check twice? The XIValuatorClassInfo is struct, so the FirstOrDefault will return the default struct when not found.
+ _pressureXIValuatorClassInfo = pressureXIValuatorClassInfo;
+ }
+
+ var touchMajorXIValuatorClassInfo = _pointerDevice.Valuators.FirstOrDefault(t => t.Label == touchMajorAtom);
+ if (touchMajorXIValuatorClassInfo.Label == touchMajorAtom)
+ {
+ _touchMajorXIValuatorClassInfo = touchMajorXIValuatorClassInfo;
+ }
+
+ var touchMinorXIValuatorClassInfo = _pointerDevice.Valuators.FirstOrDefault(t => t.Label == touchMinorAtom);
+ if (touchMinorXIValuatorClassInfo.Label == touchMinorAtom)
+ {
+ _touchMinorXIValuatorClassInfo = touchMinorXIValuatorClassInfo;
+ }
}
/*
@@ -256,6 +285,57 @@ namespace Avalonia.X11
}
}
+ if(_touchMajorXIValuatorClassInfo is {} touchMajorXIValuatorClassInfo)
+ {
+ double? touchMajor = null;
+ double? touchMinor = null;
+ PixelRect screenBounds = default;
+ if (ev.Valuators.TryGetValue(touchMajorXIValuatorClassInfo.Number, out var touchMajorValue))
+ {
+ var pixelPoint = new PixelPoint((int)ev.RootPosition.X, (int)ev.RootPosition.Y);
+ var screen = _platform.Screens.ScreenFromPoint(pixelPoint);
+ var screenBoundsFromPoint = screen?.Bounds;
+ Debug.Assert(screenBoundsFromPoint != null);
+ if (screenBoundsFromPoint != null)
+ {
+ screenBounds = screenBoundsFromPoint.Value;
+
+ // 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;
+ }
+ }
+
+ if (touchMajor != null)
+ {
+ if(_touchMinorXIValuatorClassInfo is {} touchMinorXIValuatorClassInfo)
+ {
+ if (ev.Valuators.TryGetValue(touchMinorXIValuatorClassInfo.Number, out var touchMinorValue))
+ {
+ touchMinor = (touchMinorValue - touchMinorXIValuatorClassInfo.Min) /
+ (touchMinorXIValuatorClassInfo.Max - touchMinorXIValuatorClassInfo.Min) * screenBounds.Height;
+ }
+ }
+
+ if (touchMinor == null)
+ {
+ touchMinor = touchMajor;
+ }
+
+ var center = ev.Position;
+ var leftX = center.X - touchMajor.Value / 2;
+ var topY = center.Y - touchMinor.Value / 2;
+
+ rawPointerPoint.ContactRect = new Rect
+ (
+ leftX,
+ topY,
+ touchMajor.Value,
+ touchMinor.Value
+ );
+ }
+ }
+
client.ScheduleXI2Input(new RawTouchEventArgs(client.TouchDevice,
ev.Timestamp, client.InputRoot, type, rawPointerPoint, ev.Modifiers, ev.Detail));
return;
@@ -340,6 +420,7 @@ namespace Avalonia.X11
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; }
@@ -385,6 +466,7 @@ namespace Avalonia.X11
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++)
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
index 9158c6db6c..caa2d33a25 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
@@ -436,6 +436,38 @@ namespace Avalonia.Win32
{
foreach (var touchInput in touchInputs)
{
+ var position = PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100));
+ var rawPointerPoint = new RawPointerPoint()
+ {
+ Position = position,
+ };
+
+ // Try to get the touch width and height.
+ // See https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-touchinput
+ // > The width of the touch contact area in hundredths of a pixel in physical screen coordinates. This value is only valid if the dwMask member has the TOUCHEVENTFMASK_CONTACTAREA flag set.
+ const int TOUCHEVENTFMASK_CONTACTAREA = 0x0004; // Known as TOUCHINPUTMASKF_CONTACTAREA in the docs.
+ if ((touchInput.Mask & TOUCHEVENTFMASK_CONTACTAREA) != 0)
+ {
+ var centerX = touchInput.X / 100.0;
+ var centerY = touchInput.Y / 100.0;
+
+ var rightX = centerX + touchInput.CxContact / 100.0 /
+ 2 /*The center X add the half width is the right X*/;
+ var bottomY = centerY + touchInput.CyContact / 100.0 /
+ 2 /*The center Y add the half height is the bottom Y*/;
+
+ var bottomRightPixelPoint =
+ new PixelPoint((int)rightX, (int)bottomY);
+ var bottomRightPosition = PointToClient(bottomRightPixelPoint);
+
+ var centerPosition = position;
+ var halfWidth = bottomRightPosition.X - centerPosition.X;
+ var halfHeight = bottomRightPosition.Y - centerPosition.Y;
+ var leftTopPosition = new Point(centerPosition.X - halfWidth, centerPosition.Y - halfHeight);
+
+ rawPointerPoint.ContactRect = new Rect(leftTopPosition, bottomRightPosition);
+ }
+
input.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time,
Owner,
touchInput.Flags.HasAllFlags(TouchInputFlags.TOUCHEVENTF_UP) ?
@@ -443,7 +475,7 @@ namespace Avalonia.Win32
touchInput.Flags.HasAllFlags(TouchInputFlags.TOUCHEVENTF_DOWN) ?
RawPointerEventType.TouchBegin :
RawPointerEventType.TouchUpdate,
- PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)),
+ rawPointerPoint,
WindowsKeyboardDevice.Instance.Modifiers,
touchInput.Id));
}
@@ -1053,13 +1085,35 @@ namespace Avalonia.Win32
{
var pointerInfo = info.pointerInfo;
var point = PointToClient(new PixelPoint(pointerInfo.ptPixelLocationX, pointerInfo.ptPixelLocationY));
- return new RawPointerPoint
+
+ var pointerPoint = new RawPointerPoint
{
Position = point,
// POINTER_PEN_INFO.pressure is normalized to a range between 0 and 1024, with 512 as a default.
// But in our API we use range from 0.0 to 1.0.
- Pressure = info.pressure / 1024f
+ Pressure = info.pressure / 1024f,
};
+
+ // See https://learn.microsoft.com/en-us/windows/win32/inputmsg/touch-mask-constants
+ // > TOUCH_MASK_CONTACTAREA: rcContact of the POINTER_TOUCH_INFO structure is valid.
+ if ((info.touchMask & TouchMask.TOUCH_MASK_CONTACTAREA) != 0)
+ {
+ // See https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-pointer_touch_info
+ // > The predicted screen coordinates of the contact area, in pixels. By default, if the device does not report a contact area, this field defaults to a 0-by-0 rectangle centered around the pointer location.
+ var leftTopPixelPoint =
+ new PixelPoint(info.rcContactLeft, info.rcContactTop);
+ var leftTopPosition = PointToClient(leftTopPixelPoint);
+
+ var bottomRightPixelPoint =
+ new PixelPoint(info.rcContactRight, info.rcContactBottom);
+ var bottomRightPosition = PointToClient(bottomRightPixelPoint);
+
+ // Why not use ptPixelLocationX and ptPixelLocationY to as leftTopPosition?
+ // Because ptPixelLocationX and ptPixelLocationY will be the center of the contact area.
+ pointerPoint.ContactRect = new Rect(leftTopPosition, bottomRightPosition);
+ }
+
+ return pointerPoint;
}
private RawPointerPoint CreateRawPointerPoint(POINTER_PEN_INFO info)
{