ソースを参照

Merge pull request #2581 from AvaloniaUI/multitouch

Multitouch support (Win32/X11)
Nikita Tsukanov 6 年 前
コミット
c978cb39ab
51 ファイル変更1074 行追加550 行削除
  1. 3 2
      dirs.proj
  2. 2 0
      samples/ControlCatalog.NetCore/Program.cs
  3. 1 0
      samples/ControlCatalog/MainView.xaml
  4. 99 0
      samples/ControlCatalog/Pages/PointersPage.cs
  5. 8 8
      src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs
  6. 0 13
      src/Avalonia.Controls/Calendar/CalendarButton.cs
  7. 0 13
      src/Avalonia.Controls/Calendar/CalendarDayButton.cs
  8. 1 31
      src/Avalonia.Controls/Calendar/CalendarItem.cs
  9. 6 12
      src/Avalonia.Controls/MenuItem.cs
  10. 2 2
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  11. 11 11
      src/Avalonia.Controls/Platform/InProcessDragSource.cs
  12. 2 2
      src/Avalonia.Controls/Primitives/Popup.cs
  13. 9 9
      src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs
  14. 5 0
      src/Avalonia.Input/IMouseDevice.cs
  15. 18 0
      src/Avalonia.Input/IPointer.cs
  16. 5 3
      src/Avalonia.Input/IPointerDevice.cs
  17. 86 71
      src/Avalonia.Input/MouseDevice.cs
  18. 63 0
      src/Avalonia.Input/Pointer.cs
  19. 72 23
      src/Avalonia.Input/PointerEventArgs.cs
  20. 45 0
      src/Avalonia.Input/PointerPoint.cs
  21. 11 0
      src/Avalonia.Input/PointerWheelEventArgs.cs
  22. 2 2
      src/Avalonia.Input/Raw/RawMouseWheelEventArgs.cs
  23. 9 6
      src/Avalonia.Input/Raw/RawPointerEventArgs.cs
  24. 15 0
      src/Avalonia.Input/Raw/RawTouchEventArgs.cs
  25. 72 0
      src/Avalonia.Input/TouchDevice.cs
  26. 1 1
      src/Avalonia.Native/WindowImplBase.cs
  27. 3 1
      src/Avalonia.X11/X11Platform.cs
  28. 13 13
      src/Avalonia.X11/X11Window.cs
  29. 51 18
      src/Avalonia.X11/XI2Manager.cs
  30. 8 1
      src/Avalonia.X11/XIStructs.cs
  31. 9 9
      src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs
  32. 9 9
      src/Linux/Avalonia.LinuxFramebuffer/Mice.cs
  33. 12 12
      src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs
  34. 72 0
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  35. 5 2
      src/Windows/Avalonia.Win32/Win32Platform.cs
  36. 61 17
      src/Windows/Avalonia.Win32/WindowImpl.cs
  37. 1 0
      src/iOS/Avalonia.iOS/Avalonia.iOS.csproj
  38. 8 8
      src/iOS/Avalonia.iOS/TopLevelImpl.cs
  39. 34 72
      tests/Avalonia.Controls.UnitTests/ButtonTests.cs
  40. 5 9
      tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
  41. 8 35
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
  42. 3 6
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  43. 9 32
      tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs
  44. 116 0
      tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs
  45. 40 14
      tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs
  46. 4 11
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  47. 35 54
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  48. 2 2
      tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs
  49. 2 0
      tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj
  50. 9 10
      tests/Avalonia.Interactivity.UnitTests/GestureTests.cs
  51. 7 6
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs

+ 3 - 2
dirs.proj

@@ -8,10 +8,11 @@
     <ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml/PortableXaml/**/*.*proj" />
     <ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml/XamlIl/**/*.*proj" />
   </ItemGroup>
-<!-- Disabled on CI because of ancient MSBuild project format -->
-  <ItemGroup>
+  <ItemGroup Condition="!Exists('$(MSBuildExtensionsPath)\Xamarin\Android')">
     <ProjectReference Remove="src/Android/**/*.*proj" />
     <ProjectReference Remove="samples/ControlCatalog.Android/ControlCatalog.Android.csproj" />
+  </ItemGroup>
+  <ItemGroup Condition="!Exists('$(MSBuildExtensionsPath)\Xamarin\iOS') Or $([MSBuild]::IsOsPlatform('Windows')) ">
     <ProjectReference Remove="src/iOS/**/*.*proj" />
     <ProjectReference Remove="samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj" />
   </ItemGroup>

+ 2 - 0
samples/ControlCatalog.NetCore/Program.cs

@@ -46,6 +46,8 @@ namespace ControlCatalog.NetCore
         public static AppBuilder BuildAvaloniaApp()
             => AppBuilder.Configure<App>()
                 .UsePlatformDetect()
+                .With(new X11PlatformOptions {EnableMultiTouch = true})
+                .With(new Win32PlatformOptions {EnableMultitouch = true})
                 .UseSkia()
                 .UseReactiveUI();
 

+ 1 - 0
samples/ControlCatalog/MainView.xaml

@@ -31,6 +31,7 @@
       <TabItem Header="Menu"><pages:MenuPage/></TabItem>
       <TabItem Header="Notifications"><pages:NotificationsPage/></TabItem>
 	  <TabItem Header="NumericUpDown"><pages:NumericUpDownPage/></TabItem>
+      <TabItem Header="Pointers (Touch)"><pages:PointersPage/></TabItem>
       <TabItem Header="ProgressBar"><pages:ProgressBarPage/></TabItem>
       <TabItem Header="RadioButton"><pages:RadioButtonPage/></TabItem>
       <TabItem Header="Slider"><pages:SliderPage/></TabItem>

+ 99 - 0
samples/ControlCatalog/Pages/PointersPage.cs

@@ -0,0 +1,99 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Media;
+using Avalonia.Media.Immutable;
+
+namespace ControlCatalog.Pages
+{
+    public class PointersPage : Control
+    {
+        class PointerInfo
+        {
+            public Point Point { get; set; }
+            public Color Color { get; set; }
+        }
+
+        private static Color[] AllColors = new[]
+        {
+            Colors.Aqua,
+            Colors.Beige, 
+            Colors.Chartreuse, 
+            Colors.Coral,
+            Colors.Fuchsia,
+            Colors.Crimson,
+            Colors.Lavender, 
+            Colors.Orange,
+            Colors.Orchid,
+            Colors.ForestGreen,
+            Colors.SteelBlue,
+            Colors.PapayaWhip,
+            Colors.PaleVioletRed,
+            Colors.Goldenrod,
+            Colors.Maroon,
+            Colors.Moccasin,
+            Colors.Navy,
+            Colors.Wheat,
+            Colors.Violet,
+            Colors.Sienna,
+            Colors.Indigo,
+            Colors.Honeydew
+        };
+        
+        private Dictionary<IPointer, PointerInfo> _pointers = new Dictionary<IPointer, PointerInfo>();
+
+        public PointersPage()
+        {
+            ClipToBounds = true;
+        }
+        
+        void UpdatePointer(PointerEventArgs e)
+        {
+            if (!_pointers.TryGetValue(e.Pointer, out var info))
+            {
+                if (e.RoutedEvent == PointerMovedEvent)
+                    return;
+                var colors = AllColors.Except(_pointers.Values.Select(c => c.Color)).ToArray();
+                var color = colors[new Random().Next(0, colors.Length - 1)];
+                _pointers[e.Pointer] = info = new PointerInfo {Color = color};
+            }
+
+            info.Point = e.GetPosition(this);
+            InvalidateVisual();
+        }
+        
+        protected override void OnPointerPressed(PointerPressedEventArgs e)
+        {
+            UpdatePointer(e);
+            e.Pointer.Capture(this);
+            base.OnPointerPressed(e);
+        }
+
+        protected override void OnPointerMoved(PointerEventArgs e)
+        {
+            UpdatePointer(e);
+            base.OnPointerMoved(e);
+        }
+
+        protected override void OnPointerReleased(PointerReleasedEventArgs e)
+        {
+            _pointers.Remove(e.Pointer);
+            InvalidateVisual();
+        }
+
+        public override void Render(DrawingContext context)
+        {
+            context.FillRectangle(Brushes.Transparent, new Rect(default, Bounds.Size));
+            foreach (var pt in _pointers.Values)
+            {
+                var brush = new ImmutableSolidColorBrush(pt.Color);
+                context.DrawGeometry(brush, null, new EllipseGeometry(new Rect(pt.Point.X - 75, pt.Point.Y - 75,
+                    150, 150)));
+            }
+            
+        }
+    }
+}

+ 8 - 8
src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs

@@ -33,7 +33,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers
                 return null;
             }
 
-            RawMouseEventType? mouseEventType = null;
+            RawPointerEventType? mouseEventType = null;
             var eventTime = DateTime.Now;
             //Basic touch support
             switch (e.Action)
@@ -42,17 +42,17 @@ namespace Avalonia.Android.Platform.Specific.Helpers
                     //may be bot flood the evnt system with too many event especially on not so powerfull mobile devices
                     if ((eventTime - _lastTouchMoveEventTime).TotalMilliseconds > 10)
                     {
-                        mouseEventType = RawMouseEventType.Move;
+                        mouseEventType = RawPointerEventType.Move;
                     }
                     break;
 
                 case MotionEventActions.Down:
-                    mouseEventType = RawMouseEventType.LeftButtonDown;
+                    mouseEventType = RawPointerEventType.LeftButtonDown;
 
                     break;
 
                 case MotionEventActions.Up:
-                    mouseEventType = RawMouseEventType.LeftButtonUp;
+                    mouseEventType = RawPointerEventType.LeftButtonUp;
                     break;
             }
 
@@ -75,14 +75,14 @@ namespace Avalonia.Android.Platform.Specific.Helpers
                     //we need to generate mouse move before first mouse down event
                     //as this is the way buttons are working every time
                     //otherwise there is a problem sometimes
-                    if (mouseEventType == RawMouseEventType.LeftButtonDown)
+                    if (mouseEventType == RawPointerEventType.LeftButtonDown)
                     {
-                        var me = new RawMouseEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot,
-                                    RawMouseEventType.Move, _point, InputModifiers.None);
+                        var me = new RawPointerEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot,
+                                    RawPointerEventType.Move, _point, InputModifiers.None);
                         _view.Input(me);
                     }
 
-                    var mouseEvent = new RawMouseEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot,
+                    var mouseEvent = new RawPointerEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot,
                         mouseEventType.Value, _point, InputModifiers.LeftMouseButton);
                     _view.Input(mouseEvent);
 

+ 0 - 13
src/Avalonia.Controls/Calendar/CalendarButton.cs

@@ -176,18 +176,5 @@ namespace Avalonia.Controls.Primitives
             if (e.MouseButton == MouseButton.Left)
                 CalendarLeftMouseButtonUp?.Invoke(this, e);
         }
-        
-        /// <summary>
-        /// We need to simulate the MouseLeftButtonUp event for the
-        /// CalendarButton that stays in Pressed state after MouseCapture is
-        /// released since there is no actual MouseLeftButtonUp event for the
-        /// release.
-        /// </summary>
-        /// <param name="e">Event arguments.</param>
-        internal void SendMouseLeftButtonUp(PointerReleasedEventArgs e)
-        {
-            e.Handled = false;
-            base.OnPointerReleased(e);
-        }
     }
 }

+ 0 - 13
src/Avalonia.Controls/Calendar/CalendarDayButton.cs

@@ -234,18 +234,5 @@ namespace Avalonia.Controls.Primitives
             if (e.MouseButton == MouseButton.Left)
                 CalendarDayButtonMouseUp?.Invoke(this, e);
         }
-
-        /// <summary>
-        /// We need to simulate the MouseLeftButtonUp event for the
-        /// CalendarDayButton that stays in Pressed state after MouseCapture is
-        /// released since there is no actual MouseLeftButtonUp event for the
-        /// release.
-        /// </summary>
-        /// <param name="e">Event arguments.</param>
-        internal void SendMouseLeftButtonUp(PointerReleasedEventArgs e)
-        {
-            e.Handled = false;
-            base.OnPointerReleased(e);
-        }
     }
 }

+ 1 - 31
src/Avalonia.Controls/Calendar/CalendarItem.cs

@@ -934,22 +934,6 @@ namespace Avalonia.Controls.Primitives
                 // The button is in Pressed state. Change the state to normal.
                 if (e.Device.Captured == b)
                     e.Device.Capture(null);
-                // null check is added for unit tests
-                if (_downEventArg != null)
-                {
-                    var arg =
-                        new PointerReleasedEventArgs()
-                        {
-                            Device = _downEventArg.Device,
-                            MouseButton = _downEventArg.MouseButton,
-                            Handled = _downEventArg.Handled,
-                            InputModifiers = _downEventArg.InputModifiers,
-                            Route = _downEventArg.Route,
-                            Source = _downEventArg.Source
-                        };
-
-                    b.SendMouseLeftButtonUp(arg);
-                }
                 _lastCalendarDayButton = b;
             }
         }
@@ -1221,21 +1205,7 @@ namespace Avalonia.Controls.Primitives
                 if (e.Device.Captured == b)
                     e.Device.Capture(null);
                 //b.ReleaseMouseCapture();
-                if (_downEventArgYearView != null)
-                {
-                    var args =
-                        new PointerReleasedEventArgs()
-                        {
-                            Device = _downEventArgYearView.Device,
-                            MouseButton = _downEventArgYearView.MouseButton,
-                            Handled = _downEventArgYearView.Handled,
-                            InputModifiers = _downEventArgYearView.InputModifiers,
-                            Route = _downEventArgYearView.Route,
-                            Source = _downEventArgYearView.Source
-                        };
-
-                    b.SendMouseLeftButtonUp(args);
-                }
+
                 _lastCalendarButton = b;
             }
         }

+ 6 - 12
src/Avalonia.Controls/MenuItem.cs

@@ -337,12 +337,9 @@ namespace Avalonia.Controls
         {
             base.OnPointerEnter(e);
 
-            RaiseEvent(new PointerEventArgs
-            {
-                Device = e.Device,
-                RoutedEvent = PointerEnterItemEvent,
-                Source = this,
-            });
+            var point = e.GetPointerPoint(null);
+            RaiseEvent(new PointerEventArgs(PointerEnterItemEvent, this, e.Pointer, this.VisualRoot, point.Position,
+                point.Properties, e.InputModifiers));
         }
 
         /// <inheritdoc/>
@@ -350,12 +347,9 @@ namespace Avalonia.Controls
         {
             base.OnPointerLeave(e);
 
-            RaiseEvent(new PointerEventArgs
-            {
-                Device = e.Device,
-                RoutedEvent = PointerLeaveItemEvent,
-                Source = this,
-            });
+            var point = e.GetPointerPoint(null);
+            RaiseEvent(new PointerEventArgs(PointerLeaveItemEvent, this, e.Pointer, this.VisualRoot, point.Position,
+                point.Properties, e.InputModifiers));
         }
 
         /// <summary>

