Skip to content

Commit 27ad98b

Browse files
lindexiwalterlv
authored andcommitted
feat: Support ContactRect in X11 and Windows platform (AvaloniaUI#16498)
* feat: Support ContactRect in X11 platform * Provide compat value for ContactRect property * Using touch position to get the screen. * Try get the correct screen * Remove the code of log. * Improve performance avoid the GC stress. * Add a second overload in PointerPointProperties to avoid break API change * Support touch size in WM_Touch * Support get ContactRect in WM_POINTER * Fix the coordinate of touch contact area * Fix the calcuate of WM_Touch contact area * Fix the X11 area contact * Fix the touch major and minor default value is not null. The XIValuatorClassInfo is struct, so the FirstOrDefault will return the default struct when not found.
1 parent ad10939 commit 27ad98b

File tree

6 files changed

+184
-30
lines changed

6 files changed

+184
-30
lines changed

src/Avalonia.Base/Input/PenDevice.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ private void ProcessRawEvent(RawPointerEventArgs e)
5252
}
5353

5454
var props = new PointerPointProperties(e.InputModifiers, e.Type.ToUpdateKind(),
55-
e.Point.Twist, e.Point.Pressure, e.Point.XTilt, e.Point.YTilt);
55+
e.Point.Twist, e.Point.Pressure, e.Point.XTilt, e.Point.YTilt, e.Point.ContactRect);
5656
var keyModifiers = e.InputModifiers.ToKeyModifiers();
5757

5858
bool shouldReleasePointer = false;

src/Avalonia.Base/Input/PointerPoint.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ public PointerPoint(IPointer pointer, Point position, PointerPointProperties pro
3535
/// </summary>
3636
public record struct PointerPointProperties
3737
{
38+
/// <summary>
39+
/// Gets the bounding rectangle of the contact area (typically from touch input).
40+
/// </summary>
41+
public Rect ContactRect { get; }
42+
3843
/// <summary>
3944
/// Gets a value that indicates whether the pointer input was triggered by the primary action mode of an input device.
4045
/// </summary>
@@ -155,17 +160,22 @@ public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kin
155160
}
156161

157162
public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind,
158-
float twist, float pressure, float xTilt, float yTilt
159-
) : this (modifiers, kind)
163+
float twist, float pressure, float xTilt, float yTilt) : this(modifiers, kind, twist, pressure, xTilt, yTilt, default)
164+
{
165+
}
166+
167+
public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind,
168+
float twist, float pressure, float xTilt, float yTilt, Rect contactRect) : this(modifiers, kind)
160169
{
161170
Twist = twist;
162171
Pressure = pressure;
163172
XTilt = xTilt;
164173
YTilt = yTilt;
174+
ContactRect = contactRect;
165175
}
166176

167177
internal PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind, RawPointerPoint rawPoint)
168-
: this(modifiers, kind, rawPoint.Twist, rawPoint.Pressure, rawPoint.XTilt, rawPoint.YTilt)
178+
: this(modifiers, kind, rawPoint.Twist, rawPoint.Pressure, rawPoint.XTilt, rawPoint.YTilt, rawPoint.ContactRect)
169179
{
170180
}
171181

src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,14 @@ public record struct RawPointerPoint
143143
/// <inheritdoc cref="PointerPointProperties.YTilt" />
144144
public float YTilt { get; set; }
145145

146+
/// <inheritdoc cref="PointerPointProperties.ContactRect" />
147+
public Rect ContactRect
148+
{
149+
get => _contactRect ?? new Rect(Position, new Size());
150+
set => _contactRect = value;
151+
}
152+
153+
private Rect? _contactRect;
146154

