Skip to content

feat: Support ContactRect in X11 and Windows platform #16498

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Oct 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Avalonia.Base/Input/PenDevice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ private void ProcessRawEvent(RawPointerEventArgs e)
}

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;
Expand Down
16 changes: 13 additions & 3 deletions src/Avalonia.Base/Input/PointerPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ public PointerPoint(IPointer pointer, Point position, PointerPointProperties pro
/// </summary>
public record struct PointerPointProperties
{
/// <summary>
/// Gets the bounding rectangle of the contact area (typically from touch input).
/// </summary>
public Rect ContactRect { get; }

/// <summary>
/// Gets a value that indicates whether the pointer input was triggered by the primary action mode of an input device.
/// </summary>
Expand Down Expand Up @@ -155,17 +160,22 @@ public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kin
}

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

Expand Down
8 changes: 8 additions & 0 deletions src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,14 @@ public record struct RawPointerPoint
/// <inheritdoc cref="PointerPointProperties.YTilt" />
public float YTilt { get; set; }

/// <inheritdoc cref="PointerPointProperties.ContactRect" />
public Rect ContactRect
{
get => _contactRect ?? new Rect(Position, new Size());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should be new Size(1,1) by default I believe

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maxkatz6 Sorry, I do not think so. I think the size should be zero here as the WinUI do.

set => _contactRect = value;
}

private Rect? _contactRect;

public RawPointerPoint()
{
Expand Down
4 changes: 2 additions & 2 deletions src/Avalonia.X11/X11Platform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ internal class AvaloniaX11Platform : IWindowingPlatform
{
private Lazy<KeyboardDevice> _keyboardDevice = new Lazy<KeyboardDevice>(() => new KeyboardDevice());
public KeyboardDevice KeyboardDevice => _keyboardDevice.Value;
public Dictionary<IntPtr, X11PlatformThreading.EventHandler> Windows =
public Dictionary<IntPtr, X11PlatformThreading.EventHandler> Windows { get; } =
new Dictionary<IntPtr, X11PlatformThreading.EventHandler>();
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; }
Expand Down
86 changes: 84 additions & 2 deletions src/Avalonia.X11/XI2Manager.cs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -97,13 +101,15 @@ public bool HasMotion(ParsedDeviceEvent ev)
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++)
{
Expand All @@ -118,8 +124,31 @@ public bool Init(AvaloniaX11Platform platform)

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

/*
Expand Down Expand Up @@ -256,6 +285,57 @@ private void OnDeviceEvent(IXI2Client client, ParsedDeviceEvent ev)
}
}

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;
Expand Down Expand Up @@ -340,6 +420,7 @@ internal unsafe class ParsedDeviceEvent
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; }
Expand Down Expand Up @@ -385,6 +466,7 @@ public ParsedDeviceEvent(XIDeviceEvent* ev)

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++)
Expand Down
60 changes: 57 additions & 3 deletions src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -436,14 +436,46 @@ protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam,
{
foreach (var touchInput in touchInputs)
{
var position = PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to drop WM_TOUCH handling for ContactRect, as it's not going to be used anywhere on modern devices. WM_POINTER is the main way we handle touch/pen input.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maxkatz6 There is the other issues in avalonia, that the RegisterTouchWindow will be call event enable the WM_POINTER.

And the win7 system still use the WM_TOUCH now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't support win7 at this point.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, we try to keep Avalonia compatible with Win7, but we don't go extra miles to support some specific features. And if some code is only used on Win7, we don't really need it, as it only adds maintenance cost.

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) ?
RawPointerEventType.TouchEnd :
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));
}
Expand Down Expand Up @@ -1053,13 +1085,35 @@ private RawPointerPoint CreateRawPointerPoint(POINTER_TOUCH_INFO info)
{
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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addition: The behavior here is identical to that of WinUI 3, even when the DPI is not set to 100%.

}

return pointerPoint;
}
private RawPointerPoint CreateRawPointerPoint(POINTER_PEN_INFO info)
{
Expand Down
Loading