+ 2 - 2
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@@ -373,9 +373,9 @@ namespace Avalonia.Controls.Platform
 
         protected internal virtual void RawInput(RawInputEventArgs e)
         {
-            var mouse = e as RawMouseEventArgs;
+            var mouse = e as RawPointerEventArgs;
 
-            if (mouse?.Type == RawMouseEventType.NonClientLeftButtonDown)
+            if (mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
             {
                 Menu.Close();
             }

+ 11 - 11
src/Avalonia.Controls/Platform/InProcessDragSource.cs

@@ -43,7 +43,7 @@ namespace Avalonia.Platform
                 _lastPosition = default(Point);
                 _allowedEffects = allowedEffects;
 
-                using (_inputManager.PreProcess.OfType<RawMouseEventArgs>().Subscribe(ProcessMouseEvents))
+                using (_inputManager.PreProcess.OfType<RawPointerEventArgs>().Subscribe(ProcessMouseEvents))
                 {
                     using (_inputManager.PreProcess.OfType<RawKeyEventArgs>().Subscribe(ProcessKeyEvents))
                     {
@@ -153,7 +153,7 @@ namespace Avalonia.Platform
             }
         }
 
-        private void ProcessMouseEvents(RawMouseEventArgs e)
+        private void ProcessMouseEvents(RawPointerEventArgs e)
         {
             if (!_initialInputModifiers.HasValue)
                 _initialInputModifiers = e.InputModifiers & MOUSE_INPUTMODIFIERS;
@@ -174,22 +174,22 @@ namespace Avalonia.Platform
             
             switch (e.Type)
             {
-                case RawMouseEventType.LeftButtonDown:
-                case RawMouseEventType.RightButtonDown:
-                case RawMouseEventType.MiddleButtonDown:
-                case RawMouseEventType.NonClientLeftButtonDown:
+                case RawPointerEventType.LeftButtonDown:
+                case RawPointerEventType.RightButtonDown:
+                case RawPointerEventType.MiddleButtonDown:
+                case RawPointerEventType.NonClientLeftButtonDown:
                     CancelDragging();
                     e.Handled = true;
                     return;
-                case RawMouseEventType.LeaveWindow:
+                case RawPointerEventType.LeaveWindow:
                     RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, e.Root, e.Position,  e.InputModifiers); break;
-                case RawMouseEventType.LeftButtonUp:
+                case RawPointerEventType.LeftButtonUp:
                     CheckDraggingAccepted(InputModifiers.LeftMouseButton); break;
-                case RawMouseEventType.MiddleButtonUp:
+                case RawPointerEventType.MiddleButtonUp:
                     CheckDraggingAccepted(InputModifiers.MiddleMouseButton); break;
-                case RawMouseEventType.RightButtonUp:
+                case RawPointerEventType.RightButtonUp:
                     CheckDraggingAccepted(InputModifiers.RightMouseButton); break;
-                case RawMouseEventType.Move:
+                case RawPointerEventType.Move:
                     var mods = e.InputModifiers & MOUSE_INPUTMODIFIERS;
                     if (_initialInputModifiers.Value != mods)
                     {

+ 2 - 2
src/Avalonia.Controls/Primitives/Popup.cs

@@ -421,9 +421,9 @@ namespace Avalonia.Controls.Primitives
 
         private void ListenForNonClientClick(RawInputEventArgs e)
         {
-            var mouse = e as RawMouseEventArgs;
+            var mouse = e as RawPointerEventArgs;
 
-            if (!StaysOpen && mouse?.Type == RawMouseEventType.NonClientLeftButtonDown)
+            if (!StaysOpen && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
             {
                 Close();
             }

+ 9 - 9
src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs

@@ -39,21 +39,21 @@ namespace Avalonia.Controls.Remote.Server
             KeyboardDevice = AvaloniaLocator.Current.GetService<IKeyboardDevice>();
         }
 
-        private static RawMouseEventType GetAvaloniaEventType (Avalonia.Remote.Protocol.Input.MouseButton button, bool pressed)
+        private static RawPointerEventType GetAvaloniaEventType (Avalonia.Remote.Protocol.Input.MouseButton button, bool pressed)
         {
             switch (button)
             {
                 case Avalonia.Remote.Protocol.Input.MouseButton.Left:
-                    return pressed ? RawMouseEventType.LeftButtonDown : RawMouseEventType.LeftButtonUp;
+                    return pressed ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp;
 
                 case Avalonia.Remote.Protocol.Input.MouseButton.Middle:
-                    return pressed ? RawMouseEventType.MiddleButtonDown : RawMouseEventType.MiddleButtonUp;
+                    return pressed ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp;
 
                 case Avalonia.Remote.Protocol.Input.MouseButton.Right:
-                    return pressed ? RawMouseEventType.RightButtonDown : RawMouseEventType.RightButtonUp;
+                    return pressed ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp;
 
                 default:
-                    return RawMouseEventType.Move;
+                    return RawPointerEventType.Move;
             }
         }
 
@@ -166,11 +166,11 @@ namespace Avalonia.Controls.Remote.Server
                 {
                     Dispatcher.UIThread.Post(() =>
                     {
-                        Input?.Invoke(new RawMouseEventArgs(
+                        Input?.Invoke(new RawPointerEventArgs(
                             MouseDevice, 
                             0, 
                             InputRoot, 
-                            RawMouseEventType.Move, 
+                            RawPointerEventType.Move, 
                             new Point(pointer.X, pointer.Y), 
                             GetAvaloniaInputModifiers(pointer.Modifiers)));
                     }, DispatcherPriority.Input);
@@ -179,7 +179,7 @@ namespace Avalonia.Controls.Remote.Server
                 {
                     Dispatcher.UIThread.Post(() =>
                     {
-                        Input?.Invoke(new RawMouseEventArgs(
+                        Input?.Invoke(new RawPointerEventArgs(
                             MouseDevice,
                             0,
                             InputRoot,
@@ -192,7 +192,7 @@ namespace Avalonia.Controls.Remote.Server
                 {
                     Dispatcher.UIThread.Post(() =>
                     {
-                        Input?.Invoke(new RawMouseEventArgs(
+                        Input?.Invoke(new RawPointerEventArgs(
                             MouseDevice,
                             0,
                             InputRoot,

+ 5 - 0
src/Avalonia.Input/IMouseDevice.cs

@@ -1,6 +1,8 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using System;
+
 namespace Avalonia.Input
 {
     /// <summary>
@@ -11,6 +13,9 @@ namespace Avalonia.Input
         /// <summary>
         /// Gets the mouse position, in screen coordinates.
         /// </summary>
+        [Obsolete("Use PointerEventArgs.GetPosition")]
         PixelPoint Position { get; }
+
+        void SceneInvalidated(IInputRoot root, Rect rect);
     }
 }

+ 18 - 0
src/Avalonia.Input/IPointer.cs

@@ -0,0 +1,18 @@
+namespace Avalonia.Input
+{
+    public interface IPointer
+    {
+        int Id { get; }
+        void Capture(IInputElement control);
+        IInputElement Captured { get; }
+        PointerType Type { get; }
+        bool IsPrimary { get; }
+        
+    }
+
+    public enum PointerType
+    {
+        Mouse,
+        Touch
+    }
+}

+ 5 - 3
src/Avalonia.Input/IPointerDevice.cs

@@ -1,18 +1,20 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using System;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Input
 {
     public interface IPointerDevice : IInputDevice
     {
+        [Obsolete("Use IPointer")]
         IInputElement Captured { get; }
-
+        
+        [Obsolete("Use IPointer")]
         void Capture(IInputElement control);
 
+        [Obsolete("Use PointerEventArgs.GetPosition")]
         Point GetPosition(IVisual relativeTo);
-
-        void SceneInvalidated(IInputRoot root, Rect rect);
     }
 }

+ 86 - 71
src/Avalonia.Input/MouseDevice.cs

@@ -14,14 +14,18 @@ namespace Avalonia.Input
     /// <summary>
     /// Represents a mouse device.
     /// </summary>
-    public class MouseDevice : IMouseDevice
+    public class MouseDevice : IMouseDevice, IPointer
     {
         private int _clickCount;
         private Rect _lastClickRect;
         private ulong _lastClickTime;
         private IInputElement _captured;
         private IDisposable _capturedSubscription;
-       
+
+        PointerType IPointer.Type => PointerType.Mouse;
+        bool IPointer.IsPrimary => true;
+        int IPointer.Id { get; } = Pointer.GetNextFreeId();
+        
         /// <summary>
         /// Gets the control that is currently capturing by the mouse, if any.
         /// </summary>
@@ -96,7 +100,7 @@ namespace Avalonia.Input
 
         public void ProcessRawEvent(RawInputEventArgs e)
         {
-            if (!e.Handled && e is RawMouseEventArgs margs)
+            if (!e.Handled && e is RawPointerEventArgs margs)
                 ProcessRawEvent(margs);
         }
 
@@ -117,42 +121,53 @@ namespace Avalonia.Input
             }
         }
 
-        private void ProcessRawEvent(RawMouseEventArgs e)
+        int ButtonCount(PointerPointProperties props)
+        {
+            var rv = 0;
+            if (props.IsLeftButtonPressed)
+                rv++;
+            if (props.IsMiddleButtonPressed)
+                rv++;
+            if (props.IsRightButtonPressed)
+                rv++;
+            return rv;
+        }
+        
+        private void ProcessRawEvent(RawPointerEventArgs e)
         {
             Contract.Requires<ArgumentNullException>(e != null);
 
             var mouse = (IMouseDevice)e.Device;
 
             Position = e.Root.PointToScreen(e.Position);
-
+            var props = CreateProperties(e);
             switch (e.Type)
             {
-                case RawMouseEventType.LeaveWindow:
+                case RawPointerEventType.LeaveWindow:
                     LeaveWindow(mouse, e.Root, e.InputModifiers);
                     break;
-                case RawMouseEventType.LeftButtonDown:
-                case RawMouseEventType.RightButtonDown:
-                case RawMouseEventType.MiddleButtonDown:
-                    e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position,
-                         e.Type == RawMouseEventType.LeftButtonDown
-                            ? MouseButton.Left
-                            : e.Type == RawMouseEventType.RightButtonDown ? MouseButton.Right : MouseButton.Middle,
-                        e.InputModifiers);
+                case RawPointerEventType.LeftButtonDown:
+                case RawPointerEventType.RightButtonDown:
+                case RawPointerEventType.MiddleButtonDown:
+                    if (ButtonCount(props) > 1)
+                        e.Handled = MouseMove(mouse, e.Root, e.Position, props, e.InputModifiers);
+                    else
+                        e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position,
+                            props, e.InputModifiers);
                     break;
-                case RawMouseEventType.LeftButtonUp:
-                case RawMouseEventType.RightButtonUp:
-                case RawMouseEventType.MiddleButtonUp:
-                    e.Handled = MouseUp(mouse, e.Root, e.Position,
-                        e.Type == RawMouseEventType.LeftButtonUp
-                            ? MouseButton.Left
-                            : e.Type == RawMouseEventType.RightButtonUp ? MouseButton.Right : MouseButton.Middle,
-                        e.InputModifiers);
+                case RawPointerEventType.LeftButtonUp:
+                case RawPointerEventType.RightButtonUp:
+                case RawPointerEventType.MiddleButtonUp:
+                    if (ButtonCount(props) != 0)
+                        e.Handled = MouseMove(mouse, e.Root, e.Position, props, e.InputModifiers);
+                    else
+                        e.Handled = MouseUp(mouse, e.Root, e.Position, props, e.InputModifiers);
                     break;
-                case RawMouseEventType.Move:
-                    e.Handled = MouseMove(mouse, e.Root, e.Position, e.InputModifiers);
+                case RawPointerEventType.Move:
+                    e.Handled = MouseMove(mouse, e.Root, e.Position, props, e.InputModifiers);
                     break;
-                case RawMouseEventType.Wheel:
-                    e.Handled = MouseWheel(mouse, e.Root, e.Position, ((RawMouseWheelEventArgs)e).Delta, e.InputModifiers);
+                case RawPointerEventType.Wheel:
+                    e.Handled = MouseWheel(mouse, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, e.InputModifiers);
                     break;
             }
         }
@@ -165,7 +180,30 @@ namespace Avalonia.Input
             ClearPointerOver(this, root, inputModifiers);
         }
 
-        private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p, MouseButton button, InputModifiers inputModifiers)
+
+        PointerPointProperties CreateProperties(RawPointerEventArgs args)
+        {
+            var rv = new PointerPointProperties(args.InputModifiers);
+
+            if (args.Type == RawPointerEventType.LeftButtonDown)
+                rv.IsLeftButtonPressed = true;
+            if (args.Type == RawPointerEventType.MiddleButtonDown)
+                rv.IsMiddleButtonPressed = true;
+            if (args.Type == RawPointerEventType.RightButtonDown)
+                rv.IsRightButtonPressed = true;
+            if (args.Type == RawPointerEventType.LeftButtonUp)
+                rv.IsLeftButtonPressed = false;
+            if (args.Type == RawPointerEventType.MiddleButtonUp)
+                rv.IsMiddleButtonPressed = false;
+            if (args.Type == RawPointerEventType.RightButtonDown)
+                rv.IsRightButtonPressed = false;
+            return rv;
+        }
+
+        private MouseButton _lastMouseDownButton;
+        private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p,
+            PointerPointProperties properties,
+            InputModifiers inputModifiers)
         {
             Contract.Requires<ArgumentNullException>(device != null);
             Contract.Requires<ArgumentNullException>(root != null);
@@ -190,16 +228,8 @@ namespace Avalonia.Input
                     _lastClickTime = timestamp;
                     _lastClickRect = new Rect(p, new Size())
                         .Inflate(new Thickness(settings.DoubleClickSize.Width / 2, settings.DoubleClickSize.Height / 2));
-
-                    var e = new PointerPressedEventArgs
-                    {
-                        Device = this,
-                        RoutedEvent = InputElement.PointerPressedEvent,
-                        Source = source,
-                        ClickCount = _clickCount,
-                        MouseButton = button,
-                        InputModifiers = inputModifiers
-                    };
+                    _lastMouseDownButton = properties.GetObsoleteMouseButton();
+                    var e = new PointerPressedEventArgs(source, this, root, p, properties, inputModifiers, _clickCount);
 
                     source.RaiseEvent(e);
                     return e.Handled;
@@ -209,7 +239,8 @@ namespace Avalonia.Input
             return false;
         }
 
-        private bool MouseMove(IMouseDevice device, IInputRoot root, Point p, InputModifiers inputModifiers)
+        private bool MouseMove(IMouseDevice device, IInputRoot root, Point p, PointerPointProperties properties,
+            InputModifiers inputModifiers)
         {
             Contract.Requires<ArgumentNullException>(device != null);
             Contract.Requires<ArgumentNullException>(root != null);
@@ -226,19 +257,15 @@ namespace Avalonia.Input
                 source = Captured;
             }
 
-            var e = new PointerEventArgs
-            {
-                Device = this,
-                RoutedEvent = InputElement.PointerMovedEvent,
-                Source = source,
-                InputModifiers = inputModifiers
-            };
+            var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, this, root,
+                p, properties, inputModifiers);
 
             source?.RaiseEvent(e);
             return e.Handled;
         }
 
-        private bool MouseUp(IMouseDevice device, IInputRoot root, Point p, MouseButton button, InputModifiers inputModifiers)
+        private bool MouseUp(IMouseDevice device, IInputRoot root, Point p, PointerPointProperties props,
+            InputModifiers inputModifiers)
         {
             Contract.Requires<ArgumentNullException>(device != null);
             Contract.Requires<ArgumentNullException>(root != null);
@@ -248,14 +275,7 @@ namespace Avalonia.Input
             if (hit != null)
             {
                 var source = GetSource(hit);
-                var e = new PointerReleasedEventArgs
-                {
-                    Device = this,
-                    RoutedEvent = InputElement.PointerReleasedEvent,
-                    Source = source,
-                    MouseButton = button,
-                    InputModifiers = inputModifiers
-                };
+                var e = new PointerReleasedEventArgs(source, this, root, p, props, inputModifiers, _lastMouseDownButton);
 
                 source?.RaiseEvent(e);
                 return e.Handled;
@@ -264,7 +284,9 @@ namespace Avalonia.Input
             return false;
         }
 
-        private bool MouseWheel(IMouseDevice device, IInputRoot root, Point p, Vector delta, InputModifiers inputModifiers)
+        private bool MouseWheel(IMouseDevice device, IInputRoot root, Point p,
+            PointerPointProperties props,
+            Vector delta, InputModifiers inputModifiers)
         {
             Contract.Requires<ArgumentNullException>(device != null);
             Contract.Requires<ArgumentNullException>(root != null);
@@ -274,14 +296,7 @@ namespace Avalonia.Input
             if (hit != null)
             {
                 var source = GetSource(hit);
-                var e = new PointerWheelEventArgs
-                {
-                    Device = this,
-                    RoutedEvent = InputElement.PointerWheelChangedEvent,
-                    Source = source,
-                    Delta = delta,
-                    InputModifiers = inputModifiers
-                };
+                var e = new PointerWheelEventArgs(source, this, root, p, props, inputModifiers, delta);
 
                 source?.RaiseEvent(e);
                 return e.Handled;
@@ -306,18 +321,19 @@ namespace Avalonia.Input
             return Captured ?? root.InputHitTest(p);
         }
 
+        PointerEventArgs CreateSimpleEvent(RoutedEvent ev, IInteractive source, InputModifiers inputModifiers)
+        {
+            return new PointerEventArgs(ev, source, this, null, default,
+                new PointerPointProperties(inputModifiers), inputModifiers);
+        }
+
         private void ClearPointerOver(IPointerDevice device, IInputRoot root, InputModifiers inputModifiers)
         {
             Contract.Requires<ArgumentNullException>(device != null);
             Contract.Requires<ArgumentNullException>(root != null);
 
             var element = root.PointerOverElement;
-            var e = new PointerEventArgs
-            {
-                RoutedEvent = InputElement.PointerLeaveEvent,
-                Device = device,
-                InputModifiers = inputModifiers
-            };
+            var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, element, inputModifiers);
 
             if (element!=null && !element.IsAttachedToVisualTree)
             {
@@ -384,7 +400,6 @@ namespace Avalonia.Input
 
             IInputElement branch = null;
 
-            var e = new PointerEventArgs { Device = device, InputModifiers = inputModifiers };
             var el = element;
 
             while (el != null)
@@ -398,8 +413,8 @@ namespace Avalonia.Input
             }
 
             el = root.PointerOverElement;
-            e.RoutedEvent = InputElement.PointerLeaveEvent;
 
+            var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, el, inputModifiers);
             if (el!=null && branch!=null && !el.IsAttachedToVisualTree)
             {
                 ClearChildrenPointerOver(e,branch,false);

+ 63 - 0
src/Avalonia.Input/Pointer.cs

@@ -0,0 +1,63 @@
+using System;
+using System.Linq;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Input
+{
+    public class Pointer : IPointer, IDisposable
+    {
+        private static int s_NextFreePointerId = 1000;
+        public static int GetNextFreeId() => s_NextFreePointerId++;
+        
+        public Pointer(int id, PointerType type, bool isPrimary, IInputElement implicitlyCaptured)
+        {
+            Id = id;
+            Type = type;
+            IsPrimary = isPrimary;
+            ImplicitlyCaptured = implicitlyCaptured;
+            if (ImplicitlyCaptured != null)
+                ImplicitlyCaptured.DetachedFromVisualTree += OnImplicitCaptureDetached;
+        }
+
+        public int Id { get; }
+
+        public void Capture(IInputElement control)
+        {
+            if (Captured != null)
+                Captured.DetachedFromVisualTree -= OnCaptureDetached;
+            Captured = control;
+            if (Captured != null)
+                Captured.DetachedFromVisualTree += OnCaptureDetached;
+        }
+
+        IInputElement GetNextCapture(IVisual parent) =>
+            parent as IInputElement ?? parent.GetVisualAncestors().OfType<IInputElement>().FirstOrDefault();
+        
+        private void OnCaptureDetached(object sender, VisualTreeAttachmentEventArgs e)
+        {
+            Capture(GetNextCapture(e.Parent));
+        }
+
+        private void OnImplicitCaptureDetached(object sender, VisualTreeAttachmentEventArgs e)
+        {
+            ImplicitlyCaptured.DetachedFromVisualTree -= OnImplicitCaptureDetached;
+            ImplicitlyCaptured = GetNextCapture(e.Parent);
+            if (ImplicitlyCaptured != null)
+                ImplicitlyCaptured.DetachedFromVisualTree += OnImplicitCaptureDetached;
+        }
+
+        public IInputElement Captured { get; private set; }
+        public IInputElement ImplicitlyCaptured { get; private set; }
+        public IInputElement GetEffectiveCapture() => Captured ?? ImplicitlyCaptured;
+            
+        public PointerType Type { get; }
+        public bool IsPrimary { get; }
+        public void Dispose()
+        {
+            if (ImplicitlyCaptured != null)
+                ImplicitlyCaptured.DetachedFromVisualTree -= OnImplicitCaptureDetached;
+            if (Captured != null)
+                Captured.DetachedFromVisualTree -= OnCaptureDetached;
+        }
+    }
+}

+ 72 - 23
src/Avalonia.Input/PointerEventArgs.cs

@@ -1,6 +1,8 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using System;
+using Avalonia.Input.Raw;
 using Avalonia.Interactivity;
 using Avalonia.VisualTree;
 
@@ -8,25 +10,65 @@ namespace Avalonia.Input
 {
     public class PointerEventArgs : RoutedEventArgs
     {
-        public PointerEventArgs()
-        {
+        private readonly IVisual _rootVisual;
+        private readonly Point _rootVisualPosition;
+        private readonly PointerPointProperties _properties;
 
+        public PointerEventArgs(RoutedEvent routedEvent,
+            IInteractive source,
+            IPointer pointer,
+            IVisual rootVisual, Point rootVisualPosition, PointerPointProperties properties,
+            InputModifiers modifiers)
+           : base(routedEvent)
+        {
+            Source = source;
+            _rootVisual = rootVisual;
+            _rootVisualPosition = rootVisualPosition;
+            _properties = properties;
+            Pointer = pointer;
+            InputModifiers = modifiers;
         }
 
-        public PointerEventArgs(RoutedEvent routedEvent)
-           : base(routedEvent)
+        class EmulatedDevice : IPointerDevice
         {
+            private readonly PointerEventArgs _ev;
 
+            public EmulatedDevice(PointerEventArgs ev)
+            {
+                _ev = ev;
+            }
+            
+            public void ProcessRawEvent(RawInputEventArgs ev) => throw new NotSupportedException();
+
+            public IInputElement Captured => _ev.Pointer.Captured;
+            public void Capture(IInputElement control)
+            {
+                _ev.Pointer.Capture(control);
+            }
+
+            public Point GetPosition(IVisual relativeTo) => _ev.GetPosition(relativeTo);
         }
 
-        public IPointerDevice Device { get; set; }
+        public IPointer Pointer { get; }
+
+        private IPointerDevice _device;
+
+        [Obsolete("Use Pointer to get pointer-specific information")]
+        public IPointerDevice Device => _device ?? (_device = new EmulatedDevice(this));
 
-        public InputModifiers InputModifiers { get; set; }
+        public InputModifiers InputModifiers { get; }
 
         public Point GetPosition(IVisual relativeTo)
         {
-            return Device.GetPosition(relativeTo);
+            if (_rootVisual == null)
+                return default;
+            if (relativeTo == null)
+                return _rootVisualPosition;
+            return _rootVisualPosition * _rootVisual.TransformToVisual(relativeTo) ?? default;
         }
+
+        public PointerPoint GetPointerPoint(IVisual relativeTo)
+            => new PointerPoint(Pointer, GetPosition(relativeTo), _properties);
     }
     
     public enum MouseButton
@@ -39,32 +81,39 @@ namespace Avalonia.Input
 
     public class PointerPressedEventArgs : PointerEventArgs
     {
-        public PointerPressedEventArgs()
-            : base(InputElement.PointerPressedEvent)
-        {
-        }
+        private readonly int _obsoleteClickCount;
 
-        public PointerPressedEventArgs(RoutedEvent routedEvent)
-            : base(routedEvent)
+        public PointerPressedEventArgs(
+            IInteractive source,
+            IPointer pointer,
+            IVisual rootVisual, Point rootVisualPosition, PointerPointProperties properties,
+            InputModifiers modifiers,
+            int obsoleteClickCount = 1)
+            : base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition, properties,
+                modifiers)
         {
+            _obsoleteClickCount = obsoleteClickCount;
         }
 
-        public int ClickCount { get; set; }
-        public MouseButton MouseButton { get; set; }
+        [Obsolete("Use DoubleTapped or DoubleRightTapped event instead")]
+        public int ClickCount => _obsoleteClickCount;
+
+        [Obsolete] public MouseButton MouseButton => GetPointerPoint(null).Properties.GetObsoleteMouseButton();
     }
 
     public class PointerReleasedEventArgs : PointerEventArgs
     {
-        public PointerReleasedEventArgs()
-            : base(InputElement.PointerReleasedEvent)
-        {
-        }
-
-        public PointerReleasedEventArgs(RoutedEvent routedEvent)
-            : base(routedEvent)
+        public PointerReleasedEventArgs(
+            IInteractive source, IPointer pointer,
+            IVisual rootVisual, Point rootVisualPosition, PointerPointProperties properties, InputModifiers modifiers,
+            MouseButton obsoleteMouseButton)
+            : base(InputElement.PointerReleasedEvent, source, pointer, rootVisual, rootVisualPosition,
+                properties, modifiers)
         {
+            MouseButton = obsoleteMouseButton;
         }
 
-        public MouseButton MouseButton { get; set; }
+        [Obsolete()]
+        public MouseButton MouseButton { get; private set; }
     }
 }

+ 45 - 0
src/Avalonia.Input/PointerPoint.cs

@@ -0,0 +1,45 @@
+namespace Avalonia.Input
+{
+    public sealed class PointerPoint
+    {
+        public PointerPoint(IPointer pointer, Point position, PointerPointProperties properties)
+        {
+            Pointer = pointer;
+            Position = position;
+            Properties = properties;
+        }
+        public IPointer Pointer { get; }
+        public PointerPointProperties Properties { get; }
+        public Point Position { get; }
+    }
+
+    public sealed class PointerPointProperties
+    {
+        public bool IsLeftButtonPressed { get; set; }
+        public bool IsMiddleButtonPressed { get; set; }
+        public bool IsRightButtonPressed { get; set; }
+
+        public PointerPointProperties()
+        {
+            
+        }
+        
+        public PointerPointProperties(InputModifiers modifiers)
+        {
+            IsLeftButtonPressed = modifiers.HasFlag(InputModifiers.LeftMouseButton);
+            IsMiddleButtonPressed = modifiers.HasFlag(InputModifiers.MiddleMouseButton);
+            IsRightButtonPressed = modifiers.HasFlag(InputModifiers.RightMouseButton);
+        }
+        
+        public MouseButton GetObsoleteMouseButton()
+        {
+            if (IsLeftButtonPressed)
+                return MouseButton.Left;
+            if (IsMiddleButtonPressed)
+                return MouseButton.Middle;
+            if (IsRightButtonPressed)
+                return MouseButton.Right;
+            return MouseButton.None;
+        }
+    }
+}

+ 11 - 0
src/Avalonia.Input/PointerWheelEventArgs.cs

@@ -1,10 +1,21 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using Avalonia.Interactivity;
+using Avalonia.VisualTree;
+
 namespace Avalonia.Input
 {
     public class PointerWheelEventArgs : PointerEventArgs
     {
         public Vector Delta { get; set; }
+
+        public PointerWheelEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual,
+            Point rootVisualPosition,
+            PointerPointProperties properties, InputModifiers modifiers, Vector delta) 
+            : base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition, properties, modifiers)
+        {
+            Delta = delta;
+        }
     }
 }

+ 2 - 2
src/Avalonia.Input/Raw/RawMouseWheelEventArgs.cs

@@ -4,7 +4,7 @@
 
 namespace Avalonia.Input.Raw
 {
-    public class RawMouseWheelEventArgs : RawMouseEventArgs
+    public class RawMouseWheelEventArgs : RawPointerEventArgs
     {
         public RawMouseWheelEventArgs(
             IInputDevice device,
@@ -12,7 +12,7 @@ namespace Avalonia.Input.Raw
             IInputRoot root,
             Point position,
             Vector delta, InputModifiers inputModifiers)
-            : base(device, timestamp, root, RawMouseEventType.Wheel, position, inputModifiers)
+            : base(device, timestamp, root, RawPointerEventType.Wheel, position, inputModifiers)
         {
             Delta = delta;
         }

+ 9 - 6
src/Avalonia.Input/Raw/RawMouseEventArgs.cs → src/Avalonia.Input/Raw/RawPointerEventArgs.cs

@@ -5,7 +5,7 @@ using System;
 
 namespace Avalonia.Input.Raw
 {
-    public enum RawMouseEventType
+    public enum RawPointerEventType
     {
         LeaveWindow,
         LeftButtonDown,
@@ -17,15 +17,18 @@ namespace Avalonia.Input.Raw
         Move,
         Wheel,
         NonClientLeftButtonDown,
+        TouchBegin,
+        TouchUpdate,
+        TouchEnd
     }
 
     /// <summary>
     /// A raw mouse event.
     /// </summary>
-    public class RawMouseEventArgs : RawInputEventArgs
+    public class RawPointerEventArgs : RawInputEventArgs
     {
         /// <summary>
-        /// Initializes a new instance of the <see cref="RawMouseEventArgs"/> class.
+        /// Initializes a new instance of the <see cref="RawPointerEventArgs"/> class.
         /// </summary>
         /// <param name="device">The associated device.</param>
         /// <param name="timestamp">The event timestamp.</param>
@@ -33,11 +36,11 @@ namespace Avalonia.Input.Raw
         /// <param name="type">The type of the event.</param>
         /// <param name="position">The mouse position, in client DIPs.</param>
         /// <param name="inputModifiers">The input modifiers.</param>
-        public RawMouseEventArgs(
+        public RawPointerEventArgs(
             IInputDevice device,
             ulong timestamp,
             IInputRoot root,
-            RawMouseEventType type,
+            RawPointerEventType type,
             Point position, 
             InputModifiers inputModifiers)
             : base(device, timestamp)
@@ -64,7 +67,7 @@ namespace Avalonia.Input.Raw
         /// <summary>
         /// Gets the type of the event.
         /// </summary>
-        public RawMouseEventType Type { get; private set; }
+        public RawPointerEventType Type { get; private set; }
 
         /// <summary>
         /// Gets the input modifiers.

+ 15 - 0
src/Avalonia.Input/Raw/RawTouchEventArgs.cs

@@ -0,0 +1,15 @@
+namespace Avalonia.Input.Raw
+{
+    public class RawTouchEventArgs : RawPointerEventArgs
+    {
+        public RawTouchEventArgs(IInputDevice device, ulong timestamp, IInputRoot root,
+            RawPointerEventType type, Point position, InputModifiers inputModifiers,
+            long touchPointId) 
+            : base(device, timestamp, root, type, position, inputModifiers)
+        {
+            TouchPointId = touchPointId;
+        }
+
+        public long TouchPointId { get; set; }
+    }
+}

+ 72 - 0
src/Avalonia.Input/TouchDevice.cs

@@ -0,0 +1,72 @@
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Input.Raw;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Input
+{
+    /// <summary>
+    /// Handles raw touch events
+    /// <remarks>
+    /// This class is supposed to be used on per-toplevel basis, don't use a shared one
+    /// </remarks>
+    /// </summary>
+    public class TouchDevice : IInputDevice
+    {
+        Dictionary<long, Pointer> _pointers = new Dictionary<long, Pointer>();
+
+        static InputModifiers GetModifiers(InputModifiers modifiers, bool left)
+        {
+            var mask = (InputModifiers)0x7fffffff ^ InputModifiers.LeftMouseButton ^ InputModifiers.MiddleMouseButton ^
+                       InputModifiers.RightMouseButton;
+            modifiers &= mask;
+            if (left)
+                modifiers |= InputModifiers.LeftMouseButton;
+            return modifiers;
+        }
+        
+        public void ProcessRawEvent(RawInputEventArgs ev)
+        {
+            var args = (RawTouchEventArgs)ev;
+            if (!_pointers.TryGetValue(args.TouchPointId, out var pointer))
+            {
+                if (args.Type == RawPointerEventType.TouchEnd)
+                    return;
+                var hit = args.Root.InputHitTest(args.Position);
+
+                _pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(),
+                    PointerType.Touch, _pointers.Count == 0, hit);
+            }
+            
+
+            var target = pointer.GetEffectiveCapture() ?? args.Root;
+            if (args.Type == RawPointerEventType.TouchBegin)
+            {
+                var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary);
+                target.RaiseEvent(new PointerPressedEventArgs(target, pointer,
+                    args.Root, args.Position, new PointerPointProperties(modifiers),
+                    modifiers));
+            }
+
+            if (args.Type == RawPointerEventType.TouchEnd)
+            {
+                _pointers.Remove(args.TouchPointId);
+                var modifiers = GetModifiers(args.InputModifiers, false);
+                using (pointer)
+                {
+                    target.RaiseEvent(new PointerReleasedEventArgs(target, pointer,
+                        args.Root, args.Position, new PointerPointProperties(modifiers),
+                        modifiers, pointer.IsPrimary ? MouseButton.Left : MouseButton.None));
+                }
+            }
+
+            if (args.Type == RawPointerEventType.TouchUpdate)
+            {
+                var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary);
+                target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root,
+                    args.Position, new PointerPointProperties(modifiers), modifiers));
+            }
+        }
+        
+    }
+}

+ 1 - 1
src/Avalonia.Native/WindowImplBase.cs

@@ -226,7 +226,7 @@ namespace Avalonia.Native
                     break;
 
                 default:
-                    Input?.Invoke(new RawMouseEventArgs(_mouse, timeStamp, _inputRoot, (RawMouseEventType)type, point.ToAvaloniaPoint(), (InputModifiers)modifiers));
+                    Input?.Invoke(new RawPointerEventArgs(_mouse, timeStamp, _inputRoot, (RawPointerEventType)type, point.ToAvaloniaPoint(), (InputModifiers)modifiers));
                     break;
             }
         }

+ 3 - 1
src/Avalonia.X11/X11Platform.cs

@@ -28,6 +28,7 @@ namespace Avalonia.X11
         public X11PlatformOptions Options { get; private set; }
         public void Initialize(X11PlatformOptions options)
         {
+            Options = options;
             XInitThreads();
             Display = XOpenDisplay(IntPtr.Zero);
             DeferredDisplay = XOpenDisplay(IntPtr.Zero);
@@ -66,7 +67,7 @@ namespace Avalonia.X11
                     GlxGlPlatformFeature.TryInitialize(Info);
             }
 
-            Options = options;
+            
         }
 
         public IntPtr DeferredDisplay { get; set; }
@@ -96,6 +97,7 @@ namespace Avalonia
         public bool UseEGL { get; set; }
         public bool UseGpu { get; set; } = true;
         public string WmClass { get; set; } = Assembly.GetEntryAssembly()?.GetName()?.Name ?? "AvaloniaApplication";
+        public bool? EnableMultiTouch { get; set; }
     }
     public static class AvaloniaX11PlatformExtensions
     {

+ 13 - 13
src/Avalonia.X11/X11Window.cs

@@ -314,9 +314,9 @@ namespace Avalonia.X11
             else if (ev.type == XEventName.FocusOut)
                 Deactivated?.Invoke();
             else if (ev.type == XEventName.MotionNotify)
-                MouseEvent(RawMouseEventType.Move, ref ev, ev.MotionEvent.state);
+                MouseEvent(RawPointerEventType.Move, ref ev, ev.MotionEvent.state);
             else if (ev.type == XEventName.LeaveNotify)
-                MouseEvent(RawMouseEventType.LeaveWindow, ref ev, ev.CrossingEvent.state);
+                MouseEvent(RawPointerEventType.LeaveWindow, ref ev, ev.CrossingEvent.state);
             else if (ev.type == XEventName.PropertyNotify)
             {
                 OnPropertyChange(ev.PropertyEvent.atom, ev.PropertyEvent.state == 0);
@@ -326,9 +326,9 @@ namespace Avalonia.X11
                 if (ActivateTransientChildIfNeeded())
                     return;
                 if (ev.ButtonEvent.button < 4)
-                    MouseEvent(ev.ButtonEvent.button == 1 ? RawMouseEventType.LeftButtonDown
-                        : ev.ButtonEvent.button == 2 ? RawMouseEventType.MiddleButtonDown
-                        : RawMouseEventType.RightButtonDown, ref ev, ev.ButtonEvent.state);
+                    MouseEvent(ev.ButtonEvent.button == 1 ? RawPointerEventType.LeftButtonDown
+                        : ev.ButtonEvent.button == 2 ? RawPointerEventType.MiddleButtonDown
+                        : RawPointerEventType.RightButtonDown, ref ev, ev.ButtonEvent.state);
                 else
                 {
                     var delta = ev.ButtonEvent.button == 4
@@ -347,9 +347,9 @@ namespace Avalonia.X11
             else if (ev.type == XEventName.ButtonRelease)
             {
                 if (ev.ButtonEvent.button < 4)
-                    MouseEvent(ev.ButtonEvent.button == 1 ? RawMouseEventType.LeftButtonUp
-                        : ev.ButtonEvent.button == 2 ? RawMouseEventType.MiddleButtonUp
-                        : RawMouseEventType.RightButtonUp, ref ev, ev.ButtonEvent.state);
+                    MouseEvent(ev.ButtonEvent.button == 1 ? RawPointerEventType.LeftButtonUp
+                        : ev.ButtonEvent.button == 2 ? RawPointerEventType.MiddleButtonUp
+                        : RawPointerEventType.RightButtonUp, ref ev, ev.ButtonEvent.state);
             }
             else if (ev.type == XEventName.ConfigureNotify)
             {
@@ -577,7 +577,7 @@ namespace Avalonia.X11
 
         public void ScheduleInput(RawInputEventArgs args)
         {
-            if (args is RawMouseEventArgs mouse)
+            if (args is RawPointerEventArgs mouse)
                 mouse.Position = mouse.Position / Scaling;
             if (args is RawDragEvent drag)
                 drag.Location = drag.Location / Scaling;
@@ -598,13 +598,13 @@ namespace Avalonia.X11
             }
         }
         
-        void MouseEvent(RawMouseEventType type, ref XEvent ev, XModifierMask mods)
+        void MouseEvent(RawPointerEventType type, ref XEvent ev, XModifierMask mods)
         {
-            var mev = new RawMouseEventArgs(
+            var mev = new RawPointerEventArgs(
                 _mouse, (ulong)ev.ButtonEvent.time.ToInt64(), _inputRoot,
                 type, new Point(ev.ButtonEvent.x, ev.ButtonEvent.y), TranslateModifiers(mods)); 
-            if(type == RawMouseEventType.Move && _inputQueue.Count>0 && _lastEvent.Event is RawMouseEventArgs ma)
-                if (ma.Type == RawMouseEventType.Move)
+            if(type == RawPointerEventType.Move && _inputQueue.Count>0 && _lastEvent.Event is RawPointerEventArgs ma)
+                if (ma.Type == RawPointerEventType.Move)
                 {
                     _lastEvent.Event = mev;
                     return;

+ 51 - 18
src/Avalonia.X11/XI2Manager.cs

@@ -11,6 +11,7 @@ namespace Avalonia.X11
     unsafe class XI2Manager
     {
         private X11Info _x11;
+        private bool _multitouch;
         private Dictionary<IntPtr, IXI2Client> _clients = new Dictionary<IntPtr, IXI2Client>();
         class DeviceInfo
         {
@@ -77,11 +78,14 @@ namespace Avalonia.X11
         
         private PointerDeviceInfo _pointerDevice;
         private AvaloniaX11Platform _platform;
+        private readonly TouchDevice _touchDevice = new TouchDevice();
+
 
         public bool Init(AvaloniaX11Platform platform)
         {
             _platform = platform;
             _x11 = platform.Info;
+            _multitouch = platform.Options?.EnableMultiTouch ?? false;
             var devices =(XIDeviceInfo*) XIQueryDevice(_x11.Display,
                 (int)XiPredefinedDeviceId.XIAllMasterDevices, out int num);
             for (var c = 0; c < num; c++)
@@ -121,16 +125,23 @@ namespace Avalonia.X11
         public XEventMask AddWindow(IntPtr xid, IXI2Client window)
         {
             _clients[xid] = window;
-
-            XiSelectEvents(_x11.Display, xid, new Dictionary<int, List<XiEventType>>
+            var events = new List<XiEventType>
             {
-                [_pointerDevice.Id] = new List<XiEventType>()
+                XiEventType.XI_Motion,
+                XiEventType.XI_ButtonPress,
+                XiEventType.XI_ButtonRelease
+            };
+
+            if (_multitouch)
+                events.AddRange(new[]
                 {
-                    XiEventType.XI_Motion,
-                    XiEventType.XI_ButtonPress,
-                    XiEventType.XI_ButtonRelease,
-                }
-            });
+                    XiEventType.XI_TouchBegin,
+                    XiEventType.XI_TouchUpdate,
+                    XiEventType.XI_TouchEnd
+                });
+
+            XiSelectEvents(_x11.Display, xid,
+                new Dictionary<int, List<XiEventType>> {[_pointerDevice.Id] = events});
                 
             // We are taking over mouse input handling from here
             return XEventMask.PointerMotionMask
@@ -154,8 +165,9 @@ namespace Avalonia.X11
                 _pointerDevice.Update(changed->Classes, changed->NumClasses);
             }
 
-            //TODO: this should only be used for non-touch devices
-            if (xev->evtype >= XiEventType.XI_ButtonPress && xev->evtype <= XiEventType.XI_Motion)
+            
+            if ((xev->evtype >= XiEventType.XI_ButtonPress && xev->evtype <= XiEventType.XI_Motion)
+                || (xev->evtype>=XiEventType.XI_TouchBegin&&xev->evtype<=XiEventType.XI_TouchEnd))
             {
                 var dev = (XIDeviceEvent*)xev;
                 if (_clients.TryGetValue(dev->EventWindow, out var client))
@@ -165,6 +177,23 @@ namespace Avalonia.X11
 
         void OnDeviceEvent(IXI2Client client, ParsedDeviceEvent ev)
         {
+            if (ev.Type == XiEventType.XI_TouchBegin 
+                || ev.Type == XiEventType.XI_TouchUpdate 
+                || ev.Type == XiEventType.XI_TouchEnd)
+            {
+                var type = ev.Type == XiEventType.XI_TouchBegin ?
+                    RawPointerEventType.TouchBegin :
+                    (ev.Type == XiEventType.XI_TouchUpdate ?
+                        RawPointerEventType.TouchUpdate :
+                        RawPointerEventType.TouchEnd);
+                client.ScheduleInput(new RawTouchEventArgs(_touchDevice,
+                    ev.Timestamp, client.InputRoot, type, ev.Position, ev.Modifiers, ev.Detail));
+                return;
+            }
+
+            if (_multitouch && ev.Emulated)
+                return;
+            
             if (ev.Type == XiEventType.XI_Motion)
             {
                 Vector scrollDelta = default;
@@ -194,23 +223,23 @@ namespace Avalonia.X11
                     client.ScheduleInput(new RawMouseWheelEventArgs(_platform.MouseDevice, ev.Timestamp,
                         client.InputRoot, ev.Position, scrollDelta, ev.Modifiers));
                 if (_pointerDevice.HasMotion(ev))
-                    client.ScheduleInput(new RawMouseEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot,
-                        RawMouseEventType.Move, ev.Position, ev.Modifiers));
+                    client.ScheduleInput(new RawPointerEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot,
+                        RawPointerEventType.Move, ev.Position, ev.Modifiers));
             }
 
             if (ev.Type == XiEventType.XI_ButtonPress || ev.Type == XiEventType.XI_ButtonRelease)
             {
                 var down = ev.Type == XiEventType.XI_ButtonPress;
                 var type =
-                    ev.Button == 1 ? (down ? RawMouseEventType.LeftButtonDown : RawMouseEventType.LeftButtonUp)
-                    : ev.Button == 2 ? (down ? RawMouseEventType.MiddleButtonDown : RawMouseEventType.MiddleButtonUp)
-                    : ev.Button == 3 ? (down ? RawMouseEventType.RightButtonDown : RawMouseEventType.RightButtonUp)
-                    : (RawMouseEventType?)null;
+                    ev.Button == 1 ? (down ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp)
+                    : ev.Button == 2 ? (down ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp)
+                    : ev.Button == 3 ? (down ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp)
+                    : (RawPointerEventType?)null;
                 if (type.HasValue)
-                    client.ScheduleInput(new RawMouseEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot,
+                    client.ScheduleInput(new RawPointerEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot,
                         type.Value, ev.Position, ev.Modifiers));
             }
-
+            
             _pointerDevice.UpdateValuators(ev.Valuators);
         }
     }
@@ -222,6 +251,8 @@ namespace Avalonia.X11
         public ulong Timestamp { get; }
         public Point Position { get; }
         public int Button { get; set; }
+        public int Detail { get; set; }
+        public bool Emulated { get; set; }
         public Dictionary<int, double> Valuators { get; }
         public ParsedDeviceEvent(XIDeviceEvent* ev)
         {
@@ -258,6 +289,8 @@ namespace Avalonia.X11
                     Valuators[c] = *values++;
             if (Type == XiEventType.XI_ButtonPress || Type == XiEventType.XI_ButtonRelease)
                 Button = ev->detail;
+            Detail = ev->detail;
+            Emulated = ev->flags.HasFlag(XiDeviceEventFlags.XIPointerEmulated);
         }
     }
     

+ 8 - 1
src/Avalonia.X11/XIStructs.cs

@@ -230,13 +230,20 @@ namespace Avalonia.X11
         public double root_y;
         public double event_x;
         public double event_y;
-        public int flags;
+        public XiDeviceEventFlags flags;
         public XIButtonState buttons;
         public XIValuatorState valuators;
         public XIModifierState mods;
         public XIModifierState group;
     }
 
+    [Flags]
+    public enum XiDeviceEventFlags : int
+    {
+        None = 0,
+        XIPointerEmulated = (1 << 16)
+    }
+
     [StructLayout(LayoutKind.Sequential)]
     unsafe struct XIEvent
     {

+ 9 - 9
src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs

@@ -145,17 +145,17 @@ namespace Avalonia.Gtk3
         private unsafe bool OnButton(IntPtr w, IntPtr ev, IntPtr userdata)
         {
             var evnt = (GdkEventButton*)ev;
-            var e = new RawMouseEventArgs(
+            var e = new RawPointerEventArgs(
                 Gtk3Platform.Mouse,
                 evnt->time,
                 _inputRoot,
                 evnt->type == GdkEventType.ButtonRelease
                     ? evnt->button == 1
-                        ? RawMouseEventType.LeftButtonUp
-                        : evnt->button == 3 ? RawMouseEventType.RightButtonUp : RawMouseEventType.MiddleButtonUp
+                        ? RawPointerEventType.LeftButtonUp
+                        : evnt->button == 3 ? RawPointerEventType.RightButtonUp : RawPointerEventType.MiddleButtonUp
                     : evnt->button == 1
-                        ? RawMouseEventType.LeftButtonDown
-                        : evnt->button == 3 ? RawMouseEventType.RightButtonDown : RawMouseEventType.MiddleButtonDown,
+                        ? RawPointerEventType.LeftButtonDown
+                        : evnt->button == 3 ? RawPointerEventType.RightButtonDown : RawPointerEventType.MiddleButtonDown,
                 new Point(evnt->x, evnt->y), GetModifierKeys(evnt->state));
             OnInput(e);
             return true;
@@ -179,11 +179,11 @@ namespace Avalonia.Gtk3
             var evnt = (GdkEventMotion*)ev;
             var position = new Point(evnt->x, evnt->y);
             Native.GdkEventRequestMotions(ev);
-            var e = new RawMouseEventArgs(
+            var e = new RawPointerEventArgs(
                 Gtk3Platform.Mouse,
                 evnt->time,
                 _inputRoot,
-                RawMouseEventType.Move,
+                RawPointerEventType.Move,
                 position, GetModifierKeys(evnt->state));
             OnInput(e);
             
@@ -237,10 +237,10 @@ namespace Avalonia.Gtk3
         {
             var evnt = (GdkEventCrossing*) pev;
             var position = new Point(evnt->x, evnt->y);
-            OnInput(new RawMouseEventArgs(Gtk3Platform.Mouse,
+            OnInput(new RawPointerEventArgs(Gtk3Platform.Mouse,
                 evnt->time,
                 _inputRoot,
-                RawMouseEventType.Move,
+                RawPointerEventType.Move,
                 position, GetModifierKeys(evnt->state)));
             return true;
         }

+ 9 - 9
src/Linux/Avalonia.LinuxFramebuffer/Mice.cs

@@ -76,9 +76,9 @@ namespace Avalonia.LinuxFramebuffer
                     _y = Math.Min(_height, Math.Max(0, _y + ev.value));
                 else
                     return;
-                Event?.Invoke(new RawMouseEventArgs(LinuxFramebufferPlatform.MouseDevice,
+                Event?.Invoke(new RawPointerEventArgs(LinuxFramebufferPlatform.MouseDevice,
                     LinuxFramebufferPlatform.Timestamp,
-                    LinuxFramebufferPlatform.TopLevel.InputRoot, RawMouseEventType.Move, new Point(_x, _y),
+                    LinuxFramebufferPlatform.TopLevel.InputRoot, RawPointerEventType.Move, new Point(_x, _y),
                     InputModifiers.None));
             }
             if (ev.type ==(int) EvType.EV_ABS)
@@ -89,24 +89,24 @@ namespace Avalonia.LinuxFramebuffer
                     _y = TranslateAxis(device.AbsY.Value, ev.value, _height);
                 else
                     return;
-                Event?.Invoke(new RawMouseEventArgs(LinuxFramebufferPlatform.MouseDevice,
+                Event?.Invoke(new RawPointerEventArgs(LinuxFramebufferPlatform.MouseDevice,
                     LinuxFramebufferPlatform.Timestamp,
-                    LinuxFramebufferPlatform.TopLevel.InputRoot, RawMouseEventType.Move, new Point(_x, _y),
+                    LinuxFramebufferPlatform.TopLevel.InputRoot, RawPointerEventType.Move, new Point(_x, _y),
                     InputModifiers.None));
             }
             if (ev.type == (short) EvType.EV_KEY)
             {
-                RawMouseEventType? type = null;
+                RawPointerEventType? type = null;
                 if (ev.code == (ushort) EvKey.BTN_LEFT)
-                    type = ev.value == 1 ? RawMouseEventType.LeftButtonDown : RawMouseEventType.LeftButtonUp;
+                    type = ev.value == 1 ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp;
                 if (ev.code == (ushort)EvKey.BTN_RIGHT)
-                    type = ev.value == 1 ? RawMouseEventType.RightButtonDown : RawMouseEventType.RightButtonUp;
+                    type = ev.value == 1 ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp;
                 if (ev.code == (ushort) EvKey.BTN_MIDDLE)
-                    type = ev.value == 1 ? RawMouseEventType.MiddleButtonDown : RawMouseEventType.MiddleButtonUp;
+                    type = ev.value == 1 ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp;
                 if (!type.HasValue)
                     return;
 
-                Event?.Invoke(new RawMouseEventArgs(LinuxFramebufferPlatform.MouseDevice,
+                Event?.Invoke(new RawPointerEventArgs(LinuxFramebufferPlatform.MouseDevice,
                     LinuxFramebufferPlatform.Timestamp,
                     LinuxFramebufferPlatform.TopLevel.InputRoot, type.Value, new Point(_x, _y), default(InputModifiers)));
             }

+ 12 - 12
src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs

@@ -160,19 +160,19 @@ namespace Avalonia.Win32.Interop.Wpf
             return rv;
         }
 
-        void MouseEvent(RawMouseEventType type, MouseEventArgs e)
-            => _ttl.Input?.Invoke(new RawMouseEventArgs(_mouse, (uint)e.Timestamp, _inputRoot, type,
+        void MouseEvent(RawPointerEventType type, MouseEventArgs e)
+            => _ttl.Input?.Invoke(new RawPointerEventArgs(_mouse, (uint)e.Timestamp, _inputRoot, type,
             e.GetPosition(this).ToAvaloniaPoint(), GetModifiers()));
 
         protected override void OnMouseDown(MouseButtonEventArgs e)
         {
-            RawMouseEventType type;
+            RawPointerEventType type;
             if(e.ChangedButton == MouseButton.Left)
-                type = RawMouseEventType.LeftButtonDown;
+                type = RawPointerEventType.LeftButtonDown;
             else if (e.ChangedButton == MouseButton.Middle)
-                type = RawMouseEventType.MiddleButtonDown;
+                type = RawPointerEventType.MiddleButtonDown;
             else if (e.ChangedButton == MouseButton.Right)
-                type = RawMouseEventType.RightButtonDown;
+                type = RawPointerEventType.RightButtonDown;
             else
                 return;
             MouseEvent(type, e);
@@ -181,13 +181,13 @@ namespace Avalonia.Win32.Interop.Wpf
 
         protected override void OnMouseUp(MouseButtonEventArgs e)
         {
-            RawMouseEventType type;
+            RawPointerEventType type;
             if (e.ChangedButton == MouseButton.Left)
-                type = RawMouseEventType.LeftButtonUp;
+                type = RawPointerEventType.LeftButtonUp;
             else if (e.ChangedButton == MouseButton.Middle)
-                type = RawMouseEventType.MiddleButtonUp;
+                type = RawPointerEventType.MiddleButtonUp;
             else if (e.ChangedButton == MouseButton.Right)
-                type = RawMouseEventType.RightButtonUp;
+                type = RawPointerEventType.RightButtonUp;
             else
                 return;
             MouseEvent(type, e);
@@ -196,14 +196,14 @@ namespace Avalonia.Win32.Interop.Wpf
 
         protected override void OnMouseMove(MouseEventArgs e)
         {
-            MouseEvent(RawMouseEventType.Move, e);
+            MouseEvent(RawPointerEventType.Move, e);
         }
 
         protected override void OnMouseWheel(MouseWheelEventArgs e) =>
             _ttl.Input?.Invoke(new RawMouseWheelEventArgs(_mouse, (uint) e.Timestamp, _inputRoot,
                 e.GetPosition(this).ToAvaloniaPoint(), new Vector(0, e.Delta), GetModifiers()));
 
-        protected override void OnMouseLeave(MouseEventArgs e) => MouseEvent(RawMouseEventType.LeaveWindow, e);
+        protected override void OnMouseLeave(MouseEventArgs e) => MouseEvent(RawPointerEventType.LeaveWindow, e);
 
         protected override void OnKeyDown(KeyEventArgs e)
             => _ttl.Input?.Invoke(new RawKeyEventArgs(_keyboard, (uint) e.Timestamp, RawKeyEventType.KeyDown,

+ 72 - 0
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@@ -574,6 +574,7 @@ namespace Avalonia.Win32.Interop
             WM_AFXLAST = 0x037F,
             WM_PENWINFIRST = 0x0380,
             WM_PENWINLAST = 0x038F,
+            WM_TOUCH = 0x0240,
             WM_APP = 0x8000,
             WM_USER = 0x0400,
 
@@ -836,10 +837,16 @@ namespace Avalonia.Win32.Interop
 
         [DllImport("user32.dll")]
         public static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg);
+
+        [DllImport("user32")]
+        public static extern IntPtr GetMessageExtraInfo();
         
         [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "RegisterClassExW")]
         public static extern ushort RegisterClassEx(ref WNDCLASSEX lpwcx);
 
+        [DllImport("user32.dll")]
+        public static extern void RegisterTouchWindow(IntPtr hWnd, int flags);
+        
         [DllImport("user32.dll")]
         public static extern bool ReleaseCapture();
 
@@ -1035,6 +1042,17 @@ namespace Avalonia.Win32.Interop
         [return: MarshalAs(UnmanagedType.Bool)]
         public static extern bool GetMonitorInfo([In] IntPtr hMonitor, ref MONITORINFO lpmi);
 
+        [DllImport("user32")]
+        public static extern bool GetTouchInputInfo(
+            IntPtr hTouchInput,
+            uint        cInputs,
+            [Out]TOUCHINPUT[] pInputs,
+            int         cbSize
+        );
+        
+        [DllImport("user32")]
+        public static extern bool CloseTouchInputHandle(IntPtr hTouchInput);
+        
         [return: MarshalAs(UnmanagedType.Bool)]
         [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "PostMessageW")]
         public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
@@ -1309,6 +1327,60 @@ namespace Avalonia.Win32.Interop
             public IntPtr hIconSm;
         }
 
+        [StructLayout(LayoutKind.Sequential)]
+        public struct TOUCHINPUT
+        {
+            public int X;
+            public int Y;
+            public IntPtr Source;
+            public uint Id;
+            public TouchInputFlags Flags;
+            public int Mask;
+            public uint Time;
+            public IntPtr ExtraInfo;
+            public int CxContact;
+            public int CyContact;
+        }
+
+        [Flags]
+        public enum TouchInputFlags
+        {
+            /// <summary>
+            /// Movement has occurred. Cannot be combined with TOUCHEVENTF_DOWN.
+            /// </summary>
+            TOUCHEVENTF_MOVE = 0x0001,
+
+            /// <summary>
+            /// The corresponding touch point was established through a new contact. Cannot be combined with TOUCHEVENTF_MOVE or TOUCHEVENTF_UP.
+            /// </summary>
+            TOUCHEVENTF_DOWN = 0x0002,
+
+            /// <summary>
+            /// A touch point was removed.
+            /// </summary>
+            TOUCHEVENTF_UP = 0x0004,
+
+            /// <summary>
+            /// A touch point is in range. This flag is used to enable touch hover support on compatible hardware. Applications that do not want support for hover can ignore this flag.
+            /// </summary>
+            TOUCHEVENTF_INRANGE = 0x0008,
+
+            /// <summary>
+            /// Indicates that this TOUCHINPUT structure corresponds to a primary contact point. See the following text for more information on primary touch points.
+            /// </summary>
+            TOUCHEVENTF_PRIMARY = 0x0010,
+
+            /// <summary>
+            /// When received using GetTouchInputInfo, this input was not coalesced.
+            /// </summary>
+            TOUCHEVENTF_NOCOALESCE = 0x0020,
+
+            /// <summary>
+            /// The touch event came from the user's palm.
+            /// </summary>
+            TOUCHEVENTF_PALM = 0x0080
+        }
+
         [Flags]
         public enum OpenFileNameFlags
         {

+ 5 - 2
src/Windows/Avalonia.Win32/Win32Platform.cs

@@ -40,6 +40,7 @@ namespace Avalonia
     {
         public bool UseDeferredRendering { get; set; } = true;
         public bool AllowEglInitialization { get; set; }
+        public bool? EnableMultitouch { get; set; }
     }
 }
 
@@ -59,7 +60,8 @@ namespace Avalonia.Win32
             CreateMessageWindow();
         }
 
-        public static bool UseDeferredRendering { get; set; }
+        public static bool UseDeferredRendering => Options.UseDeferredRendering;
+        public static Win32PlatformOptions Options { get; private set; }
 
         public Size DoubleClickSize => new Size(
             UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CXDOUBLECLK),
@@ -74,6 +76,7 @@ namespace Avalonia.Win32
 
         public static void Initialize(Win32PlatformOptions options)
         {
+            Options = options;
             AvaloniaLocator.CurrentMutable
                 .Bind<IClipboard>().ToSingleton<ClipboardImpl>()
                 .Bind<IStandardCursorFactory>().ToConstant(CursorFactory.Instance)
@@ -88,7 +91,7 @@ namespace Avalonia.Win32
                 .Bind<IPlatformIconLoader>().ToConstant(s_instance);
             if (options.AllowEglInitialization)
                 Win32GlManager.Initialize();
-            UseDeferredRendering = options.UseDeferredRendering;
+            
             _uiThread = Thread.CurrentThread;
 
             if (OleContext.Current != null)

+ 61 - 17
src/Windows/Avalonia.Win32/WindowImpl.cs

@@ -30,6 +30,8 @@ namespace Avalonia.Win32
         private UnmanagedMethods.WndProc _wndProcDelegate;
         private string _className;
         private IntPtr _hwnd;
+        private bool _multitouch;
+        private TouchDevice _touchDevice = new TouchDevice();
         private IInputRoot _owner;
         private bool _trackingMouse;
         private bool _decorated = true;
@@ -414,6 +416,15 @@ namespace Avalonia.Win32
                 IntPtr.Zero);
         }
 
+        bool ShouldIgnoreTouchEmulatedMessage()
+        {
+            if (!_multitouch)
+                return false;
+            var marker = 0xFF515700L;
+            var info = GetMessageExtraInfo().ToInt64();
+            return (info & marker) == marker;
+        }
+        
         [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Using Win32 naming for consistency.")]
         protected virtual IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
         {
@@ -519,34 +530,40 @@ namespace Avalonia.Win32
                 case UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN:
                 case UnmanagedMethods.WindowsMessage.WM_RBUTTONDOWN:
                 case UnmanagedMethods.WindowsMessage.WM_MBUTTONDOWN:
-                    e = new RawMouseEventArgs(
+                    if(ShouldIgnoreTouchEmulatedMessage())
+                        break;
+                    e = new RawPointerEventArgs(
                         WindowsMouseDevice.Instance,
                         timestamp,
                         _owner,
                         msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN
-                            ? RawMouseEventType.LeftButtonDown
+                            ? RawPointerEventType.LeftButtonDown
                             : msg == (int)UnmanagedMethods.WindowsMessage.WM_RBUTTONDOWN
-                                ? RawMouseEventType.RightButtonDown
-                                : RawMouseEventType.MiddleButtonDown,
+                                ? RawPointerEventType.RightButtonDown
+                                : RawPointerEventType.MiddleButtonDown,
                         DipFromLParam(lParam), GetMouseModifiers(wParam));
                     break;
 
                 case UnmanagedMethods.WindowsMessage.WM_LBUTTONUP:
                 case UnmanagedMethods.WindowsMessage.WM_RBUTTONUP:
                 case UnmanagedMethods.WindowsMessage.WM_MBUTTONUP:
-                    e = new RawMouseEventArgs(
+                    if(ShouldIgnoreTouchEmulatedMessage())
+                        break;
+                    e = new RawPointerEventArgs(
                         WindowsMouseDevice.Instance,
                         timestamp,
                         _owner,
                         msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONUP
-                            ? RawMouseEventType.LeftButtonUp
+                            ? RawPointerEventType.LeftButtonUp
                             : msg == (int)UnmanagedMethods.WindowsMessage.WM_RBUTTONUP
-                                ? RawMouseEventType.RightButtonUp
-                                : RawMouseEventType.MiddleButtonUp,
+                                ? RawPointerEventType.RightButtonUp
+                                : RawPointerEventType.MiddleButtonUp,
                         DipFromLParam(lParam), GetMouseModifiers(wParam));
                     break;
 
                 case UnmanagedMethods.WindowsMessage.WM_MOUSEMOVE:
+                    if(ShouldIgnoreTouchEmulatedMessage())
+                        break;
                     if (!_trackingMouse)
                     {
                         var tm = new UnmanagedMethods.TRACKMOUSEEVENT
@@ -560,11 +577,11 @@ namespace Avalonia.Win32
                         UnmanagedMethods.TrackMouseEvent(ref tm);
                     }
 
-                    e = new RawMouseEventArgs(
+                    e = new RawPointerEventArgs(
                         WindowsMouseDevice.Instance,
                         timestamp,
                         _owner,
-                        RawMouseEventType.Move,
+                        RawPointerEventType.Move,
                         DipFromLParam(lParam), GetMouseModifiers(wParam));
 
                     break;
@@ -589,29 +606,52 @@ namespace Avalonia.Win32
 
                 case UnmanagedMethods.WindowsMessage.WM_MOUSELEAVE:
                     _trackingMouse = false;
-                    e = new RawMouseEventArgs(
+                    e = new RawPointerEventArgs(
                         WindowsMouseDevice.Instance,
                         timestamp,
                         _owner,
-                        RawMouseEventType.LeaveWindow,
+                        RawPointerEventType.LeaveWindow,
                         new Point(), WindowsKeyboardDevice.Instance.Modifiers);
                     break;
 
                 case UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN:
                 case UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN:
                 case UnmanagedMethods.WindowsMessage.WM_NCMBUTTONDOWN:
-                    e = new RawMouseEventArgs(
+                    e = new RawPointerEventArgs(
                         WindowsMouseDevice.Instance,
                         timestamp,
                         _owner,
                         msg == (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN
-                            ? RawMouseEventType.NonClientLeftButtonDown
+                            ? RawPointerEventType.NonClientLeftButtonDown
                             : msg == (int)UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN
-                                ? RawMouseEventType.RightButtonDown
-                                : RawMouseEventType.MiddleButtonDown,
+                                ? RawPointerEventType.RightButtonDown
+                                : RawPointerEventType.MiddleButtonDown,
                         new Point(0, 0), GetMouseModifiers(wParam));
                     break;
-
+                case WindowsMessage.WM_TOUCH:
+                    var touchInputs = new TOUCHINPUT[wParam.ToInt32()];
+                    if (GetTouchInputInfo(lParam, (uint)wParam.ToInt32(), touchInputs, Marshal.SizeOf<TOUCHINPUT>()))
+                    {
+                        foreach (var touchInput in touchInputs)
+                        {
+                            var pt = new POINT {X = touchInput.X / 100, Y = touchInput.Y / 100};
+                            UnmanagedMethods.ScreenToClient(_hwnd, ref pt);
+                            Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time,
+                                _owner,
+                                touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_UP) ?
+                                    RawPointerEventType.TouchEnd :
+                                    touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_DOWN) ?
+                                        RawPointerEventType.TouchBegin :
+                                        RawPointerEventType.TouchUpdate,
+                                new Point(pt.X, pt.Y),
+                                WindowsKeyboardDevice.Instance.Modifiers,
+                                touchInput.Id));
+                        }
+                        CloseTouchInputHandle(lParam);
+                        return IntPtr.Zero;
+                    }
+                    
+                    break;
                 case WindowsMessage.WM_NCPAINT:
                     if (!_decorated)
                     {
@@ -754,6 +794,10 @@ namespace Avalonia.Win32
 
             Handle = new PlatformHandle(_hwnd, PlatformConstants.WindowHandleType);
 
+            _multitouch = Win32Platform.Options.EnableMultitouch ?? false;
+            if (_multitouch)
+                RegisterTouchWindow(_hwnd, 0);
+            
             if (UnmanagedMethods.ShCoreAvailable)
             {
                 uint dpix, dpiy;

+ 1 - 0
src/iOS/Avalonia.iOS/Avalonia.iOS.csproj

@@ -6,6 +6,7 @@
   </PropertyGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
+    <PackageReference Include="System.Reflection.Emit" Version="4.3.0" ExcludeAssets="All" />
   </ItemGroup>
   <Import Project="..\..\Shared\PlatformSupport\PlatformSupport.projitems" Label="Shared" />
   <Import Project="..\..\..\build\Rx.props" />

+ 8 - 8
src/iOS/Avalonia.iOS/TopLevelImpl.cs

@@ -86,11 +86,11 @@ namespace Avalonia.iOS
             {
                 var location = touch.LocationInView(this).ToAvalonia();
 
-                Input?.Invoke(new RawMouseEventArgs(
+                Input?.Invoke(new RawPointerEventArgs(
                     iOSPlatform.MouseDevice,
                     (uint)touch.Timestamp,
                     _inputRoot,
-                    RawMouseEventType.LeftButtonUp,
+                    RawPointerEventType.LeftButtonUp,
                     location,
                     InputModifiers.None));
             }
@@ -104,11 +104,11 @@ namespace Avalonia.iOS
             {
                 var location = touch.LocationInView(this).ToAvalonia();
                 _touchLastPoint = location;
-                Input?.Invoke(new RawMouseEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot,
-                    RawMouseEventType.Move, location, InputModifiers.None));
+                Input?.Invoke(new RawPointerEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot,
+                    RawPointerEventType.Move, location, InputModifiers.None));
 
-                Input?.Invoke(new RawMouseEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot,
-                    RawMouseEventType.LeftButtonDown, location, InputModifiers.None));
+                Input?.Invoke(new RawPointerEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot,
+                    RawPointerEventType.LeftButtonDown, location, InputModifiers.None));
             }
         }
 
@@ -119,8 +119,8 @@ namespace Avalonia.iOS
             {
                 var location = touch.LocationInView(this).ToAvalonia();
                 if (iOSPlatform.MouseDevice.Captured != null)
-                    Input?.Invoke(new RawMouseEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot,
-                        RawMouseEventType.Move, location, InputModifiers.LeftMouseButton));
+                    Input?.Invoke(new RawPointerEventArgs(iOSPlatform.MouseDevice, (uint)touch.Timestamp, _inputRoot,
+                        RawPointerEventType.Move, location, InputModifiers.LeftMouseButton));
                 else
                 {
                     //magic number based on test - correction of 0.02 is working perfect

+ 34 - 72
tests/Avalonia.Controls.UnitTests/ButtonTests.cs

@@ -14,6 +14,8 @@ namespace Avalonia.Controls.UnitTests
 {
     public class ButtonTests
     {
+        private MouseTestHelper _helper = new MouseTestHelper();
+        
         [Fact]
         public void Button_Is_Disabled_When_Command_Is_Disabled()
         {
@@ -102,12 +104,8 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         public void Button_Raises_Click()
         {
-            var mouse = Mock.Of<IMouseDevice>();
             var renderer = Mock.Of<IRenderer>();
-            IInputElement captured = null;
-            Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny<IVisual>())).Returns(new Point(50, 50));
-            Mock.Get(mouse).Setup(m => m.Capture(It.IsAny<IInputElement>())).Callback<IInputElement>(v => captured = v);
-            Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured);
+            var pt = new Point(50, 50);
             Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
                 .Returns<Point, IVisual, Func<IVisual, bool>>((p, r, f) =>
                     r.Bounds.Contains(p) ? new IVisual[] { r } : new IVisual[0]);
@@ -122,15 +120,15 @@ namespace Avalonia.Controls.UnitTests
 
             target.Click += (s, e) => clicked = true;
 
-            RaisePointerEnter(target, mouse);
-            RaisePointerMove(target, mouse);
-            RaisePointerPressed(target, mouse, 1, MouseButton.Left);
+            RaisePointerEnter(target);
+            RaisePointerMove(target, pt);
+            RaisePointerPressed(target, 1, MouseButton.Left, pt);
 
-            Assert.Equal(captured, target);
+            Assert.Equal(_helper.Captured, target);
 
-            RaisePointerReleased(target, mouse, MouseButton.Left);
+            RaisePointerReleased(target, MouseButton.Left, pt);
 
-            Assert.Equal(captured, null);
+            Assert.Equal(_helper.Captured, null);
 
             Assert.True(clicked);
         }
@@ -138,12 +136,8 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         public void Button_Does_Not_Raise_Click_When_PointerReleased_Outside()
         {
-            var mouse = Mock.Of<IMouseDevice>();
             var renderer = Mock.Of<IRenderer>();
-            IInputElement captured = null;
-            Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny<IVisual>())).Returns(new Point(200, 50));
-            Mock.Get(mouse).Setup(m => m.Capture(It.IsAny<IInputElement>())).Callback<IInputElement>(v => captured = v);
-            Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured);
+            
             Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
                 .Returns<Point, IVisual, Func<IVisual, bool>>((p, r, f) =>
                     r.Bounds.Contains(p) ? new IVisual[] { r } : new IVisual[0]);
@@ -158,16 +152,16 @@ namespace Avalonia.Controls.UnitTests
 
             target.Click += (s, e) => clicked = true;
 
-            RaisePointerEnter(target, mouse);
-            RaisePointerMove(target, mouse);
-            RaisePointerPressed(target, mouse, 1, MouseButton.Left);
-            RaisePointerLeave(target, mouse);
+            RaisePointerEnter(target);
+            RaisePointerMove(target, new Point(50,50));
+            RaisePointerPressed(target, 1, MouseButton.Left, new Point(50, 50));
+            RaisePointerLeave(target);
 
-            Assert.Equal(captured, target);
+            Assert.Equal(_helper.Captured, target);
 
-            RaisePointerReleased(target, mouse, MouseButton.Left);
+            RaisePointerReleased(target, MouseButton.Left, new Point(200, 50));
 
-            Assert.Equal(captured, null);
+            Assert.Equal(_helper.Captured, null);
 
             Assert.False(clicked);
         }
@@ -175,12 +169,8 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         public void Button_With_RenderTransform_Raises_Click()
         {
-            var mouse = Mock.Of<IMouseDevice>();
             var renderer = Mock.Of<IRenderer>();
-            IInputElement captured = null;
-            Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny<IVisual>())).Returns(new Point(150, 50));
-            Mock.Get(mouse).Setup(m => m.Capture(It.IsAny<IInputElement>())).Callback<IInputElement>(v => captured = v);
-            Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured);
+            var pt = new Point(150, 50);
             Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
                 .Returns<Point, IVisual, Func<IVisual, bool>>((p, r, f) =>
                     r.Bounds.Contains(p.Transform(r.RenderTransform.Value.Invert())) ?
@@ -204,15 +194,15 @@ namespace Avalonia.Controls.UnitTests
 
             target.Click += (s, e) => clicked = true;
 
-            RaisePointerEnter(target, mouse);
-            RaisePointerMove(target, mouse);
-            RaisePointerPressed(target, mouse, 1, MouseButton.Left);
+            RaisePointerEnter(target);
+            RaisePointerMove(target, pt);
+            RaisePointerPressed(target, 1, MouseButton.Left, pt);
 
-            Assert.Equal(captured, target);
+            Assert.Equal(_helper.Captured, target);
 
-            RaisePointerReleased(target, mouse, MouseButton.Left);
+            RaisePointerReleased(target, MouseButton.Left, pt);
 
-            Assert.Equal(captured, null);
+            Assert.Equal(_helper.Captured, null);
 
             Assert.True(clicked);
         }
@@ -278,57 +268,29 @@ namespace Avalonia.Controls.UnitTests
             public PixelPoint PointToScreen(Point p) => throw new NotImplementedException();
         }
 
-        private void RaisePointerPressed(Button button, IMouseDevice device, int clickCount, MouseButton mouseButton)
+        private void RaisePointerPressed(Button button, int clickCount, MouseButton mouseButton, Point position)
         {
-            button.RaiseEvent(new PointerPressedEventArgs
-            {
-                RoutedEvent = InputElement.PointerPressedEvent,
-                Source = button,
-                MouseButton = mouseButton,
-                ClickCount = clickCount,
-                Device = device,
-            });
+            _helper.Down(button, mouseButton, position, clickCount: clickCount);
         }
 
-        private void RaisePointerReleased(Button button, IMouseDevice device, MouseButton mouseButton)
+        private void RaisePointerReleased(Button button, MouseButton mouseButton, Point pt)
         {
-            button.RaiseEvent(new PointerReleasedEventArgs
-            {
-                RoutedEvent = InputElement.PointerReleasedEvent,
-                Source = button,
-                MouseButton = mouseButton,
-                Device = device,
-            });
+            _helper.Up(button, mouseButton, pt);
         }
 
-        private void RaisePointerEnter(Button button, IMouseDevice device)
+        private void RaisePointerEnter(Button button)
         {
-            button.RaiseEvent(new PointerEventArgs
-            {
-                RoutedEvent = InputElement.PointerEnterEvent,
-                Source = button,
-                Device = device,
-            });
+            _helper.Enter(button);
         }
 
-        private void RaisePointerLeave(Button button, IMouseDevice device)
+        private void RaisePointerLeave(Button button)
         {
-            button.RaiseEvent(new PointerEventArgs
-            {
-                RoutedEvent = InputElement.PointerLeaveEvent,
-                Source = button,
-                Device = device,
-            });
+            _helper.Leave(button);
         }
 
-        private void RaisePointerMove(Button button, IMouseDevice device)
+        private void RaisePointerMove(Button button, Point pos)
         {
-            button.RaiseEvent(new PointerEventArgs
-            {
-                RoutedEvent = InputElement.PointerMovedEvent,
-                Source = button,
-                Device = device,
-            });
+            _helper.Move(button, pos);
         }
 
         private class TestCommand : ICommand

+ 5 - 9
tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs

@@ -15,6 +15,8 @@ namespace Avalonia.Controls.UnitTests
 {
     public class ComboBoxTests
     {
+        MouseTestHelper _helper = new MouseTestHelper();
+        
         [Fact]
         public void Clicking_On_Control_Toggles_IsDropDownOpen()
         {
@@ -23,17 +25,11 @@ namespace Avalonia.Controls.UnitTests
                 Items = new[] { "Foo", "Bar" },
             };
 
-            target.RaiseEvent(new PointerPressedEventArgs
-            {
-                RoutedEvent = InputElement.PointerPressedEvent,
-            });
-
+            _helper.Down(target);
+            _helper.Up(target);
             Assert.True(target.IsDropDownOpen);
 
-            target.RaiseEvent(new PointerPressedEventArgs
-            {
-                RoutedEvent = InputElement.PointerPressedEvent,
-            });
+            _helper.Down(target);
 
             Assert.False(target.IsDropDownOpen);
         }

+ 8 - 35
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@@ -14,6 +14,7 @@ namespace Avalonia.Controls.UnitTests
     public class ContextMenuTests
     {
         private Mock<IPopupImpl> popupImpl;
+        private MouseTestHelper _mouse = new MouseTestHelper();
 
         [Fact]
         public void Clicking_On_Control_Toggles_ContextMenu()
@@ -31,19 +32,11 @@ namespace Avalonia.Controls.UnitTests
 
                 new Window { Content = target };
 
-                target.RaiseEvent(new PointerReleasedEventArgs
-                {
-                    RoutedEvent = InputElement.PointerReleasedEvent,
-                    MouseButton = MouseButton.Right
-                });
+                _mouse.Click(target, MouseButton.Right);
 
                 Assert.True(sut.IsOpen);
 
-                target.RaiseEvent(new PointerReleasedEventArgs
-                {
-                    RoutedEvent = InputElement.PointerReleasedEvent,
-                    MouseButton = MouseButton.None
-                });
+                _mouse.Click(target);
 
                 Assert.False(sut.IsOpen);
                 popupImpl.Verify(x => x.Show(), Times.Once);
@@ -69,19 +62,11 @@ namespace Avalonia.Controls.UnitTests
 
                 Avalonia.Application.Current.MainWindow = window;
 
-                target.RaiseEvent(new PointerReleasedEventArgs
-                {
-                    RoutedEvent = InputElement.PointerReleasedEvent,
-                    MouseButton = MouseButton.Right
-                });
+                _mouse.Click(target, MouseButton.Right);
 
                 Assert.True(sut.IsOpen);
 
-                target.RaiseEvent(new PointerReleasedEventArgs
-                {
-                    RoutedEvent = InputElement.PointerReleasedEvent,
-                    MouseButton = MouseButton.Right
-                });
+                _mouse.Click(target, MouseButton.Right);
 
                 Assert.True(sut.IsOpen);
                 popupImpl.Verify(x => x.Hide(), Times.Once);
@@ -106,11 +91,7 @@ namespace Avalonia.Controls.UnitTests
 
                 sut.ContextMenuOpening += (c, e) => { eventCalled = true; e.Cancel = true; };
 
-                target.RaiseEvent(new PointerReleasedEventArgs
-                {
-                    RoutedEvent = InputElement.PointerReleasedEvent,
-                    MouseButton = MouseButton.Right
-                });
+                _mouse.Click(target, MouseButton.Right);
 
                 Assert.True(eventCalled);
                 Assert.False(sut.IsOpen);
@@ -136,19 +117,11 @@ namespace Avalonia.Controls.UnitTests
 
                 sut.ContextMenuClosing += (c, e) => { eventCalled = true; e.Cancel = true; };
 
-                target.RaiseEvent(new PointerReleasedEventArgs
-                {
-                    RoutedEvent = InputElement.PointerReleasedEvent,
-                    MouseButton = MouseButton.Right
-                });
+                _mouse.Click(target, MouseButton.Right);
 
                 Assert.True(sut.IsOpen);
 
-                target.RaiseEvent(new PointerReleasedEventArgs
-                {
-                    RoutedEvent = InputElement.PointerReleasedEvent,
-                    MouseButton = MouseButton.None
-                });
+                _mouse.Click(target, MouseButton.Right);
 
                 Assert.True(eventCalled);
                 Assert.True(sut.IsOpen);

+ 3 - 6
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@@ -16,6 +16,8 @@ namespace Avalonia.Controls.UnitTests
 {
     public class ListBoxTests
     {
+        private MouseTestHelper _mouse = new MouseTestHelper();
+        
         [Fact]
         public void Should_Use_ItemTemplate_To_Create_Item_Content()
         {
@@ -225,12 +227,7 @@ namespace Avalonia.Controls.UnitTests
 
         private void RaisePressedEvent(ListBox listBox, ListBoxItem item, MouseButton mouseButton)
         {
-            listBox.RaiseEvent(new PointerPressedEventArgs
-            {
-                Source = item,
-                RoutedEvent = InputElement.PointerPressedEvent,
-                MouseButton = mouseButton
-            });
+            _mouse.Click(listBox, item, mouseButton);
         }
 
         [Fact]

+ 9 - 32
tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs

@@ -18,6 +18,8 @@ namespace Avalonia.Controls.UnitTests
 {
     public class ListBoxTests_Single
     {
+        MouseTestHelper _mouse = new MouseTestHelper();
+        
         [Fact]
         public void Focusing_Item_With_Tab_Should_Not_Select_It()
         {
@@ -68,12 +70,7 @@ namespace Avalonia.Controls.UnitTests
             };
 
             ApplyTemplate(target);
-
-            target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs
-            {
-                RoutedEvent = InputElement.PointerPressedEvent,
-                MouseButton = MouseButton.Left,
-            });
+            _mouse.Click(target.Presenter.Panel.Children[0]);
 
             Assert.Equal(0, target.SelectedIndex);
         }
@@ -90,11 +87,7 @@ namespace Avalonia.Controls.UnitTests
             ApplyTemplate(target);
             target.SelectedIndex = 0;
 
-            target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs
-            {
-                RoutedEvent = InputElement.PointerPressedEvent,
-                MouseButton = MouseButton.Left,
-            });
+            _mouse.Click(target.Presenter.Panel.Children[0]);
 
             Assert.Equal(0, target.SelectedIndex);
         }
@@ -111,11 +104,7 @@ namespace Avalonia.Controls.UnitTests
 
             ApplyTemplate(target);
 
-            target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs
-            {
-                RoutedEvent = InputElement.PointerPressedEvent,
-                MouseButton = MouseButton.Left,
-            });
+            _mouse.Click(target.Presenter.Panel.Children[0]);
 
             Assert.Equal(0, target.SelectedIndex);
         }
@@ -133,11 +122,7 @@ namespace Avalonia.Controls.UnitTests
             ApplyTemplate(target);
             target.SelectedIndex = 0;
 
-            target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs
-            {
-                RoutedEvent = InputElement.PointerPressedEvent,
-                MouseButton = MouseButton.Left,
-            });
+            _mouse.Click(target.Presenter.Panel.Children[0]);
 
             Assert.Equal(-1, target.SelectedIndex);
         }
@@ -155,11 +140,7 @@ namespace Avalonia.Controls.UnitTests
             ApplyTemplate(target);
             target.SelectedIndex = 0;
 
-            target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs
-            {
-                RoutedEvent = InputElement.PointerPressedEvent,
-                MouseButton = MouseButton.Left,
-            });
+            _mouse.Click(target.Presenter.Panel.Children[0]);
 
             Assert.Equal(0, target.SelectedIndex);
         }
@@ -177,11 +158,7 @@ namespace Avalonia.Controls.UnitTests
             ApplyTemplate(target);
             target.SelectedIndex = 1;
 
-            target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs
-            {
-                RoutedEvent = InputElement.PointerPressedEvent,
-                MouseButton = MouseButton.Left,
-            });
+            _mouse.Click(target.Presenter.Panel.Children[0]);
 
             Assert.Equal(0, target.SelectedIndex);
         }
@@ -306,4 +283,4 @@ namespace Avalonia.Controls.UnitTests
             target.Presenter.ApplyTemplate();
         }
     }
-}
+}

+ 116 - 0
tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs

@@ -0,0 +1,116 @@
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls.UnitTests
+{
+    public class MouseTestHelper
+    {
+
+        class TestPointer : IPointer
+        {
+            public int Id { get; } = Pointer.GetNextFreeId();
+
+            public void Capture(IInputElement control)
+            {
+                Captured = control;
+            }
+
+            public IInputElement Captured { get; set; }
+            public PointerType Type => PointerType.Mouse;
+            public bool IsPrimary => true;
+        }
+        
+        TestPointer _pointer = new TestPointer();
+
+        private InputModifiers _pressedButtons;
+        public IInputElement Captured => _pointer.Captured;
+
+        InputModifiers Convert(MouseButton mouseButton)
+            => (mouseButton == MouseButton.Left ? InputModifiers.LeftMouseButton
+                : mouseButton == MouseButton.Middle ? InputModifiers.MiddleMouseButton
+                : mouseButton == MouseButton.Right ? InputModifiers.RightMouseButton : InputModifiers.None);
+        
+        int ButtonCount(PointerPointProperties props)
+        {
+            var rv = 0;
+            if (props.IsLeftButtonPressed)
+                rv++;
+            if (props.IsMiddleButtonPressed)
+                rv++;
+            if (props.IsRightButtonPressed)
+                rv++;
+            return rv;
+        }
+
+        private MouseButton _pressedButton;
+
+        InputModifiers GetModifiers(InputModifiers modifiers) => modifiers | _pressedButtons;
+        
+        public void Down(IInteractive target, MouseButton mouseButton = MouseButton.Left, Point position = default,
+            InputModifiers modifiers = default, int clickCount = 1)
+            => Down(target, target, mouseButton, position, modifiers, clickCount);
+        
+        public void Down(IInteractive target, IInteractive source, MouseButton mouseButton = MouseButton.Left, 
+            Point position = default, InputModifiers modifiers = default, int clickCount = 1)
+        {
+            _pressedButtons |= Convert(mouseButton);
+            var props = new PointerPointProperties(_pressedButtons);
+            if (ButtonCount(props) > 1)
+                Move(target, source, position);
+            else
+            {
+                _pressedButton = mouseButton;
+                target.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, props,
+                    GetModifiers(modifiers), clickCount));
+            }
+        }
+
+        public void Move(IInteractive target, in Point position, InputModifiers modifiers = default) => Move(target, target, position, modifiers);
+        public void Move(IInteractive target, IInteractive source, in Point position, InputModifiers modifiers = default)
+        {
+            target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, (IVisual)target, position,
+                new PointerPointProperties(_pressedButtons), GetModifiers(modifiers)));
+        }
+
+        public void Up(IInteractive target, MouseButton mouseButton = MouseButton.Left, Point position = default,
+            InputModifiers modifiers = default)
+            => Up(target, target, mouseButton, position, modifiers);
+        
+        public void Up(IInteractive target, IInteractive source, MouseButton mouseButton = MouseButton.Left,
+            Point position = default, InputModifiers modifiers = default)
+        {
+            var conv = Convert(mouseButton);
+            _pressedButtons = (_pressedButtons | conv) ^ conv;
+            var props = new PointerPointProperties(_pressedButtons);
+            if (ButtonCount(props) == 0)
+                target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position, props,
+                    GetModifiers(modifiers), _pressedButton));
+            else
+                Move(target, source, position);
+        }
+
+        public void Click(IInteractive target, MouseButton button = MouseButton.Left, Point position = default,
+            InputModifiers modifiers = default)
+            => Click(target, target, button, position, modifiers);
+        public void Click(IInteractive target, IInteractive source, MouseButton button = MouseButton.Left, 
+            Point position = default, InputModifiers modifiers = default)
+        {
+            Down(target, source, button, position, modifiers);
+            Up(target, source, button, position, modifiers);
+        }
+        
+        public void Enter(IInteractive target)
+        {
+            target.RaiseEvent(new PointerEventArgs(InputElement.PointerEnterEvent, target, _pointer, (IVisual)target, default,
+                new PointerPointProperties(_pressedButtons), _pressedButtons));
+        }
+
+        public void Leave(IInteractive target)
+        {
+            target.RaiseEvent(new PointerEventArgs(InputElement.PointerLeaveEvent, target, _pointer, (IVisual)target, default,
+                new PointerPointProperties(_pressedButtons), _pressedButtons));
+        }
+
+    }
+}

+ 40 - 14
tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs

@@ -2,6 +2,7 @@
 using Avalonia.Controls.Platform;
 using Avalonia.Input;
 using Avalonia.Interactivity;
+using Avalonia.VisualTree;
 using Moq;
 using Xunit;
 
@@ -9,6 +10,16 @@ namespace Avalonia.Controls.UnitTests.Platform
 {
     public class DefaultMenuInteractionHandlerTests
     {
+        static PointerEventArgs CreateArgs(RoutedEvent ev, IInteractive source) 
+            => new PointerEventArgs(ev, source, new FakePointer(), (IVisual)source, default, new PointerPointProperties(), default);
+
+        static PointerPressedEventArgs CreatePressed(IInteractive source) => new PointerPressedEventArgs(source,
+            new FakePointer(), (IVisual)source, default, new PointerPointProperties {IsLeftButtonPressed = true},
+            default);
+        
+        static PointerReleasedEventArgs CreateReleased(IInteractive source) => new PointerReleasedEventArgs(source,
+            new FakePointer(), (IVisual)source, default, new PointerPointProperties(), default, MouseButton.Left);
+        
         public class TopLevel
         {
             [Fact]
@@ -121,7 +132,8 @@ namespace Avalonia.Controls.UnitTests.Platform
                     x.IsTopLevel == true && 
                     x.HasSubMenu == true &&
                     x.Parent == menu);
-                var e = new PointerPressedEventArgs { MouseButton = MouseButton.Left, Source = item };
+
+                var e = CreatePressed(item);
 
                 target.PointerPressed(item, e);
                 Mock.Get(menu).Verify(x => x.Close());
@@ -141,7 +153,7 @@ namespace Avalonia.Controls.UnitTests.Platform
                     x.IsTopLevel == true &&
                     x.HasSubMenu == true &&
                     x.Parent == menu.Object);
-                var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = nextItem };
+                var e = CreateArgs(MenuItem.PointerEnterItemEvent, nextItem);
 
                 menu.SetupGet(x => x.SelectedItem).Returns(item);
 
@@ -161,7 +173,7 @@ namespace Avalonia.Controls.UnitTests.Platform
                 var target = new DefaultMenuInteractionHandler(false);
                 var menu = new Mock<IMenu>();
                 var item = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.Parent == menu.Object);
-                var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item };
+                var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item);
 
                 menu.SetupGet(x => x.SelectedItem).Returns(item);
                 target.PointerLeave(item, e);
@@ -176,7 +188,7 @@ namespace Avalonia.Controls.UnitTests.Platform
                 var target = new DefaultMenuInteractionHandler(false);
                 var menu = new Mock<IMenu>();
                 var item = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.Parent == menu.Object);
