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