diff --git a/src/Uno.UI.Runtime.Skia.Gtk/Input/GtkCorePointerInputSource.cs b/src/Uno.UI.Runtime.Skia.Gtk/Input/GtkCorePointerInputSource.cs
index 2fdeea2f5ee5..c163cb6e5185 100644
--- a/src/Uno.UI.Runtime.Skia.Gtk/Input/GtkCorePointerInputSource.cs
+++ b/src/Uno.UI.Runtime.Skia.Gtk/Input/GtkCorePointerInputSource.cs
@@ -475,6 +475,7 @@ private void UseDevice(PointerPoint pointer, Gdk.Device device)
properties.IsRightButtonPressed = IsPressed(state, ModifierType.Button3Mask, properties.PointerUpdateKind, RightButtonPressed, RightButtonReleased);
properties.IsXButton1Pressed = IsPressed(state, ModifierType.Button4Mask, properties.PointerUpdateKind, XButton1Pressed, XButton1Released);
properties.IsXButton2Pressed = IsPressed(state, ModifierType.Button5Mask, properties.PointerUpdateKind, XButton1Pressed, XButton2Released);
+ properties.IsTouchPad = dev.Source == InputSource.Touchpad;
break;
case PointerDeviceType.Pen:
diff --git a/src/Uno.UI.Runtime.Skia.X11/X11PointerInputSource.CoreProtocol.cs b/src/Uno.UI.Runtime.Skia.X11/X11PointerInputSource.CoreProtocol.cs
new file mode 100644
index 000000000000..1e11d9f529c2
--- /dev/null
+++ b/src/Uno.UI.Runtime.Skia.X11/X11PointerInputSource.CoreProtocol.cs
@@ -0,0 +1,142 @@
+using System;
+using Windows.Devices.Input;
+using Windows.Foundation;
+using Windows.UI.Core;
+using Windows.UI.Input;
+using Microsoft.UI.Xaml.Controls;
+using Uno.UI.Hosting;
+
+namespace Uno.WinUI.Runtime.Skia.X11;
+
+internal partial class X11PointerInputSource
+{
+ private const int LEFT = 1;
+ private const int MIDDLE = 2;
+ private const int RIGHT = 3;
+ private const int SCROLL_UP = 4;
+ private const int SCROLL_DOWN = 5;
+ private const int SCROLL_LEFT = 6;
+ private const int SCROLL_RIGHT = 7;
+ private const int XButton1 = 8;
+ private const int XButton2 = 9;
+
+ private Point _mousePosition;
+ private int _pressedButtons; // // bit 0 is not used
+
+ public void ProcessLeaveEvent(XCrossingEvent ev)
+ {
+ _mousePosition = new Point(ev.x, ev.y);
+
+ var point = CreatePointFromCurrentState(ev.time);
+ var modifiers = X11XamlRootHost.XModifierMaskToVirtualKeyModifiers(ev.state);
+
+ var args = new PointerEventArgs(point, modifiers);
+
+ CreatePointFromCurrentState(ev.time);
+ X11XamlRootHost.QueueAction(_host, () => RaisePointerExited(args));
+ }
+
+ public void ProcessEnterEvent(XCrossingEvent ev)
+ {
+ _mousePosition = new Point(ev.x, ev.y);
+
+ var args = CreatePointerEventArgsFromCurrentState(ev.time, ev.state);
+ X11XamlRootHost.QueueAction(_host, () => RaisePointerEntered(args));
+ }
+
+ public void ProcessMotionNotifyEvent(XMotionEvent ev)
+ {
+ _mousePosition = new Point(ev.x, ev.y);
+
+ var args = CreatePointerEventArgsFromCurrentState(ev.time, ev.state);
+ X11XamlRootHost.QueueAction(_host, () => RaisePointerMoved(args));
+ }
+
+ public void ProcessButtonPressedEvent(XButtonEvent ev)
+ {
+ _mousePosition = new Point(ev.x, ev.y);
+ _pressedButtons = (byte)(_pressedButtons | 1 << ev.button);
+
+ var args = CreatePointerEventArgsFromCurrentState(ev.time, ev.state);
+
+ if (ev.button is SCROLL_LEFT or SCROLL_RIGHT or SCROLL_UP or SCROLL_DOWN)
+ {
+ // These scrolling events are shown as a ButtonPressed with a corresponding ButtonReleased in succession.
+ // We arbitrarily choose to handle this on the Pressed side and ignore the Released side.
+ // Note that this makes scrolling discrete, i.e. there is no Scrolling delta. Instead, we get a separate
+ // Pressed/Released pair for each scroll wheel "detent".
+
+ var props = args.CurrentPoint.Properties;
+ props.IsHorizontalMouseWheel = ev.button is SCROLL_LEFT or SCROLL_RIGHT;
+ props.MouseWheelDelta = ev.button is SCROLL_LEFT or SCROLL_UP ?
+ ScrollContentPresenter.ScrollViewerDefaultMouseWheelDelta :
+ -ScrollContentPresenter.ScrollViewerDefaultMouseWheelDelta;
+
+ X11XamlRootHost.QueueAction(_host, () => RaisePointerWheelChanged(args));
+ }
+ else
+ {
+ X11XamlRootHost.QueueAction(_host, () => RaisePointerPressed(args));
+ }
+ }
+
+ // Note about removing devices: the server emits a ButtonRelease if a device is removed
+ // while a button is held.
+ public void ProcessButtonReleasedEvent(XButtonEvent ev)
+ {
+ // TODO: what if button released when not same_screen?
+ if (ev.button is SCROLL_LEFT or SCROLL_RIGHT or SCROLL_UP or SCROLL_DOWN)
+ {
+ // Scroll events are already handled in ProcessButtonPressedEvent
+ return;
+ }
+
+ _mousePosition = new Point(ev.x, ev.y);
+ _pressedButtons = (byte)(_pressedButtons & ~(1 << ev.button));
+
+ var args = CreatePointerEventArgsFromCurrentState(ev.time, ev.state);
+ X11XamlRootHost.QueueAction(_host, () => RaisePointerReleased(args));
+ }
+
+ private PointerEventArgs CreatePointerEventArgsFromCurrentState(IntPtr time, XModifierMask state)
+ {
+ var point = CreatePointFromCurrentState(time);
+ var modifiers = X11XamlRootHost.XModifierMaskToVirtualKeyModifiers(state);
+
+ return new PointerEventArgs(point, modifiers);
+ }
+
+ ///
+ /// Create a new PointerPoint from the current state of the PointerInputSource
+ ///
+ private PointerPoint CreatePointFromCurrentState(IntPtr time)
+ {
+ var properties = new PointerPointProperties
+ {
+ // TODO: fill this comprehensively
+ IsLeftButtonPressed = (_pressedButtons & (1 << LEFT)) != 0,
+ IsMiddleButtonPressed = (_pressedButtons & (1 << MIDDLE)) != 0,
+ IsRightButtonPressed = (_pressedButtons & (1 << RIGHT)) != 0
+ };
+
+ var scale = ((IXamlRootHost)_host).RootElement?.XamlRoot is { } root
+ ? root.RasterizationScale
+ : 1;
+
+ // Time is given in milliseconds since system boot
+ // This doesn't match the format of WinUI. See also: https://github.com/unoplatform/uno/issues/14535
+ var point = new PointerPoint(
+ frameId: (uint)time, // UNO TODO: How should set the frame, timestamp may overflow.
+ timestamp: (uint)time,
+ PointerDevice.For(PointerDeviceType.Mouse),
+ 0, // TODO: XInput
+ new Point(_mousePosition.X / scale, _mousePosition.Y / scale),
+ new Point(_mousePosition.X / scale, _mousePosition.Y / scale),
+ // TODO: is isInContact correct?
+ properties.HasPressedButton,
+ properties
+ );
+
+ return point;
+ }
+}
diff --git a/src/Uno.UI.Runtime.Skia.X11/X11PointerInputSource.Mouse.cs b/src/Uno.UI.Runtime.Skia.X11/X11PointerInputSource.Mouse.cs
deleted file mode 100644
index 93ad960a9797..000000000000
--- a/src/Uno.UI.Runtime.Skia.X11/X11PointerInputSource.Mouse.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-using Windows.Foundation;
-using Microsoft.UI.Xaml.Controls;
-
-namespace Uno.WinUI.Runtime.Skia.X11;
-
-internal partial class X11PointerInputSource
-{
- private const int LEFT = 1;
- private const int MIDDLE = 2;
- private const int RIGHT = 3;
- private const int SCROLL_DOWN = 4;
- private const int SCROLL_UP = 5;
- private const int SCROLL_LEFT = 6;
- private const int SCROLL_RIGHT = 7;
-
- private Point _mousePosition;
- private byte _pressedButtons; // // bit 0 is not used
-
- public void ProcessMotionNotifyEvent(XMotionEvent ev)
- {
- _mousePosition = new Point(ev.x, ev.y);
-
- var args = CreatePointerEventArgsFromCurrentState(ev.time, ev.state);
- X11XamlRootHost.QueueAction(_host, () => RaisePointerMoved(args));
- }
-
- public void ProcessButtonPressedEvent(XButtonEvent ev)
- {
- _mousePosition = new Point(ev.x, ev.y);
- _pressedButtons = (byte)(_pressedButtons | 1 << ev.button);
-
- var args = CreatePointerEventArgsFromCurrentState(ev.time, ev.state);
-
- if (ev.button is SCROLL_LEFT or SCROLL_RIGHT or SCROLL_UP or SCROLL_DOWN)
- {
- // These scrolling events are shown as a ButtonPressed with a corresponding ButtonReleased in succession.
- // We arbitrarily choose to handle this on the Pressed side and ignore the Released side.
- // Note that this makes scrolling discrete, i.e. there is no Scrolling delta. Instead, we get a separate
- // Pressed/Released pair for each scroll wheel "detent".
-
- var props = args.CurrentPoint.Properties;
- props.IsHorizontalMouseWheel = ev.button is SCROLL_LEFT or SCROLL_RIGHT;
- props.MouseWheelDelta = ev.button is SCROLL_LEFT or SCROLL_UP ?
- -ScrollContentPresenter.ScrollViewerDefaultMouseWheelDelta :
- ScrollContentPresenter.ScrollViewerDefaultMouseWheelDelta;
-
- X11XamlRootHost.QueueAction(_host, () => RaisePointerWheelChanged(args));
- }
- else
- {
- X11XamlRootHost.QueueAction(_host, () => RaisePointerPressed(args));
- }
- }
-
- public void ProcessButtonReleasedEvent(XButtonEvent ev)
- {
- // TODO: what if button released when not same_screen?
- if (ev.button is SCROLL_LEFT or SCROLL_RIGHT or SCROLL_UP or SCROLL_DOWN)
- {
- // Scroll events are already handled in ProcessButtonPressedEvent
- return;
- }
-
- _mousePosition = new Point(ev.x, ev.y);
- _pressedButtons = (byte)(_pressedButtons & ~(1 << ev.button));
-
- var args = CreatePointerEventArgsFromCurrentState(ev.time, ev.state);
- X11XamlRootHost.QueueAction(_host, () => RaisePointerReleased(args));
- }
-}
diff --git a/src/Uno.UI.Runtime.Skia.X11/X11PointerInputSource.XInput.cs b/src/Uno.UI.Runtime.Skia.X11/X11PointerInputSource.XInput.cs
new file mode 100644
index 000000000000..def8517ff535
--- /dev/null
+++ b/src/Uno.UI.Runtime.Skia.X11/X11PointerInputSource.XInput.cs
@@ -0,0 +1,524 @@
+// Copyright 1996-1997 by Frederic Lepied, France.
+//
+// Permission to use, copy, modify, distribute, and sell this software and its
+// documentation for any purpose is hereby granted without fee, provided that
+// the above copyright notice appear in all copies and that both that
+// copyright notice and this permission notice appear in supporting
+// documentation, and that the name of the authors not be used in
+// advertising or publicity pertaining to distribution of the software without
+// specific, written prior permission. The authors make no
+// representations about the suitability of this software for any purpose. It
+// is provided "as is" without express or implied warranty.
+//
+// THE AUTHORS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
+// INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
+// EVENT SHALL THE AUTHORS BE LIABLE FOR ANY SPECIAL, INDIRECT OR
+// CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
+// DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
+// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+//
+// Copyright © 2007 Peter Hutterer
+// Copyright © 2009 Red Hat, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice (including the next
+// paragraph) shall be included in all copies or substantial portions of the
+// Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+// The MIT License (MIT)
+//
+// Copyright (c) .NET Foundation and Contributors
+// All Rights Reserved
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+// https://gitlab.freedesktop.org/xorg/app/xinput/-/blob/f550dfbb347ec62ca59e86f79a9e4fe43417ab39/src/xinput.c
+// https://gitlab.freedesktop.org/xorg/app/xinput/-/blob/f550dfbb347ec62ca59e86f79a9e4fe43417ab39/src/test_xi2.c
+// https://github.com/AvaloniaUI/Avalonia/blob/e0127c610c38701c3af34f580273f6efd78285b5/src/Avalonia.X11/XI2Manager.cs
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Windows.Devices.Input;
+using Windows.Foundation;
+using Windows.UI.Input;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Uno.Collections;
+using Uno.Disposables;
+using Uno.Foundation.Logging;
+using Uno.UI.Hosting;
+using PointerEventArgs = Windows.UI.Core.PointerEventArgs;
+namespace Uno.WinUI.Runtime.Skia.X11;
+
+// Thanks to the amazing Peter Hutterer and Martin Kepplinger for creating evemu recordings
+// for touchscreens
+// https://github.com/whot/evemu-devices
+
+internal partial class X11PointerInputSource
+{
+ private readonly Dictionary _lastHorizontalTouchpadWheelPosition = new();
+ private readonly Dictionary _lastVerticalTouchpadWheelPosition = new();
+ private readonly Dictionary _deviceInfoCache = new();
+
+ public unsafe PointerEventArgs CreatePointerEventArgsFromDeviceEvent(XIDeviceEvent data)
+ {
+ (double wheelDelta, bool isHorizontalMouseWheel) = (0, false);
+ if (data.evtype is XiEventType.XI_ButtonPress or XiEventType.XI_ButtonRelease)
+ {
+ // Check the similar implementation for scrolling using the core protocol for more
+ // information on what this is.
+ // Unlike the equivalent core protocol events, this only works for an actual mouse with
+ // a real wheel. A touchpad does not behave the same way.
+ (wheelDelta, isHorizontalMouseWheel) = data.detail switch
+ {
+ 1 << SCROLL_RIGHT => (-ScrollContentPresenter.ScrollViewerDefaultMouseWheelDelta, true),
+ 1 << SCROLL_DOWN => (-ScrollContentPresenter.ScrollViewerDefaultMouseWheelDelta, false),
+ 1 << SCROLL_LEFT => (ScrollContentPresenter.ScrollViewerDefaultMouseWheelDelta, true),
+ 1 << SCROLL_UP => (ScrollContentPresenter.ScrollViewerDefaultMouseWheelDelta, false),
+ _ => (0, false)
+ };
+ }
+
+ var info = GetDevicePropertiesFromId(data.sourceid);
+
+ // Touchpad scrolling manifests in a Motion event where the current scrolling "position" is recorded in the
+ // valuator. There is no direct way to get the delta, so we have to record the old position and diff it
+ // with the new position. This also means that the first "tick" will not result in a WheelChanged event,
+ // but will only be used to set the initial wheel position.
+ // Also, we can get a "diagonal scroll" where both the horizontal and the vertical positions change.
+ // Our PointerEventArgs don't support this, so we arbitrarily choose to make diagonal scrolling
+ // result in a vertical scroll.
+ // IMPORTANT: DO NOT FORGET TO RESET POSITIONS ON LEAVE.
+ if (data.evtype is XiEventType.XI_Motion && info is { } info_)
+ {
+ var valuators = data.valuators;
+ var values = valuators.Values;
+ for (var i = 0; i < valuators.MaskLen * 8; i++)
+ {
+ if (XLib.XIMaskIsSet(valuators.Mask, i))
+ {
+ var (valuator, value) = (i, *values++);
+ if (valuator == info_.HorizontalValuator || valuator == info_.VerticalValuator)
+ {
+ isHorizontalMouseWheel = valuator == info_.HorizontalValuator;
+ var (dict, increment) = isHorizontalMouseWheel ?
+ (_lastHorizontalTouchpadWheelPosition, info_.HorizontalIncrement!.Value) :
+ (_lastVerticalTouchpadWheelPosition, info_.VerticalIncrement!.Value);
+ if (dict.TryGetValue(data.sourceid, out var oldValue))
+ {
+ wheelDelta = (oldValue - value) * ScrollContentPresenter.ScrollViewerDefaultMouseWheelDelta / increment;
+ }
+ dict[data.sourceid] = value;
+ }
+ // DO NOT BREAK OUT OF THE FOR LOOP. We still need to update the other valuator positions.
+ }
+ }
+ }
+
+ var mask = 0;
+ for (var i = 0; i < data.buttons.MaskLen; i++) // masklen <= 4
+ {
+ mask |= data.buttons.Mask[i] << (8 * i);
+ }
+
+ if (data.evtype is XiEventType.XI_ButtonPress)
+ {
+ mask |= (1 << data.detail); // the newly pressed button is not added to the mask yet.
+ }
+ else if (data.evtype is XiEventType.XI_ButtonRelease)
+ {
+ mask &= ~(1 << data.detail); // the newly released button is not removed from the mask yet.
+ }
+
+ var properties = new PointerPointProperties
+ {
+ IsLeftButtonPressed = (mask & (1 << LEFT)) != 0,
+ IsMiddleButtonPressed = (mask & (1 << MIDDLE)) != 0,
+ IsRightButtonPressed = (mask & (1 << RIGHT)) != 0,
+ IsXButton1Pressed = (mask & (1 << XButton1)) != 0,
+ IsXButton2Pressed = (mask & (1 << XButton2)) != 0,
+ IsHorizontalMouseWheel = isHorizontalMouseWheel,
+ IsInRange = true,
+ IsTouchPad = info is { } && IsTouchpad(info.Value.Properties),
+ MouseWheelDelta = (int)Math.Round(wheelDelta),
+ PointerUpdateKind = data.detail switch
+ {
+ LEFT when data.evtype == XiEventType.XI_ButtonPress => PointerUpdateKind.LeftButtonPressed,
+ LEFT when data.evtype == XiEventType.XI_ButtonRelease => PointerUpdateKind.LeftButtonReleased,
+ RIGHT when data.evtype == XiEventType.XI_ButtonPress => PointerUpdateKind.RightButtonPressed,
+ RIGHT when data.evtype == XiEventType.XI_ButtonRelease => PointerUpdateKind.RightButtonReleased,
+ MIDDLE when data.evtype == XiEventType.XI_ButtonPress => PointerUpdateKind.MiddleButtonPressed,
+ MIDDLE when data.evtype == XiEventType.XI_ButtonRelease => PointerUpdateKind.MiddleButtonReleased,
+ XButton1 when data.evtype == XiEventType.XI_ButtonPress => PointerUpdateKind.XButton1Pressed,
+ XButton1 when data.evtype == XiEventType.XI_ButtonRelease => PointerUpdateKind.XButton1Released,
+ XButton2 when data.evtype == XiEventType.XI_ButtonPress => PointerUpdateKind.XButton2Pressed,
+ XButton2 when data.evtype == XiEventType.XI_ButtonRelease => PointerUpdateKind.XButton2Released,
+ _ => PointerUpdateKind.Other
+ }
+ };
+
+ var scale = ((IXamlRootHost)_host).RootElement?.XamlRoot is { } root
+ ? XamlRoot.GetDisplayInformation(root).RawPixelsPerViewPixel
+ : 1;
+
+ // Time is given in milliseconds since system boot
+ // This doesn't match the format of WinUI. See also: https://github.com/unoplatform/uno/issues/14535
+ var point = new PointerPoint(
+ frameId: (uint)data.time, // UNO TODO: How should we set the frame, timestamp may overflow.
+ timestamp: (ulong)data.time,
+ data.evtype is XiEventType.XI_TouchBegin or XiEventType.XI_TouchEnd or XiEventType.XI_TouchUpdate ? PointerDevice.For(PointerDeviceType.Touch) : PointerDevice.For(PointerDeviceType.Mouse),
+ (uint)data.sourceid,
+ new Point(data.event_x / scale, data.event_y / scale),
+ new Point(data.event_x / scale, data.event_y / scale),
+ properties.HasPressedButton,
+ properties
+ );
+
+ var modifiers = X11XamlRootHost.XModifierMaskToVirtualKeyModifiers((XModifierMask)(data.mods.Base & 0xffff));
+
+ return new PointerEventArgs(point, modifiers);
+ }
+
+ // This method is comment-free. See the very similar (but more involved) implementation of
+ // CreatePointerEventArgsFromDeviceEvent for comments.
+ public unsafe PointerEventArgs CreatePointerEventArgsFromEnterLeaveEvent(XIEnterLeaveEvent data, PointerDeviceType pointerType)
+ {
+ var mask = 0;
+ for (var i = 0; i < data.buttons.MaskLen; i++) // masklen <= 4
+ {
+ mask |= data.buttons.Mask[i] << (8 * i);
+ }
+
+ var properties = new PointerPointProperties
+ {
+ IsLeftButtonPressed = (mask & (1 << LEFT)) != 0,
+ IsMiddleButtonPressed = (mask & (1 << MIDDLE)) != 0,
+ IsRightButtonPressed = (mask & (1 << RIGHT)) != 0,
+ IsXButton1Pressed = (mask & (1 << XButton1)) != 0,
+ IsXButton2Pressed = (mask & (1 << XButton2)) != 0,
+ IsInRange = true,
+ IsTouchPad = GetDevicePropertiesFromId(data.sourceid) is { } info && IsTouchpad(info.Properties),
+ IsHorizontalMouseWheel = false,
+ };
+
+ var point = new PointerPoint(
+ frameId: (uint)data.time, // UNO TODO: How should we set the frame, timestamp may overflow.
+ timestamp: (ulong)data.time,
+ PointerDevice.For(PointerDeviceType.Mouse),
+ (uint)data.sourceid,
+ new Point(data.event_x, data.event_y),
+ new Point(data.event_x, data.event_y),
+ properties.HasPressedButton,
+ properties
+ );
+
+ var modifiers = X11XamlRootHost.XModifierMaskToVirtualKeyModifiers((XModifierMask)(data.mods.Base & 0xffff));
+
+ return new PointerEventArgs(point, modifiers);
+ }
+
+ // Note about removing devices: the server emits a ButtonRelease if a device is removed
+ // while a button is held.
+ public void HandleXI2Event(XEvent ev)
+ {
+ var evtype = (XiEventType)ev.GenericEventCookie.evtype;
+ if (this.Log().IsEnabled(LogLevel.Trace))
+ {
+ this.Log().Trace($"XI2 EVENT: {evtype}");
+ }
+ switch (evtype)
+ {
+ case XiEventType.XI_Enter:
+ case XiEventType.XI_Leave:
+ {
+ _lastHorizontalTouchpadWheelPosition.Clear();
+ _lastVerticalTouchpadWheelPosition.Clear();
+ var args = CreatePointerEventArgsFromEnterLeaveEvent(
+ ev.GenericEventCookie.GetEvent(),
+ PointerDeviceType.Mouse); // https://www.x.org/releases/X11R7.7/doc/inputproto/XI2proto.txt: Touch events do not generate enter/leave events.
+ if (evtype is XiEventType.XI_Enter)
+ {
+ X11XamlRootHost.QueueAction(_host, () => RaisePointerEntered(args));
+ }
+ else
+ {
+ X11XamlRootHost.QueueAction(_host, () => RaisePointerExited(args));
+ }
+ }
+ break;
+ case XiEventType.XI_Motion:
+ case XiEventType.XI_ButtonPress:
+ case XiEventType.XI_ButtonRelease:
+ case XiEventType.XI_TouchBegin:
+ case XiEventType.XI_TouchEnd:
+ case XiEventType.XI_TouchUpdate:
+ {
+ var data = ev.GenericEventCookie.GetEvent();
+ if (data.deviceid == data.sourceid)
+ {
+ // The X Server sends 2 events. One for the master device and one for
+ // the slave device. We only want the slave device. This happens even for
+ // motion events. Here's an example I logged from the xinput utility.
+ // EVENT type 4 (ButtonPress)
+ // device: 11 (11)
+ // time: 15331402
+ // detail: 1
+ // flags:
+ // root: 991.92/597.13
+ // event: 131.92/163.13
+ // buttons:
+ // modifiers: locked 0x10 latched 0 base 0 effective: 0x10
+ // group: locked 0 latched 0 base 0 effective: 0
+ // valuators:
+ // windows: root 0x533 event 0x4c00001 child 0x0
+ // EVENT type 4 (ButtonPress)
+ // device: 2 (11)
+ // time: 15331402
+ // detail: 1
+ // flags:
+ // root: 991.92/597.13
+ // event: 131.92/163.13
+ // buttons:
+ // modifiers: locked 0x10 latched 0 base 0 effective: 0x10
+ // group: locked 0 latched 0 base 0 effective: 0
+ // valuators:
+ // windows: root 0x533 event 0x4c00001 child 0x0
+ break;
+ }
+
+ var args = CreatePointerEventArgsFromDeviceEvent(data);
+ switch (evtype)
+ {
+ case XiEventType.XI_Motion when args.CurrentPoint.Properties.MouseWheelDelta != 0:
+ X11XamlRootHost.QueueAction(_host, () => RaisePointerWheelChanged(args));
+ break;
+ case XiEventType.XI_Motion:
+ X11XamlRootHost.QueueAction(_host, () => RaisePointerMoved(args));
+ break;
+ case XiEventType.XI_ButtonPress when args.CurrentPoint.Properties.MouseWheelDelta != 0:
+ X11XamlRootHost.QueueAction(_host, () => RaisePointerWheelChanged(args));
+ break;
+ case XiEventType.XI_ButtonPress:
+ X11XamlRootHost.QueueAction(_host, () => RaisePointerPressed(args));
+ break;
+ case XiEventType.XI_ButtonRelease when args.CurrentPoint.Properties.MouseWheelDelta == 0:
+ // if delta != 0, then this is the ButtonRelease of the (ButtonPress,ButtonRelease) pair
+ // used for scrolling. We arbitrarily choose to handle it on the ButtonPress side.
+ X11XamlRootHost.QueueAction(_host, () => RaisePointerReleased(args));
+ break;
+ }
+ }
+ break;
+ case XiEventType.XI_DeviceChanged:
+ {
+ var data = ev.GenericEventCookie.GetEvent();
+ _deviceInfoCache.Remove(data.sourceid);
+ break;
+ }
+ default:
+ if (this.Log().IsEnabled(LogLevel.Error))
+ {
+ this.Log().Error($"XI2 ERROR: received an unexpected {evtype} event");
+ }
+ break;
+ }
+ }
+
+ // for some stupid reason, the device id can be reused for other devices if
+ // the original device is unplugged. For example, if device D1 is assigned device id 1
+ // but is then unplugged, another device D2 can be assigned id 1. This means that we
+ // can't cache the lookups, and instead are forced to make this call every single time :(
+ private bool IsTouchpad(IEnumerable props)
+ {
+ // X Input cannot distinguish between trackpads and mice. We do this
+ // by testing for properties that should be available on trackpads.
+ //
+ // For libinput, here's the output of `xinput list-props` on my Lenovo trackpad
+ // Device 'ELAN0001:00 04F3:3140 Touchpad':
+ // Device Enabled (168): 1
+ // Coordinate Transformation Matrix (170): 1.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 1.000000
+ // libinput Tapping Enabled (324): 1
+ // libinput Tapping Enabled Default (325): 0
+ // libinput Tapping Drag Enabled (326): 1
+ // libinput Tapping Drag Enabled Default (327): 1
+ // libinput Tapping Drag Lock Enabled (328): 0
+ // libinput Tapping Drag Lock Enabled Default (329): 0
+ // libinput Tapping Button Mapping Enabled (330): 1, 0
+ // libinput Tapping Button Mapping Default (331): 1, 0
+ // libinput Natural Scrolling Enabled (297): 0
+ // libinput Natural Scrolling Enabled Default (298): 0
+ // libinput Disable While Typing Enabled (332): 1
+ // libinput Disable While Typing Enabled Default (333): 1
+ // libinput Scroll Methods Available (299): 1, 1, 0
+ // libinput Scroll Method Enabled (300): 1, 0, 0
+ // libinput Scroll Method Enabled Default (301): 1, 0, 0
+ // libinput Click Methods Available (334): 1, 1
+ // libinput Click Method Enabled (335): 1, 0
+ // libinput Click Method Enabled Default (336): 1, 0
+ // libinput Middle Emulation Enabled (337): 0
+ // libinput Middle Emulation Enabled Default (338): 0
+ // libinput Accel Speed (306): 0.000000
+ // libinput Accel Speed Default (307): 0.000000
+ // libinput Accel Profiles Available (308): 1, 1, 1
+ // libinput Accel Profile Enabled (309): 1, 0, 0
+ // libinput Accel Profile Enabled Default (310): 1, 0, 0
+ // libinput Accel Custom Fallback Points (311):
+ // libinput Accel Custom Fallback Step (312): 0.000000
+ // libinput Accel Custom Motion Points (313):
+ // libinput Accel Custom Motion Step (314): 0.000000
+ // libinput Accel Custom Scroll Points (315):
+ // libinput Accel Custom Scroll Step (316): 0.000000
+ // libinput Left Handed Enabled (317): 0
+ // libinput Left Handed Enabled Default (318): 0
+ // libinput Send Events Modes Available (282): 1, 1
+ // libinput Send Events Mode Enabled (283): 0, 0
+ // libinput Send Events Mode Enabled Default (284): 0, 0
+ // Device Node (285): "/dev/input/event7"
+ // Device Product ID (286): 1267, 12608
+ // libinput Drag Lock Buttons (319):
+ // libinput Horizontal Scroll Enabled (320): 1
+ // libinput Scrolling Pixel Distance (321): 15
+ // libinput Scrolling Pixel Distance Default (322): 15
+ // libinput High Resolution Wheel Scroll Enabled (323): 1
+
+ // here's the output when swapping out libinput for synaptics
+ // Device 'ELAN0001:00 04F3:3140 Touchpad':
+ // Device Enabled (168): 1
+ // Coordinate Transformation Matrix (170): 1.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 1.000000
+ // Device Accel Profile (293): 1
+ // Device Accel Constant Deceleration (294): 2.500000
+ // Device Accel Adaptive Deceleration (295): 1.000000
+ // Device Accel Velocity Scaling (296): 12.500000
+ // Synaptics Edges (327): 128, 3081, 113, 1984
+ // Synaptics Finger (328): 25, 30, 0
+ // Synaptics Tap Time (329): 180
+ // Synaptics Tap Move (330): 168
+ // Synaptics Tap Durations (331): 180, 180, 100
+ // Synaptics ClickPad (332): 1
+ // Synaptics Middle Button Timeout (333): 0
+ // Synaptics Two-Finger Pressure (334): 282
+ // Synaptics Two-Finger Width (335): 7
+ // Synaptics Scrolling Distance (336): 76, 76
+ // Synaptics Edge Scrolling (337): 0, 0, 0
+ // Synaptics Two-Finger Scrolling (338): 1, 0
+ // Synaptics Move Speed (339): 1.000000, 1.750000, 0.052178, 0.000000
+ // Synaptics Off (340): 0
+ // Synaptics Locked Drags (341): 0
+ // Synaptics Locked Drags Timeout (342): 5000
+ // Synaptics Tap Action (343): 0, 0, 0, 0, 0, 0, 0
+ // Synaptics Click Action (344): 1, 3, 2
+ // Synaptics Circular Scrolling (345): 0
+ // Synaptics Circular Scrolling Distance (346): 0.100000
+ // Synaptics Circular Scrolling Trigger (347): 0
+ // Synaptics Circular Pad (348): 0
+ // Synaptics Palm Detection (349): 0
+ // Synaptics Palm Dimensions (350): 10, 200
+ // Synaptics Coasting Speed (351): 20.000000, 50.000000
+ // Synaptics Pressure Motion (352): 30, 160
+ // Synaptics Pressure Motion Factor (353): 1.000000, 1.000000
+ // Synaptics Grab Event Device (354): 0
+ // Synaptics Gestures (355): 1
+ // Synaptics Capabilities (356): 1, 0, 0, 1, 1, 0, 0
+ // Synaptics Pad Resolution (357): 32, 32
+ // Synaptics Area (358): 0, 0, 0, 0
+ // Synaptics Soft Button Areas (359): 1604, 0, 1719, 0, 0, 0, 0, 0
+ // Synaptics Noise Cancellation (360): 19, 19
+ // Device Product ID (286): 1267, 12608
+ // Device Node (285): "/dev/input/event8"
+ return props.Any(p => p is "Synaptics Tap Time" or "libinput Tapping Enabled");
+ }
+
+ private unsafe DeviceInfo? GetDevicePropertiesFromId(int id)
+ {
+ if (_deviceInfoCache.TryGetValue(id, out var result))
+ {
+ return result;
+ }
+ var display = _host.TopX11Window.Display;
+ var infos = XLib.XIQueryDevice(display, (int)XiPredefinedDeviceId.XIAllDevices, out var ndevices);
+ using var deviceInfoDisposable = Disposable.Create(() => XLib.XIFreeDeviceInfo(infos));
+
+ for (var i = 0; i < ndevices; i++)
+ {
+ if (infos[i].Deviceid == id)
+ {
+ var info = infos[i];
+
+ IntPtr* props = X11Helper.XIListProperties(display, info.Deviceid, out var nprops);
+ using var propsDisposable = Disposable.Create(() =>
+ {
+ _ = XLib.XFree((IntPtr)props);
+ });
+ var propsResult = new List();
+ for (var index = 0; index < nprops; index++)
+ {
+ var name = XLib.GetAtomName(display, props[index]);
+ if (name is { })
+ {
+ propsResult.Add(name);
+ }
+ }
+
+ int? horizontalValuator = null;
+ double? horizontalIncrement = null;
+ int? verticalValuator = null;
+ double? verticalIncrement = null;
+ for (var index = 0; index < info.NumClasses; index++)
+ {
+ if (info.Classes[index]->Type == XiDeviceClass.XIScrollClass)
+ {
+ var classInfo = (XIScrollClassInfo*)info.Classes[index];
+ if (classInfo->ScrollType == XiScrollType.Horizontal)
+ {
+ (horizontalValuator, horizontalIncrement) = (classInfo->Number, classInfo->Increment);
+ }
+ else
+ {
+ (verticalValuator, verticalIncrement) = (classInfo->Number, classInfo->Increment);
+ }
+ }
+ }
+
+ var @out = new DeviceInfo(new ImmutableList(propsResult), horizontalValuator, horizontalIncrement, verticalValuator, verticalIncrement);
+ _deviceInfoCache[id] = @out;
+ return @out;
+ }
+ }
+
+ return null;
+ }
+
+ private record struct DeviceInfo(ImmutableList Properties, int? HorizontalValuator, double? HorizontalIncrement, int? VerticalValuator, double? VerticalIncrement);
+}
diff --git a/src/Uno.UI.Runtime.Skia.X11/X11PointerInputSource.cs b/src/Uno.UI.Runtime.Skia.X11/X11PointerInputSource.cs
index 8c9b224057ed..4aac2cd5eabf 100644
--- a/src/Uno.UI.Runtime.Skia.X11/X11PointerInputSource.cs
+++ b/src/Uno.UI.Runtime.Skia.X11/X11PointerInputSource.cs
@@ -137,67 +137,4 @@ private void LogNotSupported([CallerMemberName] string member = "")
this.Log().Debug($"{member} not supported on Skia for X11.");
}
}
-
- public void ProcessLeaveEvent(XCrossingEvent ev)
- {
- _mousePosition = new Point(ev.x, ev.y);
-
- var point = CreatePointFromCurrentState(ev.time);
- var modifiers = X11XamlRootHost.XModifierMaskToVirtualKeyModifiers(ev.state);
-
- var args = new PointerEventArgs(point, modifiers);
-
- CreatePointFromCurrentState(ev.time);
- X11XamlRootHost.QueueAction(_host, () => RaisePointerExited(args));
- }
-
- public void ProcessEnterEvent(XCrossingEvent ev)
- {
- _mousePosition = new Point(ev.x, ev.y);
-
- var args = CreatePointerEventArgsFromCurrentState(ev.time, ev.state);
- X11XamlRootHost.QueueAction(_host, () => RaisePointerEntered(args));
- }
-
- private PointerEventArgs CreatePointerEventArgsFromCurrentState(IntPtr time, XModifierMask state)
- {
- var point = CreatePointFromCurrentState(time);
- var modifiers = X11XamlRootHost.XModifierMaskToVirtualKeyModifiers(state);
-
- return new PointerEventArgs(point, modifiers);
- }
-
- ///
- /// Create a new PointerPoint from the current state of the PointerInputSource
- ///
- private PointerPoint CreatePointFromCurrentState(IntPtr time)
- {
- var properties = new PointerPointProperties
- {
- // TODO: fill this comprehensively like GTK's AsPointerArgs
- IsLeftButtonPressed = (_pressedButtons & (1 << LEFT)) != 0,
- IsMiddleButtonPressed = (_pressedButtons & (1 << MIDDLE)) != 0,
- IsRightButtonPressed = (_pressedButtons & (1 << RIGHT)) != 0
- };
-
- var scale = ((IXamlRootHost)_host).RootElement?.XamlRoot is { } root
- ? root.RasterizationScale
- : 1;
-
- // Time is given in milliseconds since system boot
- // This matches the format of WinUI. See also: https://github.com/unoplatform/uno/issues/14535
- var point = new PointerPoint(
- frameId: (uint)time, // UNO TODO: How should set the frame, timestamp may overflow.
- timestamp: (uint)time,
- PointerDevice.For(PointerDeviceType.Mouse),
- 0, // TODO: XInput
- new Point(_mousePosition.X / scale, _mousePosition.Y / scale),
- new Point(_mousePosition.X / scale, _mousePosition.Y / scale),
- // TODO: is isInContact correct?
- (_pressedButtons & 0b1111) != 0,
- properties
- );
-
- return point;
- }
}
diff --git a/src/Uno.UI.Runtime.Skia.X11/X11XamlRootHost.XInput.cs b/src/Uno.UI.Runtime.Skia.X11/X11XamlRootHost.XInput.cs
new file mode 100644
index 000000000000..7a8ecfd6e8f5
--- /dev/null
+++ b/src/Uno.UI.Runtime.Skia.X11/X11XamlRootHost.XInput.cs
@@ -0,0 +1,165 @@
+// Copyright 1996-1997 by Frederic Lepied, France.
+//
+// Permission to use, copy, modify, distribute, and sell this software and its
+// documentation for any purpose is hereby granted without fee, provided that
+// the above copyright notice appear in all copies and that both that
+// copyright notice and this permission notice appear in supporting
+// documentation, and that the name of the authors not be used in
+// advertising or publicity pertaining to distribution of the software without
+// specific, written prior permission. The authors make no
+// representations about the suitability of this software for any purpose. It
+// is provided "as is" without express or implied warranty.
+//
+// THE AUTHORS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
+// INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
+// EVENT SHALL THE AUTHORS BE LIABLE FOR ANY SPECIAL, INDIRECT OR
+// CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
+// DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
+// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+//
+// Copyright © 2007 Peter Hutterer
+// Copyright © 2009 Red Hat, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice (including the next
+// paragraph) shall be included in all copies or substantial portions of the
+// Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+// The MIT License (MIT)
+//
+// Copyright (c) .NET Foundation and Contributors
+// All Rights Reserved
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+// https://gitlab.freedesktop.org/xorg/app/xinput/-/blob/f550dfbb347ec62ca59e86f79a9e4fe43417ab39/src/xinput.c
+// https://gitlab.freedesktop.org/xorg/app/xinput/-/blob/f550dfbb347ec62ca59e86f79a9e4fe43417ab39/src/test_xi2.c
+// https://github.com/AvaloniaUI/Avalonia/blob/e0127c610c38701c3af34f580273f6efd78285b5/src/Avalonia.X11/XI2Manager.cs
+
+using System;
+namespace Uno.WinUI.Runtime.Skia.X11;
+
+// Excerpt from the spec :
+// Interoperability between version 1.x and 2.0
+// --------------------------------------------
+//
+// There is little interaction between 1.x and 2.x versions of the X Input
+// Extension. Clients are requested to avoid mixing XI1.x and XI2 code as much as
+// possible. Several direct incompatibilities are observable:
+
+// Accordingly, we only use version 2 of the extension and default to the core input events if
+// version 2 is not present. Note that XInput 2 was first released in 2009.
+internal partial class X11XamlRootHost
+{
+ private enum XIVersion
+ {
+ XI2_0,
+ XI2_1,
+ XI2_2,
+ XI2_3,
+ XI2_4,
+ Unsupported
+ }
+
+ // These should match X11XamlRootHost.EventsHandledByXI2Mask
+ private const int XI2Mask =
+ (1 << (int)XiEventType.XI_ButtonPress) |
+ (1 << (int)XiEventType.XI_ButtonRelease) |
+ (1 << (int)XiEventType.XI_Motion) |
+ (1 << (int)XiEventType.XI_Enter) |
+ (1 << (int)XiEventType.XI_Leave) |
+ (1 << (int)XiEventType.XI_DeviceChanged);
+
+ private const int XI2_2Mask =
+ (1 << (int)XiEventType.XI_TouchBegin) |
+ (1 << (int)XiEventType.XI_TouchUpdate) |
+ (1 << (int)XiEventType.XI_TouchEnd);
+
+ private (XIVersion, int)? _xi2Details;
+
+ private unsafe (XIVersion version, int opcode) GetXI2Details(IntPtr display)
+ {
+ if (_xi2Details is { } d)
+ {
+ return d;
+ }
+
+ if (!XLib.XQueryExtension(display, "XInputExtension", out var _xi2Opcode, out _, out _))
+ {
+ _xi2Details = (XIVersion.Unsupported, _xi2Opcode);
+ }
+
+ var version = X11Helper.XGetExtensionVersion(display, "XInputExtension");
+ if (version->major_version != 2)
+ {
+ _xi2Details = (XIVersion.Unsupported, _xi2Opcode);
+ }
+
+ _xi2Details = version->minor_version switch
+ {
+ 0 => (XIVersion.XI2_0, _xi2Opcode),
+ 1 => (XIVersion.XI2_1, _xi2Opcode),
+ 2 => (XIVersion.XI2_2, _xi2Opcode),
+ 3 => (XIVersion.XI2_3, _xi2Opcode),
+ 4 => (XIVersion.XI2_4, _xi2Opcode),
+ _ => throw new ArgumentException("XI2 version is not between 2.0 and 2.4. There should be no 2.5 or above.")
+ };
+
+ return _xi2Details.Value;
+ }
+
+ private unsafe void SetXIEventMask(X11Window x11Window)
+ {
+ var m = stackalloc XIEventMask[1];
+ m->Deviceid = (int)XiPredefinedDeviceId.XIAllDevices;
+ m->MaskLen = (int)XiEventType.XI_LASTEVENT;
+ // from XI2.h:
+ // #define XIMaskLen(event) (((event) >> 3) + 1)
+ // #define XI_GestureSwipeEnd 32
+ // #define XI_LASTEVENT XI_GestureSwipeEnd
+ // So XIMaskLen(XI_LASTEVENT) is always 4
+ // m->mask = calloc(m->mask_len, sizeof(char));
+ var mask = stackalloc int[1];
+ *mask |= XI2Mask;
+ if (GetXI2Details(x11Window.Display).version >= XIVersion.XI2_2)
+ {
+ *mask |= XI2_2Mask;
+ }
+ m->Mask = mask;
+ m->MaskLen = 4;
+ m->Deviceid = (int)XiPredefinedDeviceId.XIAllDevices;
+ var _1 = XLib.XISelectEvents(x11Window.Display, x11Window.Window, m, 1);
+ var _2 = XLib.XSync(x11Window.Display, false);
+ }
+}
diff --git a/src/Uno.UI.Runtime.Skia.X11/X11XamlRootHost.cs b/src/Uno.UI.Runtime.Skia.X11/X11XamlRootHost.cs
index 820cd3975901..be601ce73bb0 100644
--- a/src/Uno.UI.Runtime.Skia.X11/X11XamlRootHost.cs
+++ b/src/Uno.UI.Runtime.Skia.X11/X11XamlRootHost.cs
@@ -41,6 +41,13 @@ internal partial class X11XamlRootHost : IXamlRootHost
(IntPtr)EventMask.LeaveWindowMask |
(IntPtr)EventMask.FocusChangeMask |
(IntPtr)EventMask.NoEventMask;
+ // We only use XI2 for pointer stuff. We use the core protocol events for everything else.
+ private const IntPtr EventsHandledByXI2Mask =
+ (IntPtr)EventMask.ButtonPressMask |
+ (IntPtr)EventMask.PointerMotionMask |
+ (IntPtr)EventMask.EnterWindowMask |
+ (IntPtr)EventMask.LeaveWindowMask |
+ (IntPtr)EventMask.NoEventMask;
private static bool _firstWindowCreated;
private static readonly object _x11WindowToXamlRootHostMutex = new();
@@ -352,25 +359,27 @@ private void Initialize()
// For the root window (that does nothing but act as an anchor for children,
// we don't bother with OpenGL, since we don't render on this window anyway.
IntPtr rootXWindow = XLib.XRootWindow(display, screen);
- IntPtr rootUnoWindow = CreateSoftwareRenderWindow(display, screen, size, rootXWindow);
- XLib.XSelectInput(display, rootUnoWindow, RootEventsMask);
- XLib.XSelectInput(display, rootXWindow, (IntPtr)EventMask.PropertyChangeMask); // to update dpi when X resources change
- _x11Window = new X11Window(display, rootUnoWindow);
+ _x11Window = CreateSoftwareRenderWindow(display, screen, size, rootXWindow);
var topWindowDisplay = XLib.XOpenDisplay(IntPtr.Zero);
- if (FeatureConfiguration.Rendering.UseOpenGLOnX11 ?? IsOpenGLSupported(display))
- {
- _x11TopWindow = CreateGLXWindow(topWindowDisplay, screen, size, rootUnoWindow);
- }
- else
+ _x11TopWindow = FeatureConfiguration.Rendering.UseOpenGLOnX11 ?? IsOpenGLSupported(display)
+ ? CreateGLXWindow(topWindowDisplay, screen, size, RootX11Window.Window)
+ : CreateSoftwareRenderWindow(topWindowDisplay, screen, size, RootX11Window.Window);
+
+ var usingXi2 = GetXI2Details(display).version is not XIVersion.Unsupported;
+ if (usingXi2)
{
- var topWindow = CreateSoftwareRenderWindow(topWindowDisplay, screen, size, rootUnoWindow);
- XLib.XSelectInput(topWindowDisplay, topWindow, TopEventsMask);
- _x11TopWindow = new X11Window(display, topWindow);
+ SetXIEventMask(TopX11Window);
}
+ XLib.XSelectInput(RootX11Window.Display, RootX11Window.Window, RootEventsMask);
+ // to update dpi when X resources change
+ XLib.XSelectInput(RootX11Window.Display, rootXWindow, (IntPtr)EventMask.PropertyChangeMask);
+ // We make sure not to select events that will be handled by a corresponding XI2 event
+ XLib.XSelectInput(TopX11Window.Display, TopX11Window.Window, usingXi2 ? TopEventsMask & ~EventsHandledByXI2Mask : TopEventsMask);
+
// Tell the WM to send a WM_DELETE_WINDOW message before closing
IntPtr deleteWindow = X11Helper.GetAtom(display, X11Helper.WM_DELETE_WINDOW);
- _ = XLib.XSetWMProtocols(display, rootUnoWindow, new[] { deleteWindow }, 1);
+ _ = XLib.XSetWMProtocols(RootX11Window.Display, RootX11Window.Window, new[] { deleteWindow }, 1);
lock (_x11WindowToXamlRootHostMutex)
{
@@ -380,7 +389,7 @@ private void Initialize()
_ = X11Helper.XClearWindow(RootX11Window.Display, RootX11Window.Window); // the root window is never drawn, just always blank
- if (FeatureConfiguration.Rendering.UseOpenGLOnX11 ?? IsOpenGLSupported(display))
+ if (FeatureConfiguration.Rendering.UseOpenGLOnX11 ?? IsOpenGLSupported(TopX11Window.Display))
{
_renderer = new X11OpenGLRenderer(this, TopX11Window);
}
@@ -441,7 +450,6 @@ private unsafe static X11Window CreateGLXWindow(IntPtr display, int screen, Size
// Not sure why this is needed, commented out until further notice
// attribs.override_redirect = /* True */ 1;
attribs.colormap = XLib.XCreateColormap(display, parent, visual->visual, /* AllocNone */ 0);
- attribs.event_mask = TopEventsMask;
var window = XLib.XCreateWindow(
display,
parent,
@@ -461,7 +469,7 @@ private unsafe static X11Window CreateGLXWindow(IntPtr display, int screen, Size
return new X11Window(display, window, (stencil, samples, context));
}
- private static IntPtr CreateSoftwareRenderWindow(IntPtr display, int screen, Size size, IntPtr parent)
+ private static X11Window CreateSoftwareRenderWindow(IntPtr display, int screen, Size size, IntPtr parent)
{
var matchVisualInfoResult = XLib.XMatchVisualInfo(display, screen, DefaultColorDepth, 4, out var info);
var success = matchVisualInfoResult != 0;
@@ -509,7 +517,7 @@ private static IntPtr CreateSoftwareRenderWindow(IntPtr display, int screen, Siz
var window = XLib.XCreateWindow(display, parent, 0, 0, (int)size.Width,
(int)size.Height, 0, (int)depth, /* InputOutput */ 1, visual,
(UIntPtr)(valueMask), ref xSetWindowAttributes);
- return window;
+ return new X11Window(display, window);
}
private bool IsOpenGLSupported(IntPtr display)
diff --git a/src/Uno.UI.Runtime.Skia.X11/X11XamlRootHost.x11events.cs b/src/Uno.UI.Runtime.Skia.X11/X11XamlRootHost.x11events.cs
index 706b96981010..0403a19bb2a2 100644
--- a/src/Uno.UI.Runtime.Skia.X11/X11XamlRootHost.x11events.cs
+++ b/src/Uno.UI.Runtime.Skia.X11/X11XamlRootHost.x11events.cs
@@ -1,12 +1,9 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Globalization;
using System.Threading;
-using Windows.Foundation;
using Windows.System;
using Windows.UI.Core;
-using Microsoft.UI.Xaml;
using Uno.Foundation.Logging;
using Uno.UI.Hosting;
@@ -176,7 +173,8 @@ static IEnumerable GetEvents(IntPtr display)
break;
}
}
- else if (@event.AnyEvent.window == x11Window.Window)
+ else if (@event.AnyEvent.window == x11Window.Window ||
+ (@event.type is XEventName.GenericEvent && @event.GenericEventCookie.extension == GetXI2Details(x11Window.Window).opcode))
{
switch (@event.type)
{
@@ -228,6 +226,26 @@ static IEnumerable GetEvents(IntPtr display)
case XEventName.EnterNotify:
_pointerSource?.ProcessEnterEvent(@event.CrossingEvent);
break;
+ case XEventName.GenericEvent:
+ var eventWithData = @event;
+ var cookiePtr = &eventWithData.GenericEventCookie;
+ var getEventDataSucceeded = XLib.XGetEventData(TopX11Window.Display, cookiePtr);
+
+ try
+ {
+ if (getEventDataSucceeded && _pointerSource is { } pointerSource)
+ {
+ pointerSource.HandleXI2Event(eventWithData);
+ }
+ }
+ finally
+ {
+ if (getEventDataSucceeded)
+ {
+ XLib.XFreeEventData(TopX11Window.Display, cookiePtr);
+ }
+ }
+ break;
case XEventName.KeyPress:
_keyboardSource?.ProcessKeyboardEvent(@event.KeyEvent, true);
break;
diff --git a/src/Uno.UI.Runtime.Skia.X11/X11_Bindings/X11Helper.cs b/src/Uno.UI.Runtime.Skia.X11/X11_Bindings/X11Helper.cs
index fef883d516e8..4a1f549f404b 100644
--- a/src/Uno.UI.Runtime.Skia.X11/X11_Bindings/X11Helper.cs
+++ b/src/Uno.UI.Runtime.Skia.X11/X11_Bindings/X11Helper.cs
@@ -41,6 +41,8 @@ internal static partial class X11Helper
private const string libX11 = "libX11.so.6";
private const string libX11Randr = "libXrandr.so.2";
private const string libXext = "libXext.so.6";
+ private const string libXInput = "libXi.so.6";
+
public static readonly IntPtr CurrentTime = IntPtr.Zero;
public static readonly IntPtr None = IntPtr.Zero;
@@ -393,6 +395,12 @@ public unsafe static partial int XChangeWindowAttributes(
[LibraryImport(libX11)]
public static partial int XHeightOfScreen(IntPtr screen);
+ [DllImport(libXInput)]
+ public static extern unsafe XExtensionVersion* XGetExtensionVersion(IntPtr display, string name);
+
+ [LibraryImport(libXInput)]
+ public static unsafe partial IntPtr* XIListProperties(IntPtr display, int deviceid, out int num_props_return);
+
[LibraryImport(libX11)]
public static partial IntPtr XResourceManagerString(IntPtr display);
@@ -631,4 +639,14 @@ public struct Pollfd
public short events;
public short revents;
}
+
+ [StructLayout(LayoutKind.Sequential)]
+#pragma warning disable CA1815 // Override equals and operator equals on value types
+ public struct XExtensionVersion
+#pragma warning restore CA1815 // Override equals and operator equals on value types
+ {
+ public int present;
+ public short major_version;
+ public short minor_version;
+ }
}
diff --git a/src/Uno.UI.Runtime.Skia.X11/X11_Bindings/x11Bindings_XInput.cs b/src/Uno.UI.Runtime.Skia.X11/X11_Bindings/x11Bindings_XInput.cs
new file mode 100644
index 000000000000..eda3bc06cd6a
--- /dev/null
+++ b/src/Uno.UI.Runtime.Skia.X11/X11_Bindings/x11Bindings_XInput.cs
@@ -0,0 +1,354 @@
+// The MIT License (MIT)
+//
+// Copyright (c) .NET Foundation and Contributors
+// All Rights Reserved
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+// https://github.com/AvaloniaUI/Avalonia/blob/3b961906e43904d41b4e88b4d33ea9bd9a976ee5/src/Avalonia.X11/XIStructs.cs
+
+using System;
+using System.Runtime.InteropServices;
+using Bool = System.Boolean;
+using Atom = System.IntPtr;
+// ReSharper disable IdentifierTypo
+// ReSharper disable FieldCanBeMadeReadOnly.Global
+// ReSharper disable MemberCanBePrivate.Global
+#pragma warning disable 649
+
+namespace Uno.WinUI.Runtime.Skia.X11
+{
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct XIAddMasterInfo
+ {
+ public int Type;
+ public IntPtr Name;
+ public Bool SendCore;
+ public Bool Enable;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct XIRemoveMasterInfo
+ {
+ public int Type;
+ public int Deviceid;
+ public int ReturnMode; /* AttachToMaster, Floating */
+ public int ReturnPointer;
+ public int ReturnKeyboard;
+ };
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct XIAttachSlaveInfo
+ {
+ public int Type;
+ public int Deviceid;
+ public int NewMaster;
+ };
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct XIDetachSlaveInfo
+ {
+ public int Type;
+ public int Deviceid;
+ };
+
+ [StructLayout(LayoutKind.Explicit)]
+ internal struct XIAnyHierarchyChangeInfo
+ {
+ [FieldOffset(0)]
+ public int type; /* must be first element */
+ [FieldOffset(4)]
+ public XIAddMasterInfo add;
+ [FieldOffset(4)]
+ public XIRemoveMasterInfo remove;
+ [FieldOffset(4)]
+ public XIAttachSlaveInfo attach;
+ [FieldOffset(4)]
+ public XIDetachSlaveInfo detach;
+ };
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct XIModifierState
+ {
+ public int Base;
+ public int Latched;
+ public int Locked;
+ public int Effective;
+ };
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal unsafe struct XIButtonState
+ {
+ public int MaskLen;
+ public byte* Mask;
+ };
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal unsafe struct XIValuatorState
+ {
+ public int MaskLen;
+ public byte* Mask;
+ public double* Values;
+ };
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal unsafe struct XIEventMask
+ {
+ public int Deviceid;
+ public int MaskLen;
+ public int* Mask;
+ };
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct XIAnyClassInfo
+ {
+ public XiDeviceClass Type;
+ public int Sourceid;
+ };
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal unsafe struct XIButtonClassInfo
+ {
+ public int Type;
+ public int Sourceid;
+ public int NumButtons;
+ public IntPtr* Labels;
+ public XIButtonState State;
+ };
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal unsafe struct XIKeyClassInfo
+ {
+ public int Type;
+ public int Sourceid;
+ public int NumKeycodes;
+ public int* Keycodes;
+ };
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct XIValuatorClassInfo
+ {
+ public int Type;
+ public int Sourceid;
+ public int Number;
+ public IntPtr Label;
+ public double Min;
+ public double Max;
+ public double Value;
+ public int Resolution;
+ public int Mode;
+ };
+
+ /* new in XI 2.1 */
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct XIScrollClassInfo
+ {
+ public int Type;
+ public int Sourceid;
+ public int Number;
+ public XiScrollType ScrollType;
+ public double Increment;
+ public int Flags;
+ };
+
+ internal enum XiScrollType
+ {
+ Vertical = 1,
+ Horizontal = 2
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct XITouchClassInfo
+ {
+ public int Type;
+ public int Sourceid;
+ public int Mode;
+ public int NumTouches;
+ };
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal unsafe struct XIDeviceInfo
+ {
+ public int Deviceid;
+ public IntPtr Name;
+ public XiDeviceType Use;
+ public int Attachment;
+ public Bool Enabled;
+ public int NumClasses;
+ public XIAnyClassInfo** Classes;
+ }
+
+ internal enum XiDeviceType
+ {
+ XIMasterPointer = 1,
+ XIMasterKeyboard = 2,
+ XISlavePointer = 3,
+ XISlaveKeyboard = 4,
+ XIFloatingSlave = 5
+ }
+
+ internal enum XiPredefinedDeviceId : int
+ {
+ XIAllDevices = 0,
+ XIAllMasterDevices = 1
+ }
+
+ internal enum XiDeviceClass
+ {
+ XIKeyClass = 0,
+ XIButtonClass = 1,
+ XIValuatorClass = 2,
+ XIScrollClass = 3,
+ XITouchClass = 8,
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal unsafe struct XIDeviceChangedEvent
+ {
+ public int Type; /* GenericEvent */
+ public UIntPtr Serial; /* # of last request processed by server */
+ public Bool SendEvent; /* true if this came from a SendEvent request */
+ public IntPtr Display; /* Display the event was read from */
+ public int Extension; /* XI extension offset */
+ public int Evtype; /* XI_DeviceChanged */
+ 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 int NumClasses;
+ public XIAnyClassInfo** Classes; /* same as in XIDeviceInfo */
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct XIDeviceEvent
+ {
+ public XEventName type; /* GenericEvent */
+ public UIntPtr serial; /* # of last request processed by server */
+ public Bool send_event; /* true if this came from a SendEvent request */
+ public IntPtr display; /* Display the event was read from */
+ public int extension; /* XI extension offset */
+ public XiEventType evtype;
+ public IntPtr time;
+ public int deviceid;
+ public int sourceid;
+ public int detail;
+ public IntPtr RootWindow;
+ public IntPtr EventWindow;
+ public IntPtr ChildWindow;
+ public double root_x;
+ public double root_y;
+ public double event_x;
+ public double event_y;
+ public XiDeviceEventFlags flags;
+ public XIButtonState buttons;
+ public XIValuatorState valuators;
+ public XIModifierState mods;
+ public XIModifierState group;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct XIEnterLeaveEvent
+ {
+ public XEventName type; /* GenericEvent */
+ public UIntPtr serial; /* # of last request processed by server */
+ public Bool send_event; /* true if this came from a SendEvent request */
+ public IntPtr display; /* Display the event was read from */
+ public int extension; /* XI extension offset */
+ public XiEventType evtype;
+ public IntPtr time;
+ public int deviceid;
+ public int sourceid;
+ public XiEnterLeaveDetail detail;
+ public IntPtr RootWindow;
+ public IntPtr EventWindow;
+ public IntPtr ChildWindow;
+ public double root_x;
+ public double root_y;
+ public double event_x;
+ public double event_y;
+ public int mode;
+ public int focus;
+ public int same_screen;
+ public XIButtonState buttons;
+ public XIModifierState mods;
+ public XIModifierState group;
+ }
+
+ [Flags]
+ internal enum XiDeviceEventFlags : int
+ {
+ None = 0,
+ XIPointfocuserEmulated = (1 << 16)
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal unsafe struct XIEvent
+ {
+ public int type; /* GenericEvent */
+ public UIntPtr serial; /* # of last request processed by server */
+ public Bool send_event; /* true if this came from a SendEvent request */
+ public IntPtr display; /* Display the event was read from */
+ public int extension; /* XI extension offset */
+ public XiEventType evtype;
+ public IntPtr time;
+ }
+
+ internal enum XiEventType
+ {
+ XI_DeviceChanged = 1,
+ XI_KeyPress = 2,
+ XI_KeyRelease = 3,
+ XI_ButtonPress = 4,
+ XI_ButtonRelease = 5,
+ XI_Motion = 6,
+ XI_Enter = 7,
+ XI_Leave = 8,
+ XI_FocusIn = 9,
+ XI_FocusOut = 10,
+ XI_HierarchyChanged = 11,
+ XI_PropertyEvent = 12,
+ XI_RawKeyPress = 13,
+ XI_RawKeyRelease = 14,
+ XI_RawButtonPress = 15,
+ XI_RawButtonRelease = 16,
+ XI_RawMotion = 17,
+ XI_TouchBegin = 18 /* XI 2.2 */,
+ XI_TouchUpdate = 19,
+ XI_TouchEnd = 20,
+ XI_TouchOwnership = 21,
+ XI_RawTouchBegin = 22,
+ XI_RawTouchUpdate = 23,
+ XI_RawTouchEnd = 24,
+ XI_BarrierHit = 25 /* XI 2.3 */,
+ XI_BarrierLeave = 26,
+ XI_LASTEVENT = XI_BarrierLeave,
+ }
+
+ internal enum XiEnterLeaveDetail
+ {
+ XINotifyAncestor = 0,
+ XINotifyVirtual = 1,
+ XINotifyInferior = 2,
+ XINotifyNonlinear = 3,
+ XINotifyNonlinearVirtual = 4,
+ XINotifyPointer = 5,
+ XINotifyPointerRoot = 6,
+ XINotifyDetailNone = 7
+ }
+}
diff --git a/src/Uno.UI.Runtime.Skia.X11/X11_Bindings/x11bindings_XLib.cs b/src/Uno.UI.Runtime.Skia.X11/X11_Bindings/x11bindings_XLib.cs
index 33d3af8195b4..37cecfdcd9ee 100644
--- a/src/Uno.UI.Runtime.Skia.X11/X11_Bindings/x11bindings_XLib.cs
+++ b/src/Uno.UI.Runtime.Skia.X11/X11_Bindings/x11bindings_XLib.cs
@@ -45,6 +45,7 @@ public unsafe static partial class XLib
{
private const string libX11 = "libX11.so.6";
private const string libX11Randr = "libXrandr.so.2";
+ private const string libXInput = "libXi.so.6";
[LibraryImport(libX11)]
public static partial IntPtr XOpenDisplay(IntPtr display);
@@ -81,6 +82,37 @@ public static partial IntPtr XCreateWindow(IntPtr display, IntPtr parent, int x,
[LibraryImport(libX11)]
public static partial int XPending(IntPtr diplay);
+ [LibraryImport(libX11)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool XQueryExtension(IntPtr display, [MarshalAs(UnmanagedType.LPStr)] string name,
+ out int majorOpcode, out int firstEvent, out int firstError);
+
+ [LibraryImport(libXInput)]
+ internal static partial int XIQueryVersion(IntPtr dpy, ref int major, ref int minor);
+
+ [LibraryImport(libXInput)]
+ internal static unsafe partial XIDeviceInfo* XIQueryDevice(IntPtr dpy, int deviceid, out int ndevices_return);
+
+ [LibraryImport(libXInput)]
+ internal static unsafe partial void XIFreeDeviceInfo(XIDeviceInfo* info);
+
+ internal static bool XIMaskIsSet(void* ptr, int shift) =>
+ (((byte*)ptr)[shift >> 3] & (1 << (shift & 7))) != 0;
+
+ [LibraryImport(libXInput)]
+ internal static unsafe partial int XISelectEvents(
+ IntPtr dpy,
+ IntPtr win,
+ XIEventMask* masks,
+ int num_masks
+ );
+
+ [DllImport(libX11)]
+ internal static extern unsafe bool XGetEventData(IntPtr display, XGenericEventCookie* cookie);
+
+ [LibraryImport(libX11)]
+ internal static unsafe partial void XFreeEventData(IntPtr display, void* cookie);
+
[LibraryImport(libX11)]
public static partial IntPtr XSelectInput(IntPtr display, IntPtr window, IntPtr mask);
diff --git a/src/Uno.UWP/UI/Input/PointerPointProperties.cs b/src/Uno.UWP/UI/Input/PointerPointProperties.cs
index 8c3c9a576b13..9207761b871d 100644
--- a/src/Uno.UWP/UI/Input/PointerPointProperties.cs
+++ b/src/Uno.UWP/UI/Input/PointerPointProperties.cs
@@ -80,6 +80,11 @@ public static explicit operator Windows.UI.Input.PointerPointProperties(Microsof
public bool IsInRange { get; internal set; }
+ ///
+ /// This is necessary for InteractionTracker, which behaves differently on mouse, touch and trackpad inputs.
+ ///
+ internal bool IsTouchPad { get; set; }
+
public bool IsLeftButtonPressed { get; internal set; }
public bool IsMiddleButtonPressed { get; internal set; }