-                var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item };
+                var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item);
 
                 menu.SetupGet(x => x.IsOpen).Returns(true);
                 menu.SetupGet(x => x.SelectedItem).Returns(item);
@@ -330,7 +342,7 @@ namespace Avalonia.Controls.UnitTests.Platform
                 var menu = Mock.Of<IMenu>();
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem);
-                var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item };
+                var e = CreateArgs(MenuItem.PointerEnterItemEvent, item);
 
                 target.PointerEnter(item, e);
 
@@ -346,7 +358,7 @@ namespace Avalonia.Controls.UnitTests.Platform
                 var menu = Mock.Of<IMenu>();
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem && x.HasSubMenu == true);
-                var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item };
+                var e = CreateArgs(MenuItem.PointerEnterItemEvent, item);
 
                 target.PointerEnter(item, e);
                 Mock.Get(item).Verify(x => x.Open(), Times.Never);
@@ -366,7 +378,7 @@ namespace Avalonia.Controls.UnitTests.Platform
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem);
                 var sibling = Mock.Of<IMenuItem>(x => x.Parent == parentItem && x.HasSubMenu == true && x.IsSubMenuOpen == true);
-                var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item };
+                var e = CreateArgs(MenuItem.PointerEnterItemEvent, item);
 
                 Mock.Get(parentItem).SetupGet(x => x.SubItems).Returns(new[] { item, sibling });
 