147155
public RawPointerPoint()
148156
{

src/Avalonia.X11/X11Platform.cs

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public void Initialize(X11PlatformOptions options)
5858
DeferredDisplay = XOpenDisplay(IntPtr.Zero);
5959
if (DeferredDisplay == IntPtr.Zero)
6060
throw new Exception("XOpenDisplay failed");
61-
61+
6262
OrphanedWindow = XCreateSimpleWindow(Display, XDefaultRootWindow(Display), 0, 0, 1, 1, 0, IntPtr.Zero,
6363
IntPtr.Zero);
6464
XError.Init();
@@ -86,7 +86,7 @@ public void Initialize(X11PlatformOptions options)
8686
.Bind<IPlatformIconLoader>().ToConstant(new X11IconLoader())
8787
.Bind<IMountedVolumeInfoProvider>().ToConstant(new LinuxMountedVolumeInfoProvider())
8888
.Bind<IPlatformLifetimeEventsImpl>().ToConstant(new X11PlatformLifetimeEvents(this));
89-
89+
9090
Screens = X11Screens = new X11Screens(this);
9191
if (Info.XInputVersion != null)
9292
{
@@ -126,7 +126,7 @@ public ITrayIconImpl CreateTrayIcon()
126126

127127
return dbusTrayIcon;
128128
}
129-
129+
130130
public IWindowImpl CreateWindow()
131131
{
132132
return new X11Window(this, null);
@@ -145,11 +145,11 @@ private static bool EnableIme(X11PlatformOptions options)
145145
var avaloniaImModule = Environment.GetEnvironmentVariable("AVALONIA_IM_MODULE");
146146
if (avaloniaImModule == "none")
147147
return false;
148-
148+
149149
// Use value from options when specified
150150
if (options.EnableIme.HasValue)
151151
return options.EnableIme.Value;
152-
152+
153153
// Automatically enable for CJK locales
154154
var lang = Environment.GetEnvironmentVariable("LANG");
155155
var isCjkLocale = lang != null &&
@@ -168,7 +168,7 @@ private static bool ShouldUseXim()
168168
|| Environment.GetEnvironmentVariable("GTK_IM_MODULE") == "none"
169169
|| Environment.GetEnvironmentVariable("QT_IM_MODULE") == "none")
170170
return true;
171-
171+
172172
// Check if XIM is configured
173173
var modifiers = Environment.GetEnvironmentVariable("XMODIFIERS");
174174
if (modifiers == null)
@@ -177,16 +177,16 @@ private static bool ShouldUseXim()
177177
return false;
178178
if (!modifiers.Contains("@im="))
179179
return false;
180-
180+
181181
// Check if we are configured to use it
182182
if (Environment.GetEnvironmentVariable("GTK_IM_MODULE") == "xim"
183183
|| Environment.GetEnvironmentVariable("QT_IM_MODULE") == "xim"
184184
|| Environment.GetEnvironmentVariable("AVALONIA_IM_MODULE") == "xim")
185185
return true;
186-
186+
187187
return false;
188188
}
189-
189+
190190
private static IPlatformGraphics InitializeGraphics(X11PlatformOptions opts, X11Info info)
191191
{
192192
if (opts.RenderingMode is null || !opts.RenderingMode.Any())
@@ -200,7 +200,7 @@ private static IPlatformGraphics InitializeGraphics(X11PlatformOptions opts, X11
200200
{
201201
return null;
202202
}
203-
203+
204204
if (renderingMode == X11RenderingMode.Glx)
205205
{
206206
if (GlxPlatformGraphics.TryCreate(info, opts.GlProfiles) is { } glx)
@@ -252,13 +252,13 @@ public enum X11RenderingMode
252252
/// Enables native Linux EGL rendering.
253253
/// </summary>
254254
Egl = 3,
255-
255+
256256
/// <summary>
257257
/// Enables Vulkan rendering
258258
/// </summary>
259259
Vulkan = 4
260260
}
261-
261+
262262
/// <summary>
263263
/// Platform-specific options which apply to Linux.
264264
/// </summary>
@@ -294,23 +294,23 @@ public class X11PlatformOptions
294294
/// The default value is true.
295295
/// </summary>
296296
public bool UseDBusFilePicker { get; set; } = true;
297-
297+
298298
/// <summary>
299299
/// Determines whether to use IME.
300300
/// IME would be enabled by default if the current user input language is one of the following: Mandarin, Japanese, Vietnamese or Korean.
301301
/// </summary>
302302
/// <remarks>
303-
/// Input method editor is a component that enables users to generate characters not natively available
303+
/// Input method editor is a component that enables users to generate characters not natively available
304304
/// on their input devices by using sequences of characters or mouse operations that are natively available on their input devices.
305305
/// </remarks>
306306
public bool? EnableIme { get; set; } = true;
307307

308308
/// <summary>
309309
/// Determines whether to use Input Focus Proxy.
310310
/// The default value is false.
311-
/// </summary>
311+
/// </summary>
312312
public bool EnableInputFocusProxy { get; set; }
313-
313+
314314
/// <summary>
315315
/// Determines whether to enable support for the
316316
/// X Session Management Protocol.
@@ -320,7 +320,7 @@ public class X11PlatformOptions
320320
/// Linux systems that uses Xorg. This enables apps to control how they
321321
/// can control and/or cancel the pending shutdown requested by the user.
322322
/// </remarks>
323-
public bool EnableSessionManagement { get; set; } =
323+
public bool EnableSessionManagement { get; set; } =
324324
Environment.GetEnvironmentVariable("AVALONIA_X11_USE_SESSION_MANAGEMENT") != "0";
325325

326326
/// <summary>
@@ -353,7 +353,7 @@ public class X11PlatformOptions
353353
"SVGA3D"
354354
};
355355

356-
356+
357357
public string WmClass { get; set; }
358358

359359
/// <summary>
@@ -368,7 +368,7 @@ public class X11PlatformOptions
368368
/// Retain window framebuffer contents if using CPU rendering mode.
369369
/// This will keep an offscreen bitmap for each window with contents of the previous frame
370370
/// While improving performance by saving a blit, it will increase memory consumption
371-
/// if you have many windows
371+
/// if you have many windows
372372
/// </summary>
373373
public bool? UseRetainedFramebuffer { get; set; }
374374

@@ -378,7 +378,7 @@ public class X11PlatformOptions
378378
/// Use this if you need to use GLib-based libraries on the main thread
379379
/// </summary>
380380
public bool UseGLibMainLoop { get; set; }
381-
381+
382382
/// <summary>
383383
/// If Avalonia is in control of a run loop, we propagate exceptions by stopping the run loop frame
384384
/// and rethrowing an exception. However, if there is no Avalonia-controlled run loop frame,
@@ -387,7 +387,7 @@ public class X11PlatformOptions
387387
/// This property allows to inspect such exceptions before they will be ignored
388388
/// </summary>
389389
public Action<Exception>? ExterinalGLibMainLoopExceptionLogger { get; set; }
390-
390+
391391
public X11PlatformOptions()
392392
{
393393
try

src/Avalonia.X11/XI2Manager.cs

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics;
34
using System.Linq;
5+
using System.Runtime.InteropServices;
6+
47
using Avalonia.Input;
58
using Avalonia.Input.Raw;
9+
using Avalonia.Platform;
610
using static Avalonia.X11.XLib;
711

812
namespace Avalonia.X11
@@ -97,13 +101,15 @@ public bool HasMotion(ParsedDeviceEvent ev)
97101
private AvaloniaX11Platform _platform;
98102

99103
private XIValuatorClassInfo? _pressureXIValuatorClassInfo;
104+
private XIValuatorClassInfo? _touchMajorXIValuatorClassInfo;
105+
private XIValuatorClassInfo? _touchMinorXIValuatorClassInfo;
100106

101107
public bool Init(AvaloniaX11Platform platform)
102108
{
103109
_platform = platform;
104110
_x11 = platform.Info;
105111
_multitouch = platform.Options?.EnableMultiTouch ?? true;
106-
var devices =(XIDeviceInfo*) XIQueryDevice(_x11.Display,
112+
var devices = (XIDeviceInfo*) XIQueryDevice(_x11.Display,
107113
(int)XiPredefinedDeviceId.XIAllMasterDevices, out int num);
108114
for (var c = 0; c < num; c++)
109115
{
@@ -118,8 +124,31 @@ public bool Init(AvaloniaX11Platform platform)
118124

119125
if (_multitouch)
120126
{
127+
// ABS_MT_TOUCH_MAJOR ABS_MT_TOUCH_MINOR
128+
// https://www.kernel.org/doc/html/latest/input/multi-touch-protocol.html
129+
var touchMajorAtom = XInternAtom(_x11.Display, "Abs MT Touch Major", false);
130+
var touchMinorAtom = XInternAtom(_x11.Display, "Abs MT Touch Minor", false);
131+
121132
var pressureAtom = XInternAtom(_x11.Display, "Abs MT Pressure", false);
122-
_pressureXIValuatorClassInfo = _pointerDevice.Valuators.FirstOrDefault(t => t.Label == pressureAtom);
133+
134+
var pressureXIValuatorClassInfo = _pointerDevice.Valuators.FirstOrDefault(t => t.Label == pressureAtom);
135+
if (pressureXIValuatorClassInfo.Label == pressureAtom)
136+
{
137+
// Why check twice? The XIValuatorClassInfo is struct, so the FirstOrDefault will return the default struct when not found.
138+
_pressureXIValuatorClassInfo = pressureXIValuatorClassInfo;
139+
}
140+
141+
var touchMajorXIValuatorClassInfo = _pointerDevice.Valuators.FirstOrDefault(t => t.Label == touchMajorAtom);
142+
if (touchMajorXIValuatorClassInfo.Label == touchMajorAtom)
143+
{
144+
_touchMajorXIValuatorClassInfo = touchMajorXIValuatorClassInfo;
145+
}
146+
147+
var touchMinorXIValuatorClassInfo = _pointerDevice.Valuators.FirstOrDefault(t => t.Label == touchMinorAtom);
148+
if (touchMinorXIValuatorClassInfo.Label == touchMinorAtom)
149+
{
150+
_touchMinorXIValuatorClassInfo = touchMinorXIValuatorClassInfo;
151+
}
123152
}
124153

125154
/*
@@ -256,6 +285,57 @@ private void OnDeviceEvent(IXI2Client client, ParsedDeviceEvent ev)
256285
}
257286
}
258287

288+
if(_touchMajorXIValuatorClassInfo is {} touchMajorXIValuatorClassInfo)
289+
{
290+
double? touchMajor = null;
291+
double? touchMinor = null;
292+
PixelRect screenBounds = default;
293+
if (ev.Valuators.TryGetValue(touchMajorXIValuatorClassInfo.Number, out var touchMajorValue))
294+
{
295+
var pixelPoint = new PixelPoint((int)ev.RootPosition.X, (int)ev.RootPosition.Y);
296+
var screen = _platform.Screens.ScreenFromPoint(pixelPoint);
297+
var screenBoundsFromPoint = screen?.Bounds;
298+
Debug.Assert(screenBoundsFromPoint != null);
299+
if (screenBoundsFromPoint != null)
300+
{
301+
screenBounds = screenBoundsFromPoint.Value;
302+
303+
// As https://www.kernel.org/doc/html/latest/input/multi-touch-protocol.html says, using `screenBounds.Width` is not accurate enough.
304+
touchMajor = (touchMajorValue - touchMajorXIValuatorClassInfo.Min) /
305+
(touchMajorXIValuatorClassInfo.Max - touchMajorXIValuatorClassInfo.Min) * screenBounds.Width;
306+
}
307+
}
308+
309+
if (touchMajor != null)
310+
{
311+
if(_touchMinorXIValuatorClassInfo is {} touchMinorXIValuatorClassInfo)
312+
{
313+
if (ev.Valuators.TryGetValue(touchMinorXIValuatorClassInfo.Number, out var touchMinorValue))
314+
{
315+
touchMinor = (touchMinorValue - touchMinorXIValuatorClassInfo.Min) /
316+
(touchMinorXIValuatorClassInfo.Max - touchMinorXIValuatorClassInfo.Min) * screenBounds.Height;
317+
}
318+
}
319+
320+
if (touchMinor == null)
321+
{
322+
touchMinor = touchMajor;
323+
}
324+
325+
var center = ev.Position;
326+
var leftX = center.X - touchMajor.Value / 2;
327+
var topY = center.Y - touchMinor.Value / 2;
328+
329+
rawPointerPoint.ContactRect = new Rect
330+
(
331+
leftX,
332+
topY,
333+
touchMajor.Value,
334+
touchMinor.Value
335+
);
336+
}
337+
}
338+
259339
client.ScheduleXI2Input(new RawTouchEventArgs(client.TouchDevice,
260340
ev.Timestamp, client.InputRoot, type, rawPointerPoint, ev.Modifiers, ev.Detail));
261341
return;
@@ -340,6 +420,7 @@ internal unsafe class ParsedDeviceEvent
340420
public RawInputModifiers Modifiers { get; }
341421
public ulong Timestamp { get; }
342422
public Point Position { get; }
423+
public Point RootPosition { get; }
343424
public int Button { get; set; }
344425
public int Detail { get; set; }
345426
public bool Emulated { get; set; }
@@ -385,6 +466,7 @@ public ParsedDeviceEvent(XIDeviceEvent* ev)
385466

386467
Valuators = new Dictionary<int, double>();
387468
Position = new Point(ev->event_x, ev->event_y);
469+
RootPosition = new Point(ev->root_x, ev->root_y);
388470
var values = ev->valuators.Values;
389471
if(ev->valuators.Mask != null)
390472
for (var c = 0; c < ev->valuators.MaskLen * 8; c++)

0 commit comments

Comments
 (0)