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