@@ -386,7 +398,7 @@ namespace Avalonia.Controls.UnitTests.Platform
                 var menu = Mock.Of<IMenu>();
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem);
-                var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item };
+                var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item);
 
                 Mock.Get(parentItem).SetupGet(x => x.SelectedItem).Returns(item);
                 target.PointerLeave(item, e);
@@ -403,7 +415,7 @@ namespace Avalonia.Controls.UnitTests.Platform
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem);
                 var sibling = Mock.Of<IMenuItem>(x => x.Parent == parentItem);
-                var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item };
+                var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item);
 
                 Mock.Get(parentItem).SetupGet(x => x.SelectedItem).Returns(sibling);
                 target.PointerLeave(item, e);
@@ -419,7 +431,7 @@ namespace Avalonia.Controls.UnitTests.Platform
                 var menu = Mock.Of<IMenu>();
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem && x.HasSubMenu == true && x.IsPointerOverSubMenu == true);
-                var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item };
+                var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item);
 
                 target.PointerLeave(item, e);
 
@@ -434,7 +446,7 @@ namespace Avalonia.Controls.UnitTests.Platform
                 var menu = Mock.Of<IMenu>();
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem);
-                var e = new PointerReleasedEventArgs { MouseButton = MouseButton.Left, Source = item };
+                var e = CreateReleased(item);
 
                 target.PointerReleased(item, e);
 
@@ -452,8 +464,8 @@ namespace Avalonia.Controls.UnitTests.Platform
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem && x.HasSubMenu == true);
                 var childItem = Mock.Of<IMenuItem>(x => x.Parent == item);
-                var enter = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item };
-                var leave = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item };
+                var enter = CreateArgs(MenuItem.PointerEnterItemEvent, item);
+                var leave = CreateArgs(MenuItem.PointerLeaveItemEvent, item);
 
                 // Pointer enters item; item is selected.
                 target.PointerEnter(item, enter);
@@ -488,7 +500,7 @@ namespace Avalonia.Controls.UnitTests.Platform
                 var menu = Mock.Of<IMenu>();
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem && x.HasSubMenu == true);
-                var e = new PointerPressedEventArgs { MouseButton = MouseButton.Left, Source = item };
+                var e = CreatePressed(item);
 
                 target.PointerPressed(item, e);
 
@@ -537,5 +549,19 @@ namespace Avalonia.Controls.UnitTests.Platform
                 _action = action;
             }
         }
+        
+        class FakePointer : IPointer
+        {
+            public int Id { get; } = Pointer.GetNextFreeId();
+
+            public void Capture(IInputElement control)
+            {
+                Captured = control;
+            }
+
+            public IInputElement Captured { get; set; }
+            public PointerType Type { get; }
+            public bool IsPrimary { get; } = true;
+        }
     }
 }

+ 4 - 11
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@@ -22,6 +22,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
 {
     public class SelectingItemsControlTests
     {
+        private MouseTestHelper _helper = new MouseTestHelper();
+        
         [Fact]
         public void SelectedIndex_Should_Initially_Be_Minus_1()
         {
@@ -675,12 +677,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
 
             target.ApplyTemplate();
             target.Presenter.ApplyTemplate();
-
-            target.Presenter.Panel.Children[1].RaiseEvent(new PointerPressedEventArgs
-            {
-                RoutedEvent = InputElement.PointerPressedEvent,
-                MouseButton = MouseButton.Left,
-            });
+            _helper.Down((Interactive)target.Presenter.Panel.Children[1]);
 
             var panel = target.Presenter.Panel;
 
@@ -703,11 +700,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
             target.ApplyTemplate();
             target.Presenter.ApplyTemplate();
 
-            target.Presenter.Panel.Children[1].RaiseEvent(new PointerPressedEventArgs
-            {
-                RoutedEvent = InputElement.PointerPressedEvent,
-                MouseButton = MouseButton.Left,
-            });
+            _helper.Down(target.Presenter.Panel.Children[1]);
 
             items.RemoveAt(1);
 

+ 35 - 54
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@@ -19,6 +19,8 @@ namespace Avalonia.Controls.UnitTests
 {
     public class TreeViewTests
     {
+        MouseTestHelper _mouse = new MouseTestHelper();
+        
         [Fact]
         public void Items_Should_Be_Created()
         {
@@ -130,11 +132,7 @@ namespace Avalonia.Controls.UnitTests
 
             Assert.NotNull(container);
 
-            container.RaiseEvent(new PointerPressedEventArgs
-            {
-                RoutedEvent = InputElement.PointerPressedEvent,
-                MouseButton = MouseButton.Left,
-            });
+            _mouse.Click(container);
 
             Assert.Equal(item, target.SelectedItem);
             Assert.True(container.IsSelected);
@@ -165,12 +163,7 @@ namespace Avalonia.Controls.UnitTests
 
             Assert.True(container.IsSelected);
 
-            container.RaiseEvent(new PointerPressedEventArgs
-            {
-                RoutedEvent = InputElement.PointerPressedEvent,
-                MouseButton = MouseButton.Left,
-                InputModifiers = InputModifiers.Control
-            });
+            _mouse.Click(container, modifiers: InputModifiers.Control);
 
             Assert.Null(target.SelectedItem);
             Assert.False(container.IsSelected);
@@ -205,13 +198,8 @@ namespace Avalonia.Controls.UnitTests
 
             Assert.True(container1.IsSelected);
 
-            container2.RaiseEvent(new PointerPressedEventArgs
-            {
-                RoutedEvent = InputElement.PointerPressedEvent,
-                MouseButton = MouseButton.Left,
-                InputModifiers = InputModifiers.Control
-            });
-
+            _mouse.Click(container2, modifiers: InputModifiers.Control);
+            
             Assert.Equal(item2, target.SelectedItem);
             Assert.False(container1.IsSelected);
             Assert.True(container2.IsSelected);
@@ -242,15 +230,15 @@ namespace Avalonia.Controls.UnitTests
             var item1Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1);
             var item2Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2);
 
-            TreeTestHelper.ClickContainer(item1Container, InputModifiers.Control);
+            ClickContainer(item1Container, InputModifiers.Control);
             Assert.True(item1Container.IsSelected);
 
-            TreeTestHelper.ClickContainer(item2Container, InputModifiers.Control);
+            ClickContainer(item2Container, InputModifiers.Control);
             Assert.True(item2Container.IsSelected);
 
             Assert.Equal(new[] {item1, item2}, target.SelectedItems.OfType<Node>());
 
-            TreeTestHelper.ClickContainer(item1Container, InputModifiers.Control);
+            ClickContainer(item1Container, InputModifiers.Control);
             Assert.False(item1Container.IsSelected);
 
             Assert.DoesNotContain(item1, target.SelectedItems.OfType<Node>());
@@ -281,12 +269,12 @@ namespace Avalonia.Controls.UnitTests
             var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
             var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
 
-            TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None);
+            ClickContainer(fromContainer, InputModifiers.None);
 
             Assert.True(fromContainer.IsSelected);
 
-            TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift);
-            TreeTestHelper.AssertChildrenSelected(target, rootNode);
+            ClickContainer(toContainer, InputModifiers.Shift);
+            AssertChildrenSelected(target, rootNode);
         }
 
         [Fact]
@@ -314,12 +302,12 @@ namespace Avalonia.Controls.UnitTests
             var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
             var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
 
-            TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None);
+            ClickContainer(fromContainer, InputModifiers.None);
 
             Assert.True(fromContainer.IsSelected);
 
-            TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift);
-            TreeTestHelper.AssertChildrenSelected(target, rootNode);
+            ClickContainer(toContainer, InputModifiers.Shift);
+            AssertChildrenSelected(target, rootNode);
         }
 
         [Fact]
@@ -347,12 +335,12 @@ namespace Avalonia.Controls.UnitTests
             var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
             var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
 
-            TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None);
+            ClickContainer(fromContainer, InputModifiers.None);
 
-            TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift);
-            TreeTestHelper.AssertChildrenSelected(target, rootNode);
+            ClickContainer(toContainer, InputModifiers.Shift);
+            AssertChildrenSelected(target, rootNode);
 
-            TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None);
+            ClickContainer(fromContainer, InputModifiers.None);
 
             Assert.True(fromContainer.IsSelected);
 
@@ -656,7 +644,7 @@ namespace Avalonia.Controls.UnitTests
 
                 target.RaiseEvent(keyEvent);
 
-                TreeTestHelper.AssertChildrenSelected(target, rootNode);
+                AssertChildrenSelected(target, rootNode);
             }
         }
 
@@ -687,8 +675,8 @@ namespace Avalonia.Controls.UnitTests
                 var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
                 var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
 
-                TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None);
-                TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift);
+                ClickContainer(fromContainer, InputModifiers.None);
+                ClickContainer(toContainer, InputModifiers.Shift);
 
                 var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
                 var selectAllGesture = keymap.SelectAll.First();
@@ -702,7 +690,7 @@ namespace Avalonia.Controls.UnitTests
 
                 target.RaiseEvent(keyEvent);
 
-                TreeTestHelper.AssertChildrenSelected(target, rootNode);
+                AssertChildrenSelected(target, rootNode);
             }
         }
 
@@ -733,8 +721,8 @@ namespace Avalonia.Controls.UnitTests
                 var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
                 var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
 
-                TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None);
-                TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift);
+                ClickContainer(fromContainer, InputModifiers.None);
+                ClickContainer(toContainer, InputModifiers.Shift);
 
                 var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
                 var selectAllGesture = keymap.SelectAll.First();
@@ -748,7 +736,7 @@ namespace Avalonia.Controls.UnitTests
 
                 target.RaiseEvent(keyEvent);
 
-                TreeTestHelper.AssertChildrenSelected(target, rootNode);
+                AssertChildrenSelected(target, rootNode);
             }
         }
 
@@ -871,29 +859,22 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
-        private static class TreeTestHelper
+        void ClickContainer(IControl container, InputModifiers modifiers)
         {
-            public static void ClickContainer(IControl container, InputModifiers modifiers)
-            {
-                container.RaiseEvent(new PointerPressedEventArgs
-                {
-                    RoutedEvent = InputElement.PointerPressedEvent,
-                    MouseButton = MouseButton.Left,
-                    InputModifiers = modifiers
-                });
-            }
+            _mouse.Click(container, modifiers: modifiers);
+        }
 
-            public static void AssertChildrenSelected(TreeView treeView, Node rootNode)
+        void AssertChildrenSelected(TreeView treeView, Node rootNode)
+        {
+            foreach (var child in rootNode.Children)
             {
-                foreach (var child in rootNode.Children)
-                {
-                    var container = (TreeViewItem)treeView.ItemContainerGenerator.Index.ContainerFromItem(child);
+                var container = (TreeViewItem)treeView.ItemContainerGenerator.Index.ContainerFromItem(child);
 
-                    Assert.True(container.IsSelected);
-                }
+                Assert.True(container.IsSelected);
             }
         }
 
+
         private class Node : NotifyingBase
         {
             private IAvaloniaList<Node> _children;

+ 2 - 2
tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs

@@ -225,11 +225,11 @@ namespace Avalonia.Input.UnitTests
 
         private void SendMouseMove(IInputManager inputManager, TestRoot root, Point p = new Point())
         {
-            inputManager.ProcessInput(new RawMouseEventArgs(
+            inputManager.ProcessInput(new RawPointerEventArgs(
                 root.MouseDevice,
                 0,
                 root,
-                RawMouseEventType.Move,
+                RawPointerEventType.Move,
                 p,
                 InputModifiers.None));
         }

+ 2 - 0
tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj

@@ -4,6 +4,7 @@
     <TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks>
     <OutputType>Library</OutputType>
     <IsTestProject>true</IsTestProject>
+    <LangVersion>latest</LangVersion>
   </PropertyGroup>
   <Import Project="..\..\build\UnitTests.NetCore.targets" />
   <Import Project="..\..\build\UnitTests.NetFX.props" />
@@ -19,6 +20,7 @@
     <ProjectReference Include="..\..\src\Avalonia.Layout\Avalonia.Layout.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Visuals\Avalonia.Visuals.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Styling\Avalonia.Styling.csproj" />
+    <Compile Include="..\Avalonia.Controls.UnitTests\MouseTestHelper.cs" />
   </ItemGroup>
   <ItemGroup>
     <Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />

+ 9 - 10
tests/Avalonia.Interactivity.UnitTests/GestureTests.cs

@@ -3,6 +3,7 @@
 
 using System.Collections.Generic;
 using Avalonia.Controls;
+using Avalonia.Controls.UnitTests;
 using Avalonia.Input;
 using Xunit;
 
@@ -10,6 +11,8 @@ namespace Avalonia.Interactivity.UnitTests
 {
     public class GestureTests
     {
+        private MouseTestHelper _mouse = new MouseTestHelper();
+        
         [Fact]
         public void Tapped_Should_Follow_Pointer_Pressed_Released()
         {
@@ -27,8 +30,7 @@ namespace Avalonia.Interactivity.UnitTests
             border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br"));
             border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt"));
 
-            border.RaiseEvent(new PointerPressedEventArgs());
-            border.RaiseEvent(new PointerReleasedEventArgs());
+            _mouse.Click(border);
 
             Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt" }, result);
         }
@@ -47,8 +49,7 @@ namespace Avalonia.Interactivity.UnitTests
             decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt"));
             border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt"));
 
-            border.RaiseEvent(new PointerPressedEventArgs());
-            border.RaiseEvent(new PointerReleasedEventArgs());
+            _mouse.Click(border);
 
             Assert.Equal(new[] { "bt", "dt" }, result);
         }
@@ -72,9 +73,8 @@ namespace Avalonia.Interactivity.UnitTests
             border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt"));
             border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt"));
 
-            border.RaiseEvent(new PointerPressedEventArgs());
-            border.RaiseEvent(new PointerReleasedEventArgs());
-            border.RaiseEvent(new PointerPressedEventArgs { ClickCount = 2 });
+            _mouse.Click(border);
+            _mouse.Down(border, clickCount: 2);
 
             Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp", "bdt", "ddt" }, result);
         }
@@ -103,9 +103,8 @@ namespace Avalonia.Interactivity.UnitTests
             border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt"));
             border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt"));
 
-            border.RaiseEvent(new PointerPressedEventArgs());
-            border.RaiseEvent(new PointerReleasedEventArgs());
-            border.RaiseEvent(new PointerPressedEventArgs { ClickCount = 2 });
+            _mouse.Click(border);
+            _mouse.Down(border, clickCount: 2);
 
             Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp" }, result);
         }

+ 7 - 6
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs

@@ -137,8 +137,9 @@ namespace Avalonia.Markup.Xaml.UnitTests
                     .GetVisualChildren().First()
                     .GetVisualChildren().First()
                     .GetVisualChildren().First();
-                ((Control)item).RaiseEvent(new PointerPressedEventArgs {ClickCount = 20});
-                Assert.Equal(20, w.Args.ClickCount);
+
+                ((Control)item).DataContext = "test";
+                Assert.Equal("test", w.SavedContext);
             }
         }
         
@@ -161,10 +162,10 @@ namespace Avalonia.Markup.Xaml.UnitTests
     
     public class XamlIlBugTestsEventHandlerCodeBehind : Window
     {
-        public PointerPressedEventArgs Args;
-        public void HandlePointerPressed(object sender, PointerPressedEventArgs args)
+        public object SavedContext;
+        public void HandleDataContextChanged(object sender, EventArgs args)
         {
-            Args = args;
+            SavedContext = ((Control)sender).DataContext;
         }
 
         public XamlIlBugTestsEventHandlerCodeBehind()
@@ -178,7 +179,7 @@ namespace Avalonia.Markup.Xaml.UnitTests
   <ItemsControl>
     <ItemsControl.ItemTemplate>
       <DataTemplate>
-        <Button PointerPressed='HandlePointerPressed' Content='{Binding .}' />
+        <Button DataContextChanged='HandleDataContextChanged' Content='{Binding .}' />
       </DataTemplate>
     </ItemsControl.ItemTemplate>
   </ItemsControl>