浏览代码

Merge branch 'master' into fixes/7381-reparenting-control-crash

Max Katz 3 年之前
父节点
当前提交
11b8c8f531

+ 1 - 1
.editorconfig

@@ -21,7 +21,7 @@ csharp_new_line_before_finally = true
 csharp_new_line_before_members_in_object_initializers = true
 csharp_new_line_before_members_in_anonymous_types = true
 csharp_new_line_between_query_expression_clauses = true
-trim_trailing_whitespace = true
+# trim_trailing_whitespace = true
 
 # Indentation preferences
 csharp_indent_block_contents = true

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

@@ -115,7 +115,6 @@ namespace ControlCatalog.NetCore
                 })
                 .With(new Win32PlatformOptions
                 {
-                    EnableMultitouch = true
                 })
                 .UseSkia()
                 .AfterSetup(builder =>

+ 1 - 1
samples/ControlCatalog/MainView.xaml

@@ -109,7 +109,7 @@
       <TabItem Header="OpenGL">
         <pages:OpenGlPage />
       </TabItem>
-      <TabItem Header="Pointers (Touch)">
+      <TabItem Header="Pointers">
         <pages:PointersPage />
       </TabItem>
       <TabItem Header="ProgressBar">

+ 235 - 0
samples/ControlCatalog/Pages/PointerCanvas.cs

@@ -0,0 +1,235 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Threading;
+
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Media;
+using Avalonia.Threading;
+
+namespace ControlCatalog.Pages;
+
+public class PointerCanvas : Control
+{
+    private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
+    private int _events;
+    private IDisposable? _statusUpdated;
+    private Dictionary<int, PointerPoints> _pointers = new();
+    private PointerPointProperties? _lastProperties;
+    private PointerUpdateKind? _lastNonOtherUpdateKind;
+    class PointerPoints
+    {
+        struct CanvasPoint
+        {
+            public IBrush Brush;
+            public Point Point;
+            public double Radius;
+            public double? Pressure;
+        }
+
+        readonly CanvasPoint[] _points = new CanvasPoint[1000];
+        int _index;
+
+        public void Render(DrawingContext context, bool drawPoints)
+        {
+            CanvasPoint? prev = null;
+            for (var c = 0; c < _points.Length; c++)
+            {
+                var i = (c + _index) % _points.Length;
+                var pt = _points[i];
+                var pressure = (pt.Pressure ?? prev?.Pressure ?? 0.5);
+                var thickness = pressure * 10;
+                var radius = pressure * pt.Radius;
+
+                if (drawPoints)
+                {
+                    if (pt.Brush != null)
+                    {
+                        context.DrawEllipse(pt.Brush, null, pt.Point, radius, radius);
+                    }
+                }
+                else
+                {
+                    if (prev.HasValue && prev.Value.Brush != null && pt.Brush != null
+                        && prev.Value.Pressure != null && pt.Pressure != null)
+                    {
+                        var linePen = new Pen(Brushes.Black, thickness, null, PenLineCap.Round, PenLineJoin.Round);
+                        context.DrawLine(linePen, prev.Value.Point, pt.Point);
+                    }
+                }
+                prev = pt;
+            }
+
+        }
+
+        void AddPoint(Point pt, IBrush brush, double radius, float? pressure = null)
+        {
+            _points[_index] = new CanvasPoint { Point = pt, Brush = brush, Radius = radius, Pressure = pressure };
+            _index = (_index + 1) % _points.Length;
+        }
+
+        public void HandleEvent(PointerEventArgs e, Visual v)
+        {
+            e.Handled = true;
+            var currentPoint = e.GetCurrentPoint(v);
+            if (e.RoutedEvent == PointerPressedEvent)
+                AddPoint(currentPoint.Position, Brushes.Green, 10);
+            else if (e.RoutedEvent == PointerReleasedEvent)
+                AddPoint(currentPoint.Position, Brushes.Red, 10);
+            else
+            {
+                var pts = e.GetIntermediatePoints(v);
+                for (var c = 0; c < pts.Count; c++)
+                {
+                    var pt = pts[c];
+                    AddPoint(pt.Position, c == pts.Count - 1 ? Brushes.Blue : Brushes.Black,
+                        c == pts.Count - 1 ? 5 : 2, pt.Properties.Pressure);
+                }
+            }
+        }
+    }
+
+    private int _threadSleep;
+    public static DirectProperty<PointerCanvas, int> ThreadSleepProperty =
+        AvaloniaProperty.RegisterDirect<PointerCanvas, int>(nameof(ThreadSleep), c => c.ThreadSleep, (c, v) => c.ThreadSleep = v);
+
+    public int ThreadSleep
+    {
+        get => _threadSleep;
+        set => SetAndRaise(ThreadSleepProperty, ref _threadSleep, value);
+    }
+
+    private bool _drawOnlyPoints;
+    public static DirectProperty<PointerCanvas, bool> DrawOnlyPointsProperty =
+        AvaloniaProperty.RegisterDirect<PointerCanvas, bool>(nameof(DrawOnlyPoints), c => c.DrawOnlyPoints, (c, v) => c.DrawOnlyPoints = v);
+
+    public bool DrawOnlyPoints
+    {
+        get => _drawOnlyPoints;
+        set => SetAndRaise(DrawOnlyPointsProperty, ref _drawOnlyPoints, value);
+    }
+
+    private string? _status;
+    public static DirectProperty<PointerCanvas, string?> StatusProperty =
+        AvaloniaProperty.RegisterDirect<PointerCanvas, string?>(nameof(DrawOnlyPoints), c => c.Status, (c, v) => c.Status = v,
+            defaultBindingMode: Avalonia.Data.BindingMode.TwoWay);
+
+    public string? Status
+    {
+        get => _status;
+        set => SetAndRaise(StatusProperty, ref _status, value);
+    }
+
+    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnAttachedToVisualTree(e);
+
+        _statusUpdated = DispatcherTimer.Run(() =>
+        {
+            if (_stopwatch.Elapsed.TotalMilliseconds > 250)
+            {
+                Status = $@"Events per second: {(_events / _stopwatch.Elapsed.TotalSeconds)}
+PointerUpdateKind: {_lastProperties?.PointerUpdateKind}
+Last PointerUpdateKind != Other: {_lastNonOtherUpdateKind}
+IsLeftButtonPressed: {_lastProperties?.IsLeftButtonPressed}
+IsRightButtonPressed: {_lastProperties?.IsRightButtonPressed}
+IsMiddleButtonPressed: {_lastProperties?.IsMiddleButtonPressed}
+IsXButton1Pressed: {_lastProperties?.IsXButton1Pressed}
+IsXButton2Pressed: {_lastProperties?.IsXButton2Pressed}
+IsBarrelButtonPressed: {_lastProperties?.IsBarrelButtonPressed}
+IsEraser: {_lastProperties?.IsEraser}
+IsInverted: {_lastProperties?.IsInverted}
+Pressure: {_lastProperties?.Pressure}
+XTilt: {_lastProperties?.XTilt}
+YTilt: {_lastProperties?.YTilt}
+Twist: {_lastProperties?.Twist}";
+                _stopwatch.Restart();
+                _events = 0;
+            }
+
+            return true;
+        }, TimeSpan.FromMilliseconds(10));
+    }
+
+    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnDetachedFromVisualTree(e);
+
+        _statusUpdated?.Dispose();
+    }
+
+    void HandleEvent(PointerEventArgs e)
+    {
+        _events++;
+
+        if (_threadSleep != 0)
+        {
+            Thread.Sleep(_threadSleep);
+        }
+        InvalidateVisual();
+
+        var lastPointer = e.GetCurrentPoint(this);
+        _lastProperties = lastPointer.Properties;
+
+        if (_lastProperties.PointerUpdateKind != PointerUpdateKind.Other)
+        {
+            _lastNonOtherUpdateKind = _lastProperties.PointerUpdateKind;
+        }
+
+        if (e.RoutedEvent == PointerReleasedEvent && e.Pointer.Type == PointerType.Touch)
+        {
+            _pointers.Remove(e.Pointer.Id);
+            return;
+        }
+
+        if (e.Pointer.Type != PointerType.Pen
+            || lastPointer.Properties.Pressure > 0)
+        {
+            if (!_pointers.TryGetValue(e.Pointer.Id, out var pt))
+                _pointers[e.Pointer.Id] = pt = new PointerPoints();
+            pt.HandleEvent(e, this);
+        }
+    }
+
+    public override void Render(DrawingContext context)
+    {
+        context.FillRectangle(Brushes.White, Bounds);
+        foreach (var pt in _pointers.Values)
+            pt.Render(context, _drawOnlyPoints);
+        base.Render(context);
+    }
+
+    protected override void OnPointerPressed(PointerPressedEventArgs e)
+    {
+        if (e.ClickCount == 2)
+        {
+            _pointers.Clear();
+            InvalidateVisual();
+            return;
+        }
+
+        HandleEvent(e);
+        base.OnPointerPressed(e);
+    }
+
+    protected override void OnPointerMoved(PointerEventArgs e)
+    {
+        HandleEvent(e);
+        base.OnPointerMoved(e);
+    }
+
+    protected override void OnPointerReleased(PointerReleasedEventArgs e)
+    {
+        HandleEvent(e);
+        base.OnPointerReleased(e);
+    }
+
+    protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
+    {
+        _lastProperties = null;
+        base.OnPointerCaptureLost(e);
+    }
+}

+ 109 - 0
samples/ControlCatalog/Pages/PointerContactsTab.cs

@@ -0,0 +1,109 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Linq;
+
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Media;
+using Avalonia.Media.Immutable;
+
+namespace ControlCatalog.Pages;
+
+public class PointerContactsTab : 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 PointerContactsTab()
+    {
+        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);
+        e.Handled = true;
+        base.OnPointerPressed(e);
+    }
+
+    protected override void OnPointerMoved(PointerEventArgs e)
+    {
+        UpdatePointer(e);
+        e.Handled = true;
+        base.OnPointerMoved(e);
+    }
+
+    protected override void OnPointerReleased(PointerReleasedEventArgs e)
+    {
+        _pointers.Remove(e.Pointer);
+        e.Handled = true;
+        InvalidateVisual();
+    }
+
+    protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs 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.DrawEllipse(brush, null, pt.Point, 75, 75);
+        }
+    }
+}

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

@@ -1,322 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Reactive.Linq;
-using System.Runtime.InteropServices;
-using System.Threading;
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Controls.Documents;
-using Avalonia.Input;
-using Avalonia.Layout;
-using Avalonia.Media;
-using Avalonia.Media.Immutable;
-using Avalonia.Threading;
-using Avalonia.VisualTree;
-
-namespace ControlCatalog.Pages;
-
-public class PointersPage : Decorator
-{
-    public PointersPage()
-    {
-        Child = new TabControl
-        {
-            Items = new[]
-            {
-                new TabItem() { Header = "Contacts", Content = new PointerContactsTab() },
-                new TabItem() { Header = "IntermediatePoints", Content = new PointerIntermediatePointsTab() }
-            }
-        };
-    }
-    
-    
-    class PointerContactsTab : 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 PointerContactsTab()
-        {
-            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);
-            e.Handled = true;
-            base.OnPointerPressed(e);
-        }
-
-        protected override void OnPointerMoved(PointerEventArgs e)
-        {
-            UpdatePointer(e);
-            e.Handled = true;
-            base.OnPointerMoved(e);
-        }
-
-        protected override void OnPointerReleased(PointerReleasedEventArgs e)
-        {
-            _pointers.Remove(e.Pointer);
-            e.Handled = true;
-            InvalidateVisual();
-        }
-
-        protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs 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.DrawEllipse(brush, null, pt.Point, 75, 75);
-            }
-        }
-    }
-
-    public class PointerIntermediatePointsTab : Decorator
-    {
-        public PointerIntermediatePointsTab()
-        {
-            this[TextElement.ForegroundProperty] = Brushes.Black;
-            var slider = new Slider
-            {
-                Margin = new Thickness(5),
-                Minimum = 0,
-                Maximum = 500
-            };
-
-            var status = new TextBlock()
-            {
-                HorizontalAlignment = HorizontalAlignment.Left,
-                VerticalAlignment = VerticalAlignment.Top,
-            };
-            Child = new Grid
-            {
-                Children =
-                {
-                    new PointerCanvas(slider, status),
-                    new Border
-                    {
-                        Background = Brushes.LightYellow,
-                        Child = new StackPanel
-                        {
-                            Children =
-                            {
-                                new StackPanel
-                                {
-                                    Orientation = Orientation.Horizontal,
-                                    Children =
-                                    {
-                                        new TextBlock { Text = "Thread sleep:" },
-                                        new TextBlock()
-                                        {
-                                            [!TextBlock.TextProperty] =slider.GetObservable(Slider.ValueProperty)
-                                                .Select(x=>x.ToString()).ToBinding()
-                                        }
-                                    }
-                                },
-                                slider
-                            }
-                        },
-
-                        HorizontalAlignment = HorizontalAlignment.Right,
-                        VerticalAlignment = VerticalAlignment.Top,
-                        Width = 300,
-                        Height = 60
-                    },
-                    status
-                }
-            };
-        }
-
-        class PointerCanvas : Control
-        {
-            private readonly Slider _slider;
-            private readonly TextBlock _status;
-            private int _events;
-            private Stopwatch _stopwatch = Stopwatch.StartNew();
-            private Dictionary<int, PointerPoints> _pointers = new();
-            class PointerPoints
-            {
-                struct CanvasPoint
-                {
-                    public IBrush Brush;
-                    public Point Point;
-                    public double Radius;
-                }
-
-                readonly CanvasPoint[] _points = new CanvasPoint[1000];
-                int _index;
-                
-                public  void Render(DrawingContext context)
-                {
-                    
-                    CanvasPoint? prev = null;
-                    for (var c = 0; c < _points.Length; c++)
-                    {
-                        var i = (c + _index) % _points.Length;
-                        var pt = _points[i];
-                        if (prev.HasValue && prev.Value.Brush != null && pt.Brush != null)
-                            context.DrawLine(new Pen(Brushes.Black), prev.Value.Point, pt.Point);
-                        prev = pt;
-                        if (pt.Brush != null)
-                            context.DrawEllipse(pt.Brush, null, pt.Point, pt.Radius, pt.Radius);
-
-                    }
-
-                }
-
-                void AddPoint(Point pt, IBrush brush, double radius)
-                {
-                    _points[_index] = new CanvasPoint { Point = pt, Brush = brush, Radius = radius };
-                    _index = (_index + 1) % _points.Length;
-                }
-
-                public void HandleEvent(PointerEventArgs e, Visual v)
-                {
-                    e.Handled = true;
-                    if (e.RoutedEvent == PointerPressedEvent)
-                        AddPoint(e.GetPosition(v), Brushes.Green, 10);
-                    else if (e.RoutedEvent == PointerReleasedEvent)
-                        AddPoint(e.GetPosition(v), Brushes.Red, 10);
-                    else
-                    {
-                        var pts = e.GetIntermediatePoints(v);
-                        for (var c = 0; c < pts.Count; c++)
-                        {
-                            var pt = pts[c];
-                            AddPoint(pt.Position, c == pts.Count - 1 ? Brushes.Blue : Brushes.Black,
-                                c == pts.Count - 1 ? 5 : 2);
-                        }
-                    }
-                }
-            }
-            
-            public PointerCanvas(Slider slider, TextBlock status)
-            {
-                _slider = slider;
-                _status = status;
-                DispatcherTimer.Run(() =>
-                {
-                    if (_stopwatch.Elapsed.TotalSeconds > 1)
-                    {
-                        _status.Text = "Events per second: " + (_events / _stopwatch.Elapsed.TotalSeconds);
-                        _stopwatch.Restart();
-                        _events = 0;
-                    }
-
-                    return this.GetVisualRoot() != null;
-                }, TimeSpan.FromMilliseconds(10));
-            }
-
-
-            void HandleEvent(PointerEventArgs e)
-            {
-                _events++;
-                Thread.Sleep((int)_slider.Value);
-                InvalidateVisual();
-
-                if (e.RoutedEvent == PointerReleasedEvent && e.Pointer.Type == PointerType.Touch)
-                {
-                    _pointers.Remove(e.Pointer.Id);
-                    return;
-                }
-
-                if (!_pointers.TryGetValue(e.Pointer.Id, out var pt))
-                    _pointers[e.Pointer.Id] = pt = new PointerPoints();
-                pt.HandleEvent(e, this);
-                
-                
-            }
-            
-            public override void Render(DrawingContext context)
-            {
-                context.FillRectangle(Brushes.White, Bounds);
-                foreach(var pt in _pointers.Values)
-                    pt.Render(context);
-                base.Render(context);
-            }
-
-            protected override void OnPointerPressed(PointerPressedEventArgs e)
-            {
-                if (e.ClickCount == 2)
-                {
-                    _pointers.Clear();
-                    InvalidateVisual();
-                    return;
-                }
-                
-                HandleEvent(e);
-                base.OnPointerPressed(e);
-            }
-
-            protected override void OnPointerMoved(PointerEventArgs e)
-            {
-                HandleEvent(e);
-                base.OnPointerMoved(e);
-            }
-
-            protected override void OnPointerReleased(PointerReleasedEventArgs e)
-            {
-                HandleEvent(e);
-                base.OnPointerReleased(e);
-            }
-        }
-    
-    }
-}

+ 66 - 0
samples/ControlCatalog/Pages/PointersPage.xaml

@@ -0,0 +1,66 @@
+<UserControl x:Class="ControlCatalog.Pages.PointersPage"
+             xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:local="using:ControlCatalog.Pages">
+  <TabControl>
+    <TabItem Header="Contacts">
+      <local:PointerContactsTab />
+    </TabItem>
+    <TabItem Header="IntermediatePoints">
+      <Panel TextElement.Foreground="Black">
+        <local:PointerCanvas x:Name="IntermediatePointsCanvas"
+                             DrawOnlyPoints="True"
+                             Status="{Binding #Status1TextBlock.Text, Mode=OneWayToSource}"
+                             ThreadSleep="{Binding #ThreadSleepSlider.Value}" />
+        <Border Width="300"
+                Height="60"
+                HorizontalAlignment="Right"
+                VerticalAlignment="Top">
+          <StackPanel Background="LightYellow">
+            <TextBlock Text="{Binding #ThreadSleepSlider.Value, StringFormat='Thread sleep: {0} / 500'}" />
+            <Slider x:Name="ThreadSleepSlider"
+                    Value="50"
+                    Maximum="500"
+                    Minimum="0" />
+          </StackPanel>
+        </Border>
+        <TextBlock x:Name="Status1TextBlock"
+                   HorizontalAlignment="Left"
+                   VerticalAlignment="Top" />
+      </Panel>
+    </TabItem>
+    <TabItem Header="Pressure">
+      <Panel TextElement.Foreground="Black">
+        <local:PointerCanvas x:Name="PressureCanvas"
+                             DrawOnlyPoints="False"
+                             Status="{Binding #Status2TextBlock.Text, Mode=OneWayToSource}"
+                             ThreadSleep="0" />
+        <TextBlock x:Name="Status2TextBlock"
+                   HorizontalAlignment="Left"
+                   VerticalAlignment="Top" />
+      </Panel>
+    </TabItem>
+    <TabItem Header="Capture">
+      <WrapPanel>
+        <Border Name="BorderCapture1"
+                MinWidth="250"
+                MinHeight="170"
+                Margin="5"
+                Padding="50"
+                Background="{DynamicResource SystemAccentColor}"
+                ToolTip.Placement="Bottom">
+          <TextBlock>Capture 1</TextBlock>
+        </Border>
+        <Border Name="BorderCapture2"
+                MinWidth="250"
+                MinHeight="170"
+                Margin="5"
+                Padding="50"
+                Background="{DynamicResource SystemAccentColor}"
+                ToolTip.Placement="Bottom">
+          <TextBlock>Capture 2</TextBlock>
+        </Border>
+      </WrapPanel>
+    </TabItem>
+  </TabControl>
+</UserControl>

+ 78 - 0
samples/ControlCatalog/Pages/PointersPage.xaml.cs

@@ -0,0 +1,78 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+
+namespace ControlCatalog.Pages;
+
+public class PointersPage : UserControl
+{
+    public PointersPage()
+    {
+        this.InitializeComponent();
+
+        var border1 = this.Get<Border>("BorderCapture1");
+        var border2 = this.Get<Border>("BorderCapture2");
+
+        border1.PointerPressed += Border_PointerPressed;
+        border1.PointerReleased += Border_PointerReleased;
+        border1.PointerCaptureLost += Border_PointerCaptureLost;
+        border1.PointerMoved += Border_PointerUpdated;
+        border1.PointerEntered += Border_PointerUpdated;
+        border1.PointerExited += Border_PointerUpdated;
+
+        border2.PointerPressed += Border_PointerPressed;
+        border2.PointerReleased += Border_PointerReleased;
+        border2.PointerCaptureLost += Border_PointerCaptureLost;
+        border2.PointerMoved += Border_PointerUpdated;
+        border2.PointerEntered += Border_PointerUpdated;
+        border2.PointerExited += Border_PointerUpdated;
+    }
+
+    private void Border_PointerUpdated(object sender, PointerEventArgs e)
+    {
+        var textBlock = (TextBlock)((Border)sender).Child;
+        var position = e.GetPosition((Border)sender);
+        textBlock.Text = @$"Type: {e.Pointer.Type}
+Captured: {e.Pointer.Captured == sender}
+PointerId: {e.Pointer.Id}
+Position: {(int)position.X} {(int)position.Y}";
+        e.Handled = true;
+    }
+
+    private void Border_PointerCaptureLost(object sender, PointerCaptureLostEventArgs e)
+    {
+        var textBlock = (TextBlock)((Border)sender).Child;
+        textBlock.Text = @$"Type: {e.Pointer.Type}
+Captured: {e.Pointer.Captured == sender}
+PointerId: {e.Pointer.Id}
+Position: ??? ???";
+        e.Handled = true;
+    }
+
+    private void Border_PointerReleased(object sender, PointerReleasedEventArgs e)
+    {
+        if (e.Pointer.Captured == sender)
+        {
+            e.Pointer.Capture(null);
+            e.Handled = true;
+        }
+        else
+        {
+            throw new InvalidOperationException("How?");
+        }
+    }
+
+    private void Border_PointerPressed(object sender, PointerPressedEventArgs e)
+    {
+        e.Pointer.Capture((Border)sender);
+        e.Handled = true;
+    }
+
+    private void InitializeComponent()
+    {
+        AvaloniaXamlLoader.Load(this);
+    }
+}

+ 3 - 2
src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs

@@ -66,7 +66,8 @@ namespace Avalonia.Input.GestureRecognizers
         
         public void PointerPressed(PointerPressedEventArgs e)
         {
-            if (e.Pointer.IsPrimary && e.Pointer.Type == PointerType.Touch)
+            if (e.Pointer.IsPrimary && 
+                (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen))
             {
                 EndGesture();
                 _tracking = e.Pointer;
@@ -101,7 +102,7 @@ namespace Avalonia.Input.GestureRecognizers
                 if (_scrolling)
                 {
                     var vector = _trackedRootPoint - rootPoint;
-                    var elapsed = _lastMoveTimestamp.HasValue ?
+                    var elapsed = _lastMoveTimestamp.HasValue && _lastMoveTimestamp < e.Timestamp ?
                         TimeSpan.FromMilliseconds(e.Timestamp - _lastMoveTimestamp.Value) :
                         TimeSpan.Zero;
                     

+ 6 - 1
src/Avalonia.Base/Input/IKeyboardDevice.cs

@@ -43,12 +43,17 @@ namespace Avalonia.Input
         Control = 2,
         Shift = 4,
         Meta = 8,
+
         LeftMouseButton = 16,
         RightMouseButton = 32,
         MiddleMouseButton = 64,
         XButton1MouseButton = 128,
         XButton2MouseButton = 256,
-        KeyboardMask = Alt | Control | Shift | Meta
+        KeyboardMask = Alt | Control | Shift | Meta,
+
+        PenInverted = 512,
+        PenEraser = 1024,
+        PenBarrelButton = 2048
     }
 
     [NotClientImplementable]

+ 10 - 0
src/Avalonia.Base/Input/IPenDevice.cs

@@ -0,0 +1,10 @@
+namespace Avalonia.Input
+{
+    /// <summary>
+    /// Represents a pen/stylus device.
+    /// </summary>
+    public interface IPenDevice : IPointerDevice
+    {
+
+    }
+}

+ 41 - 2
src/Avalonia.Base/Input/IPointer.cs

@@ -2,20 +2,59 @@ using Avalonia.Metadata;
 
 namespace Avalonia.Input
 {
+    /// <summary>
+    /// Identifies specific pointer generated by input device.
+    /// </summary>
+    /// <remarks>
+    /// Some devices, for instance, touchscreen might generate a pointer on each physical contact.
+    /// </remarks>
     [NotClientImplementable]
     public interface IPointer
     {
+        /// <summary>
+        /// Gets a unique identifier for the input pointer.
+        /// </summary>
         int Id { get; }
+
+        /// <summary>
+        /// Captures pointer input to the specified control.
+        /// </summary>
+        /// <param name="control">The control.</param>
+        /// <remarks>
+        /// When an element captures the pointer, it receives pointer input whether the cursor is 
+        /// within the control's bounds or not. The current pointer capture control is exposed
+        /// by the <see cref="Captured"/> property.
+        /// </remarks>
         void Capture(IInputElement? control);
+
+        /// <summary>
+        /// Gets the control that is currently capturing by the pointer, if any.
+        /// </summary>
+        /// <remarks>
+        /// When an element captures the pointer, it receives pointer input whether the cursor is 
+        /// within the control's bounds or not. To set the pointer capture, call the 
+        /// <see cref="Capture"/> method.
+        /// </remarks>
         IInputElement? Captured { get; }
+
+        /// <summary>
+        /// Gets the pointer device type.
+        /// </summary>
         PointerType Type { get; }
+
+        /// <summary>
+        /// Gets a value that indicates whether the input is from the primary pointer when multiple pointers are registered.
+        /// </summary>
         bool IsPrimary { get; }
-        
     }
 
+    /// <summary>
+    /// Enumerates pointer device types.
+    /// </summary>
     public enum PointerType
     {
         Mouse,
-        Touch
+        Touch,
+        Pen
     }
 }

+ 174 - 0
src/Avalonia.Base/Input/PenDevice.cs

@@ -0,0 +1,174 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Linq;
+using Avalonia.Input.Raw;
+using Avalonia.Platform;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Input
+{
+    /// <summary>
+    /// Represents a pen/stylus device.
+    /// </summary>
+    public class PenDevice : IPenDevice, IDisposable
+    {
+        private readonly Dictionary<long, Pointer> _pointers = new();
+        private readonly Dictionary<long, PixelPoint> _lastPositions = new();
+        private int _clickCount;
+        private Rect _lastClickRect;
+        private ulong _lastClickTime;
+        private MouseButton _lastMouseDownButton;
+
+        private bool _disposed;
+
+        public void ProcessRawEvent(RawInputEventArgs e)
+        {
+            if (!e.Handled && e is RawPointerEventArgs margs)
+                ProcessRawEvent(margs);
+        }
+
+        private void ProcessRawEvent(RawPointerEventArgs e)
+        {
+            e = e ?? throw new ArgumentNullException(nameof(e));
+
+            if (!_pointers.TryGetValue(e.RawPointerId, out var pointer))
+            {
+                if (e.Type == RawPointerEventType.LeftButtonUp
+                    || e.Type == RawPointerEventType.TouchEnd)
+                    return;
+
+                _pointers[e.RawPointerId] = pointer = new Pointer(Pointer.GetNextFreeId(),
+                    PointerType.Pen, _pointers.Count == 0);
+            }
+
+            _lastPositions[e.RawPointerId] = e.Root.PointToScreen(e.Position);
+
+            var props = new PointerPointProperties(e.InputModifiers, e.Type.ToUpdateKind(),
+                e.Point.Twist, e.Point.Pressure, e.Point.XTilt, e.Point.YTilt);
+            var keyModifiers = e.InputModifiers.ToKeyModifiers();
+
+            bool shouldReleasePointer = false;
+            switch (e.Type)
+            {
+                case RawPointerEventType.LeaveWindow:
+                    shouldReleasePointer = true;
+                    break;
+                case RawPointerEventType.LeftButtonDown:
+                    e.Handled = PenDown(pointer, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult);
+                    break;
+                case RawPointerEventType.LeftButtonUp:
+                    e.Handled = PenUp(pointer, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult);
+                    break;
+                case RawPointerEventType.Move:
+                    e.Handled = PenMove(pointer, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult, e.IntermediatePoints);
+                    break;
+            }
+
+            if (shouldReleasePointer)
+            {
+                pointer.Dispose();
+                _pointers.Remove(e.RawPointerId);
+                _lastPositions.Remove(e.RawPointerId);
+            }
+        }
+
+        private bool PenDown(Pointer pointer, ulong timestamp,
+            IInputElement root, Point p, PointerPointProperties properties,
+            KeyModifiers inputModifiers, IInputElement? hitTest)
+        {
+            var source = pointer.Captured ?? hitTest;
+
+            if (source != null)
+            {
+                pointer.Capture(source);
+                var settings = AvaloniaLocator.Current.GetService<IPlatformSettings>();
+                var doubleClickTime = settings?.DoubleClickTime.TotalMilliseconds ?? 500;
+                var doubleClickSize = settings?.DoubleClickSize ?? new Size(4, 4);
+
+                if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime)
+                {
+                    _clickCount = 0;
+                }
+
+                ++_clickCount;
+                _lastClickTime = timestamp;
+                _lastClickRect = new Rect(p, new Size())
+                    .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2));
+                _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton();
+                var e = new PointerPressedEventArgs(source, pointer, root, p, timestamp, properties, inputModifiers, _clickCount);
+                source.RaiseEvent(e);
+                return e.Handled;
+            }
+
+            return false;
+        }
+
+        private bool PenMove(Pointer pointer, ulong timestamp,
+            IInputRoot root, Point p, PointerPointProperties properties,
+            KeyModifiers inputModifiers, IInputElement? hitTest,
+            Lazy<IReadOnlyList<RawPointerPoint>?>? intermediatePoints)
+        {
+            var source = pointer.Captured ?? hitTest;
+
+            if (source is not null)
+            {
+                var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, pointer, root,
+                    p, timestamp, properties, inputModifiers, intermediatePoints);
+
+                source.RaiseEvent(e);
+                return e.Handled;
+            }
+
+            return false;
+        }
+
+        private bool PenUp(Pointer pointer, ulong timestamp,
+            IInputElement root, Point p, PointerPointProperties properties,
+            KeyModifiers inputModifiers, IInputElement? hitTest)
+        {
+            var source = pointer.Captured ?? hitTest;
+
+            if (source is not null)
+            {
+                var e = new PointerReleasedEventArgs(source, pointer, root, p, timestamp, properties, inputModifiers,
+                    _lastMouseDownButton);
+
+                source?.RaiseEvent(e);
+                pointer.Capture(null);
+                return e.Handled;
+            }
+
+            return false;
+        }
+
+        public void Dispose()
+        {
+            if (_disposed)
+                return;
+            var values = _pointers.Values.ToList();
+            _pointers.Clear();
+            _disposed = true;
+            foreach (var p in values)
+                p.Dispose();
+        }
+
+        [Obsolete]
+        IInputElement? IPointerDevice.Captured => _pointers.Values
+            .FirstOrDefault(p => p.IsPrimary)?.Captured;
+
+        [Obsolete]
+        void IPointerDevice.Capture(IInputElement? control) => _pointers.Values
+            .FirstOrDefault(p => p.IsPrimary)?.Capture(control);
+
+        [Obsolete]
+        Point IPointerDevice.GetPosition(IVisual relativeTo) => new Point(-1, -1);
+
+        public IPointer? TryGetPointer(RawPointerEventArgs ev)
+        {
+            return _pointers.TryGetValue(ev.RawPointerId, out var pointer)
+                ? pointer
+                : null;
+        }
+    }
+}

+ 19 - 3
src/Avalonia.Base/Input/PointerEventArgs.cs

@@ -67,7 +67,14 @@ namespace Avalonia.Input
             public IPointer? TryGetPointer(RawPointerEventArgs ev) => _ev.Pointer;
         }
 
+        /// <summary>
+        /// Gets specific pointer generated by input device.
+        /// </summary>
         public IPointer Pointer { get; }
+
+        /// <summary>
+        /// Gets the time when the input occurred.
+        /// </summary>
         public ulong Timestamp { get; }
 
         private IPointerDevice? _device;
@@ -91,7 +98,10 @@ namespace Avalonia.Input
                 return mods;
             }
         }
-        
+
+        /// <summary>
+        /// Gets a value that indicates which key modifiers were active at the time that the pointer event was initiated.
+        /// </summary>
         public KeyModifiers KeyModifiers { get; }
 
         private Point GetPosition(Point pt, IVisual? relativeTo)
@@ -102,7 +112,12 @@ namespace Avalonia.Input
                 return pt;
             return pt * _rootVisual.TransformToVisual(relativeTo) ?? default;
         }
-        
+
+        /// <summary>
+        /// Gets the pointer position relative to a control.
+        /// </summary>
+        /// <param name="relativeTo">The control.</param>
+        /// <returns>The pointer position in the control's coordinates.</returns>
         public Point GetPosition(IVisual? relativeTo) => GetPosition(_rootVisualPosition, relativeTo);
 
         [Obsolete("Use GetCurrentPoint")]
@@ -130,7 +145,8 @@ namespace Avalonia.Input
             for (var c = 0; c < previousPoints.Count; c++)
             {
                 var pt = previousPoints[c];
-                points[c] = new PointerPoint(Pointer, GetPosition(pt.Position, relativeTo), _properties);
+                var pointProperties = new PointerPointProperties(_properties, pt);
+                points[c] = new PointerPoint(Pointer, GetPosition(pt.Position, relativeTo), pointProperties);
             }
 
             points[points.Length - 1] = GetCurrentPoint(relativeTo);

+ 122 - 3
src/Avalonia.Base/Input/PointerPoint.cs

@@ -1,5 +1,10 @@
+using Avalonia.Input.Raw;
+
 namespace Avalonia.Input
 {
+    /// <summary>
+    /// Provides basic properties for the input pointer associated with a single mouse, pen/stylus, or touch contact.
+    /// </summary>
     public sealed class PointerPoint
     {
         public PointerPoint(IPointer pointer, Point position, PointerPointProperties properties)
@@ -8,25 +13,109 @@ namespace Avalonia.Input
             Position = position;
             Properties = properties;
         }
+
+        /// <summary>
+        /// Gets specific pointer generated by input device.
+        /// </summary>
         public IPointer Pointer { get; }
+
+        /// <summary>
+        /// Gets extended information about the input pointer.
+        /// </summary>
         public PointerPointProperties Properties { get; }
+
+        /// <summary>
+        /// Gets the location of the pointer input in client coordinates.
+        /// </summary>
         public Point Position { get; }
     }
 
+    /// <summary>
+    /// Provides extended properties for a PointerPoint object.
+    /// </summary>
     public sealed class PointerPointProperties
     {
+        /// <summary>
+        /// Gets a value that indicates whether the pointer input was triggered by the primary action mode of an input device.
+        /// </summary>
         public bool IsLeftButtonPressed { get; }
+
+        /// <summary>
+        /// Gets a value that indicates whether the pointer input was triggered by the tertiary action mode of an input device.
+        /// </summary>
         public bool IsMiddleButtonPressed { get; }
+
+        /// <summary>
+        /// Gets a value that indicates whether the pointer input was triggered by the secondary action mode (if supported) of an input device.
+        /// </summary>
         public bool IsRightButtonPressed { get; }
+
+        /// <summary>
+        /// Gets a value that indicates whether the pointer input was triggered by the first extended mouse button (XButton1).
+        /// </summary>
         public bool IsXButton1Pressed { get; }
+
+        /// <summary>
+        /// Gets a value that indicates whether the pointer input was triggered by the second extended mouse button (XButton2).
+        /// </summary>
         public bool IsXButton2Pressed { get; }
 
+        /// <summary>
+        /// Gets a value that indicates whether the barrel button of the pen/stylus device is pressed.
+        /// </summary>
+        public bool IsBarrelButtonPressed { get; }
+
+        /// <summary>
+        /// Gets a value that indicates whether the input is from a pen eraser.
+        /// </summary>
+        public bool IsEraser { get; }
+
+        /// <summary>
+        /// Gets a value that indicates whether the digitizer pen is inverted.
+        /// </summary>
+        public bool IsInverted { get; }
+
+        /// <summary>
+        /// Gets the clockwise rotation in degrees of a pen device around its own major axis (such as when the user spins the pen in their fingers).
+        /// </summary>
+        /// <returns>
+        /// A value between 0.0 and 359.0 in degrees of rotation. The default value is 0.0.
+        /// </returns>
+        public float Twist { get; }
+
+        /// <summary>
+        /// Gets a value that indicates the force that the pointer device (typically a pen/stylus) exerts on the surface of the digitizer.
+        /// </summary>
+        /// <returns>
+        /// A value from 0 to 1.0. The default value is 0.5.
+        /// </returns>
+        public float Pressure { get; } = 0.5f;
+
+        /// <summary>
+        /// Gets the plane angle between the Y-Z plane and the plane that contains the Y axis and the axis of the input device (typically a pen/stylus).
+        /// </summary>
+        /// <returns>
+        /// The value is 0.0 when the finger or pen is perpendicular to the digitizer surface, between 0.0 and 90.0 when tilted to the right of perpendicular, and between 0.0 and -90.0 when tilted to the left of perpendicular. The default value is 0.0.
+        /// </returns>
+        public float XTilt { get; }
+
+        /// <summary>
+        /// Gets the plane angle between the X-Z plane and the plane that contains the X axis and the axis of the input device (typically a pen/stylus).
+        /// </summary>
+        /// <returns>
+        /// The value is 0.0 when the finger or pen is perpendicular to the digitizer surface, between 0.0 and 90.0 when tilted towards the user, and between 0.0 and -90.0 when tilted away from the user. The default value is 0.0.
+        /// </returns>
+        public float YTilt { get; }
+
+        /// <summary>
+        /// Gets the kind of pointer state change.
+        /// </summary>
         public PointerUpdateKind PointerUpdateKind { get; }
 
         private PointerPointProperties()
-        {            
+        {
         }
-        
+
         public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind)
         {
             PointerUpdateKind = kind;
@@ -36,10 +125,13 @@ namespace Avalonia.Input
             IsRightButtonPressed = modifiers.HasAllFlags(RawInputModifiers.RightMouseButton);
             IsXButton1Pressed = modifiers.HasAllFlags(RawInputModifiers.XButton1MouseButton);
             IsXButton2Pressed = modifiers.HasAllFlags(RawInputModifiers.XButton2MouseButton);
+            IsInverted = modifiers.HasAllFlags(RawInputModifiers.PenInverted);
+            IsEraser = modifiers.HasAllFlags(RawInputModifiers.PenEraser);
+            IsBarrelButtonPressed = modifiers.HasAllFlags(RawInputModifiers.PenBarrelButton);
 
             // The underlying input source might be reporting the previous state,
             // so make sure that we reflect the current state
-            
+
             if (kind == PointerUpdateKind.LeftButtonPressed)
                 IsLeftButtonPressed = true;
             if (kind == PointerUpdateKind.LeftButtonReleased)
@@ -62,6 +154,33 @@ namespace Avalonia.Input
                 IsXButton2Pressed = false;
         }
 
+        public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind,
+            float twist, float pressure, float xTilt, float yTilt
+            ) : this (modifiers, kind)
+        {
+            Twist = twist;
+            Pressure = pressure;
+            XTilt = xTilt;
+            YTilt = yTilt;
+        }
+
+        internal PointerPointProperties(PointerPointProperties basedOn, RawPointerPoint rawPoint)
+        {
+            IsLeftButtonPressed = basedOn.IsLeftButtonPressed;
+            IsMiddleButtonPressed = basedOn.IsMiddleButtonPressed;
+            IsRightButtonPressed = basedOn.IsRightButtonPressed;
+            IsXButton1Pressed = basedOn.IsXButton1Pressed;
+            IsXButton2Pressed = basedOn.IsXButton2Pressed;
+            IsInverted = basedOn.IsInverted;
+            IsEraser = basedOn.IsEraser;
+            IsBarrelButtonPressed = basedOn.IsBarrelButtonPressed;
+
+            Twist = rawPoint.Twist;
+            Pressure = rawPoint.Pressure;
+            XTilt = rawPoint.XTilt;
+            YTilt = rawPoint.YTilt;
+        }
+
         public static PointerPointProperties None { get; } = new PointerPointProperties();
     }
 

+ 16 - 3
src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs

@@ -56,11 +56,12 @@ namespace Avalonia.Input.Raw
             Contract.Requires<ArgumentNullException>(device != null);
             Contract.Requires<ArgumentNullException>(root != null);
 
+            Point = new RawPointerPoint();
             Position = position;
             Type = type;
             InputModifiers = inputModifiers;
         }
-        
+
         /// <summary>
         /// Initializes a new instance of the <see cref="RawPointerEventArgs"/> class.
         /// </summary>
@@ -87,6 +88,11 @@ namespace Avalonia.Input.Raw
             InputModifiers = inputModifiers;
         }
 
+        /// <summary>
+        /// Gets the raw pointer identifier.
+        /// </summary>
+        public long RawPointerId { get; set; }
+
         /// <summary>
         /// Gets the pointer properties and position, in client DIPs.
         /// </summary>
@@ -130,10 +136,17 @@ namespace Avalonia.Input.Raw
         /// Pointer position, in client DIPs.
         /// </summary>
         public Point Position { get; set; }
-        
+
+        public float Twist { get; set; }
+        public float Pressure { get; set; }
+        public float XTilt { get; set; }
+        public float YTilt { get; set; }
+
+
         public RawPointerPoint()
         {
-            Position = default;
+            this = default;
+            Pressure = 0.5f;
         }
     }
 }

+ 14 - 3
src/Avalonia.Base/Input/Raw/RawTouchEventArgs.cs

@@ -1,15 +1,26 @@
+using System;
+
 namespace Avalonia.Input.Raw
 {
     public class RawTouchEventArgs : RawPointerEventArgs
     {
         public RawTouchEventArgs(IInputDevice device, ulong timestamp, IInputRoot root,
             RawPointerEventType type, Point position, RawInputModifiers inputModifiers,
-            long touchPointId) 
+            long rawPointerId) 
             : base(device, timestamp, root, type, position, inputModifiers)
         {
-            TouchPointId = touchPointId;
+            RawPointerId = rawPointerId;
+        }
+
+        public RawTouchEventArgs(IInputDevice device, ulong timestamp, IInputRoot root,
+            RawPointerEventType type, RawPointerPoint point, RawInputModifiers inputModifiers,
+            long rawPointerId)
+            : base(device, timestamp, root, type, point, inputModifiers)
+        {
+            RawPointerId = rawPointerId;
         }
 
-        public long TouchPointId { get; set; }
+        [Obsolete("Use RawPointerId")]
+        public long TouchPointId { get => RawPointerId; set => RawPointerId = value; }
     }
 }

+ 6 - 7
src/Avalonia.Base/Input/TouchDevice.cs

@@ -40,14 +40,14 @@ namespace Avalonia.Input
         {
             if (ev.Handled || _disposed)
                 return;
-            var args = (RawTouchEventArgs)ev;
-            if (!_pointers.TryGetValue(args.TouchPointId, out var pointer))
+            var args = (RawPointerEventArgs)ev;
+            if (!_pointers.TryGetValue(args.RawPointerId, out var pointer))
             {
                 if (args.Type == RawPointerEventType.TouchEnd)
                     return;
                 var hit = args.InputHitTestResult;
 
-                _pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(),
+                _pointers[args.RawPointerId] = pointer = new Pointer(Pointer.GetNextFreeId(),
                     PointerType.Touch, _pointers.Count == 0);
                 pointer.Capture(hit);
             }
@@ -88,7 +88,7 @@ namespace Avalonia.Input
 
             if (args.Type == RawPointerEventType.TouchEnd)
             {
-                _pointers.Remove(args.TouchPointId);
+                _pointers.Remove(args.RawPointerId);
                 using (pointer)
                 {
                     target.RaiseEvent(new PointerReleasedEventArgs(target, pointer,
@@ -101,7 +101,7 @@ namespace Avalonia.Input
 
             if (args.Type == RawPointerEventType.TouchCancel)
             {
-                _pointers.Remove(args.TouchPointId);
+                _pointers.Remove(args.RawPointerId);
                 using (pointer)
                     pointer.Capture(null);
                 _lastPointer = null;
@@ -129,8 +129,7 @@ namespace Avalonia.Input
 
         public IPointer? TryGetPointer(RawPointerEventArgs ev)
         {
-            return ev is RawTouchEventArgs args
-                && _pointers.TryGetValue(args.TouchPointId, out var pointer)
+            return _pointers.TryGetValue(ev.RawPointerId, out var pointer)
                 ? pointer
                 : null;
         }

+ 2 - 2
src/Avalonia.Controls.DataGrid/DataGridCell.cs

@@ -178,9 +178,9 @@ namespace Avalonia.Controls
                 {
                     var handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled);
 
-                    // Do not handle PointerPressed with touch,
+                    // Do not handle PointerPressed with touch or pen,
                     // so we can start scroll gesture on the same event.
-                    if (e.Pointer.Type != PointerType.Touch)
+                    if (e.Pointer.Type != PointerType.Touch && e.Pointer.Type != PointerType.Pen)
                     {
                         e.Handled = handled;
                     }

+ 18 - 10
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs

@@ -30,20 +30,28 @@ namespace Avalonia.Diagnostics.ViewModels
 
             if (control is AvaloniaObject ao)
             {
-                MarginThickness = ao.GetValue(Layoutable.MarginProperty);
-
-                if (HasPadding)
+                try
                 {
-                    PaddingThickness = ao.GetValue(Decorator.PaddingProperty);
-                }
+                    _updatingFromControl = true;
+                    MarginThickness = ao.GetValue(Layoutable.MarginProperty);
+
+                    if (HasPadding)
+                    {
+                        PaddingThickness = ao.GetValue(Decorator.PaddingProperty);
+                    }
 
-                if (HasBorder)
+                    if (HasBorder)
+                    {
+                        BorderThickness = ao.GetValue(Border.BorderThicknessProperty);
+                    }
+
+                    HorizontalAlignment = ao.GetValue(Layoutable.HorizontalAlignmentProperty);
+                    VerticalAlignment = ao.GetValue(Layoutable.VerticalAlignmentProperty);
+                }
+                finally
                 {
-                    BorderThickness = ao.GetValue(Border.BorderThicknessProperty);
+                    _updatingFromControl = false;
                 }
-
-                HorizontalAlignment = ao.GetValue(Layoutable.HorizontalAlignmentProperty);
-                VerticalAlignment = ao.GetValue(Layoutable.VerticalAlignmentProperty);
             }
 
             UpdateSize();

+ 5 - 7
src/Shared/RawEventGrouping.cs

@@ -2,10 +2,8 @@
 using System;
 using System.Collections.Generic;
 using Avalonia.Collections.Pooled;
-using Avalonia.Input;
 using Avalonia.Input.Raw;
 using Avalonia.Threading;
-using JetBrains.Annotations;
 
 namespace Avalonia;
 
@@ -19,7 +17,7 @@ internal class RawEventGrouper : IDisposable
     private readonly Action<RawInputEventArgs> _eventCallback;
     private readonly Queue<RawInputEventArgs> _inputQueue = new();
     private readonly Action _dispatchFromQueue;
-    readonly Dictionary<long, RawTouchEventArgs> _lastTouchPoints = new();
+    readonly Dictionary<long, RawPointerEventArgs> _lastTouchPoints = new();
     RawInputEventArgs? _lastEvent;
 
     public RawEventGrouper(Action<RawInputEventArgs> eventCallback)
@@ -49,7 +47,7 @@ internal class RawEventGrouper : IDisposable
                 _lastEvent = null;
             
             if (ev is RawTouchEventArgs { Type: RawPointerEventType.TouchUpdate } touchUpdate)
-                _lastTouchPoints.Remove(touchUpdate.TouchPointId);
+                _lastTouchPoints.Remove(touchUpdate.RawPointerId);
 
             _eventCallback?.Invoke(ev);
 
@@ -88,11 +86,11 @@ internal class RawEventGrouper : IDisposable
         {
             if (args is RawTouchEventArgs touchEvent)
             {
-                if (_lastTouchPoints.TryGetValue(touchEvent.TouchPointId, out var lastTouchEvent))
+                if (_lastTouchPoints.TryGetValue(touchEvent.RawPointerId, out var lastTouchEvent))
                     MergeEvents(lastTouchEvent, touchEvent);
                 else
                 {
-                    _lastTouchPoints[touchEvent.TouchPointId] = touchEvent;
+                    _lastTouchPoints[touchEvent.RawPointerId] = touchEvent;
                     AddToQueue(touchEvent);
                 }
             }
@@ -105,7 +103,7 @@ internal class RawEventGrouper : IDisposable
         {
             _lastTouchPoints.Clear();
             if (args is RawTouchEventArgs { Type: RawPointerEventType.TouchUpdate } touchEvent)
-                _lastTouchPoints[touchEvent.TouchPointId] = touchEvent;
+                _lastTouchPoints[touchEvent.RawPointerId] = touchEvent;
         }
         AddToQueue(args);
     }

+ 209 - 8
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@@ -225,20 +225,17 @@ namespace Avalonia.Win32.Interop
         [Flags]
         public enum ModifierKeys
         {
-            MK_CONTROL = 0x0008,
+            MK_NONE    = 0x0000,
 
             MK_LBUTTON = 0x0001,
-
-            MK_MBUTTON = 0x0010,
-
             MK_RBUTTON = 0x0002,
 
-            MK_SHIFT = 0x0004,
-
-            MK_ALT = 0x0020,
+            MK_SHIFT   = 0x0004,
+            MK_CONTROL = 0x0008,
 
+            MK_MBUTTON  = 0x0010,
+            MK_ALT      = 0x0020,
             MK_XBUTTON1 = 0x0020,
-
             MK_XBUTTON2 = 0x0040
         }
 
@@ -514,6 +511,33 @@ namespace Avalonia.Win32.Interop
             CS_DROPSHADOW = 0x00020000
         }
 
+        [Flags]
+        public enum PointerDeviceChangeFlags
+        {
+            PDC_ARRIVAL = 0x001,
+            PDC_REMOVAL = 0x002,
+            PDC_ORIENTATION_0 = 0x004,
+            PDC_ORIENTATION_90 = 0x008,
+            PDC_ORIENTATION_180 = 0x010,
+            PDC_ORIENTATION_270 = 0x020,
+            PDC_MODE_DEFAULT = 0x040,
+            PDC_MODE_CENTERED = 0x080,
+            PDC_MAPPING_CHANGE = 0x100,
+            PDC_RESOLUTION = 0x200,
+            PDC_ORIGIN = 0x400,
+            PDC_MODE_ASPECTRATIOPRESERVED = 0x800
+        }
+
+        public enum PointerInputType
+        {
+            PT_NONE = 0x00000000,
+            PT_POINTER = 0x00000001,
+            PT_TOUCH = 0x00000002,
+            PT_PEN = 0x00000003,
+            PT_MOUSE = 0x00000004,
+            PT_TOUCHPAD = 0x00000005
+        }
+
         public enum WindowsMessage : uint
         {
             WM_NULL = 0x0000,
@@ -689,6 +713,25 @@ namespace Avalonia.Win32.Interop
             WM_EXITSIZEMOVE = 0x0232,
             WM_DROPFILES = 0x0233,
             WM_MDIREFRESHMENU = 0x0234,
+
+            WM_POINTERDEVICECHANGE = 0x0238,
+            WM_POINTERDEVICEINRANGE = 0x239,
+            WM_POINTERDEVICEOUTOFRANGE = 0x23A,
+            WM_NCPOINTERUPDATE = 0x0241,
+            WM_NCPOINTERDOWN = 0x0242,
+            WM_NCPOINTERUP = 0x0243,
+            WM_POINTERUPDATE = 0x0245,
+            WM_POINTERDOWN = 0x0246,
+            WM_POINTERUP = 0x0247,
+            WM_POINTERENTER = 0x0249,
+            WM_POINTERLEAVE = 0x024A,
+            WM_POINTERACTIVATE = 0x024B,
+            WM_POINTERCAPTURECHANGED = 0x024C,
+            WM_TOUCHHITTESTING = 0x024D,
+            WM_POINTERWHEEL = 0x024E,
+            WM_POINTERHWHEEL = 0x024F,
+            DM_POINTERHITTEST = 0x0250,
+
             WM_IME_SETCONTEXT = 0x0281,
             WM_IME_NOTIFY = 0x0282,
             WM_IME_CONTROL = 0x0283,
@@ -844,6 +887,134 @@ namespace Avalonia.Win32.Interop
             SCF_ISSECURE = 0x00000001,
         }
 
+        [Flags]
+        public enum PointerFlags
+        {
+            POINTER_FLAG_NONE = 0x00000000,
+            POINTER_FLAG_NEW = 0x00000001,
+            POINTER_FLAG_INRANGE = 0x00000002,
+            POINTER_FLAG_INCONTACT = 0x00000004,
+            POINTER_FLAG_FIRSTBUTTON = 0x00000010,
+            POINTER_FLAG_SECONDBUTTON = 0x00000020,
+            POINTER_FLAG_THIRDBUTTON = 0x00000040,
+            POINTER_FLAG_FOURTHBUTTON = 0x00000080,
+            POINTER_FLAG_FIFTHBUTTON = 0x00000100,
+            POINTER_FLAG_PRIMARY = 0x00002000,
+            POINTER_FLAG_CONFIDENCE = 0x00000400,
+            POINTER_FLAG_CANCELED = 0x00000800,
+            POINTER_FLAG_DOWN = 0x00010000,
+            POINTER_FLAG_UPDATE = 0x00020000,
+            POINTER_FLAG_UP = 0x00040000,
+            POINTER_FLAG_WHEEL = 0x00080000,
+            POINTER_FLAG_HWHEEL = 0x00100000,
+            POINTER_FLAG_CAPTURECHANGED = 0x00200000,
+            POINTER_FLAG_HASTRANSFORM = 0x00400000
+        }
+
+        public enum PointerButtonChangeType : ulong
+        {
+            POINTER_CHANGE_NONE,
+            POINTER_CHANGE_FIRSTBUTTON_DOWN,
+            POINTER_CHANGE_FIRSTBUTTON_UP,
+            POINTER_CHANGE_SECONDBUTTON_DOWN,
+            POINTER_CHANGE_SECONDBUTTON_UP,
+            POINTER_CHANGE_THIRDBUTTON_DOWN,
+            POINTER_CHANGE_THIRDBUTTON_UP,
+            POINTER_CHANGE_FOURTHBUTTON_DOWN,
+            POINTER_CHANGE_FOURTHBUTTON_UP,
+            POINTER_CHANGE_FIFTHBUTTON_DOWN,
+            POINTER_CHANGE_FIFTHBUTTON_UP
+        }
+
+        [Flags]
+        public enum PenFlags
+        {
+            PEN_FLAGS_NONE = 0x00000000,
+            PEN_FLAGS_BARREL = 0x00000001,
+            PEN_FLAGS_INVERTED = 0x00000002,
+            PEN_FLAGS_ERASER = 0x00000004,
+        }
+
+        [Flags]
+        public enum PenMask
+        {
+            PEN_MASK_NONE = 0x00000000,
+            PEN_MASK_PRESSURE = 0x00000001,
+            PEN_MASK_ROTATION = 0x00000002,
+            PEN_MASK_TILT_X = 0x00000004,
+            PEN_MASK_TILT_Y = 0x00000008
+        }
+
+        [Flags]
+        public enum TouchFlags
+        {
+            TOUCH_FLAG_NONE = 0x00000000
+        }
+
+        [Flags]
+        public enum TouchMask
+        {
+            TOUCH_MASK_NONE = 0x00000000,
+            TOUCH_MASK_CONTACTAREA = 0x00000001,
+            TOUCH_MASK_ORIENTATION = 0x00000002,
+            TOUCH_MASK_PRESSURE = 0x00000004,
+        }
+
+        [StructLayout(LayoutKind.Sequential, Pack = 1)]
+        public struct POINTER_TOUCH_INFO
+        {
+            public POINTER_INFO pointerInfo;
+            public TouchFlags touchFlags;
+            public TouchMask touchMask;
+            public int rcContactLeft;
+            public int rcContactTop;
+            public int rcContactRight;
+            public int rcContactBottom;
+            public int rcContactRawLeft;
+            public int rcContactRawTop;
+            public int rcContactRawRight;
+            public int rcContactRawBottom;
+            public uint orientation;
+            public uint pressure;
+        }
+
+        [StructLayout(LayoutKind.Sequential, Pack = 1)]
+        public struct POINTER_PEN_INFO
+        {
+            public POINTER_INFO pointerInfo;
+            public PenFlags penFlags;
+            public PenMask penMask;
+            public uint pressure;
+            public uint rotation;
+            public int tiltX;
+            public int tiltY;
+        }
+
+        [StructLayout(LayoutKind.Sequential, Pack = 1)]
+        public struct POINTER_INFO
+        {
+            public PointerInputType pointerType;
+            public uint pointerId;
+            public uint frameId;
+            public PointerFlags pointerFlags;
+            public IntPtr sourceDevice;
+            public IntPtr hwndTarget;
+            public int ptPixelLocationX;
+            public int ptPixelLocationY;
+            public int ptHimetricLocationX;
+            public int ptHimetricLocationY;
+            public int ptPixelLocationRawX;
+            public int ptPixelLocationRawY;
+            public int ptHimetricLocationRawX;
+            public int ptHimetricLocationRawY;
+            public uint dwTime;
+            public uint historyCount;
+            public int inputData;
+            public ModifierKeys dwKeyStates;
+            public ulong PerformanceCount;
+            public PointerButtonChangeType ButtonChangeType;
+        }
+
         [StructLayout(LayoutKind.Sequential)]
         public struct RGBQUAD
         {
@@ -911,6 +1082,36 @@ namespace Avalonia.Win32.Interop
 
         public const int SizeOf_BITMAPINFOHEADER = 40;
 
+        [DllImport("user32.dll", SetLastError = true)]
+        public static extern bool IsMouseInPointerEnabled();
+
+        [DllImport("user32.dll", SetLastError = true)]
+        public static extern int EnableMouseInPointer(bool enable);
+
+        [DllImport("user32.dll", SetLastError = true)]
+        public static extern bool GetPointerCursorId(uint pointerId, out uint cursorId);
+
+        [DllImport("user32.dll", SetLastError = true)]
+        public static extern bool GetPointerType(uint pointerId, out PointerInputType pointerType);
+
+        [DllImport("user32.dll", SetLastError = true)]
+        public static extern bool GetPointerInfo(uint pointerId, out POINTER_INFO pointerInfo);
+
+        [DllImport("user32.dll", SetLastError = true)]
+        public static extern bool GetPointerInfoHistory(uint pointerId, ref int entriesCount, [MarshalAs(UnmanagedType.LPArray), In, Out] POINTER_INFO[] pointerInfos);
+
+        [DllImport("user32.dll", SetLastError = true)]
+        public static extern bool GetPointerPenInfo(uint pointerId, out POINTER_PEN_INFO penInfo);
+
+        [DllImport("user32.dll", SetLastError = true)]
+        public static extern bool GetPointerPenInfoHistory(uint pointerId, ref int entriesCount, [MarshalAs(UnmanagedType.LPArray), In, Out] POINTER_PEN_INFO[] penInfos);
+
+        [DllImport("user32.dll", SetLastError = true)]
+        public static extern bool GetPointerTouchInfo(uint pointerId, out POINTER_TOUCH_INFO touchInfo);
+
+        [DllImport("user32.dll", SetLastError = true)]
+        public static extern bool GetPointerTouchInfoHistory(uint pointerId, ref int entriesCount, [MarshalAs(UnmanagedType.LPArray), In, Out] POINTER_TOUCH_INFO[] touchInfos);
+
         [DllImport("user32.dll")]
         public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip,
                                                       MonitorEnumDelegate lpfnEnum, IntPtr dwData);

+ 1 - 0
src/Windows/Avalonia.Win32/Win32Platform.cs

@@ -68,6 +68,7 @@ namespace Avalonia
         /// <remarks>
         /// Multitouch allows a surface (a touchpad or touchscreen) to recognize the presence of more than one point of contact with the surface at the same time.
         /// </remarks>
+        [Obsolete("Multitouch is always enabled on supported Windows versions")]
         public bool? EnableMultitouch { get; set; } = true;
 
         /// <summary>

+ 407 - 22
src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs

@@ -1,4 +1,6 @@
 using System;
+using System.Buffers;
+using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.Runtime.InteropServices;
 using System.Text;
@@ -26,8 +28,8 @@ namespace Avalonia.Win32
             uint timestamp = unchecked((uint)GetMessageTime());
             RawInputEventArgs e = null;
             var shouldTakeFocus = false;
-
-            switch ((WindowsMessage)msg)
+            var message = (WindowsMessage)msg;
+            switch (message)
             {
                 case WindowsMessage.WM_ACTIVATE:
                     {
@@ -82,7 +84,7 @@ namespace Avalonia.Win32
                 case WindowsMessage.WM_DESTROY:
                     {
                         UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, IntPtr.Zero, IntPtr.Zero, null);
-                        
+
                         // We need to release IMM context and state to avoid leaks.
                         if (Imm32InputMethod.Current.HWND == _hwnd)
                         {
@@ -108,9 +110,9 @@ namespace Avalonia.Win32
                         var newDisplayRect = Marshal.PtrToStructure<RECT>(lParam);
                         _scaling = dpi / 96.0;
                         ScalingChanged?.Invoke(_scaling);
-                        
+
                         using (SetResizeReason(PlatformResizeReason.DpiChange))
-                        { 
+                        {
                             SetWindowPos(hWnd,
                                 IntPtr.Zero,
                                 newDisplayRect.left,
@@ -178,6 +180,10 @@ namespace Avalonia.Win32
                 case WindowsMessage.WM_MBUTTONDOWN:
                 case WindowsMessage.WM_XBUTTONDOWN:
                     {
+                        if (IsMouseInPointerEnabled)
+                        {
+                            break;
+                        }
                         shouldTakeFocus = ShouldTakeFocusOnClick;
                         if (ShouldIgnoreTouchEmulatedMessage())
                         {
@@ -188,7 +194,7 @@ namespace Avalonia.Win32
                             _mouseDevice,
                             timestamp,
                             _owner,
-                            (WindowsMessage)msg switch
+                            message switch
                             {
                                 WindowsMessage.WM_LBUTTONDOWN => RawPointerEventType.LeftButtonDown,
                                 WindowsMessage.WM_RBUTTONDOWN => RawPointerEventType.RightButtonDown,
@@ -207,6 +213,10 @@ namespace Avalonia.Win32
                 case WindowsMessage.WM_MBUTTONUP:
                 case WindowsMessage.WM_XBUTTONUP:
                     {
+                        if (IsMouseInPointerEnabled)
+                        {
+                            break;
+                        }
                         if (ShouldIgnoreTouchEmulatedMessage())
                         {
                             break;
@@ -216,7 +226,7 @@ namespace Avalonia.Win32
                             _mouseDevice,
                             timestamp,
                             _owner,
-                            (WindowsMessage)msg switch
+                            message switch
                             {
                                 WindowsMessage.WM_LBUTTONUP => RawPointerEventType.LeftButtonUp,
                                 WindowsMessage.WM_RBUTTONUP => RawPointerEventType.RightButtonUp,
@@ -231,11 +241,19 @@ namespace Avalonia.Win32
                     }
                 // Mouse capture is lost
                 case WindowsMessage.WM_CANCELMODE:
-                    _mouseDevice.Capture(null);
+                    if (!IsMouseInPointerEnabled)
+                    {
+                        _mouseDevice.Capture(null);
+                    }
+
                     break;
 
                 case WindowsMessage.WM_MOUSEMOVE:
                     {
+                        if (IsMouseInPointerEnabled)
+                        {
+                            break;
+                        }
                         if (ShouldIgnoreTouchEmulatedMessage())
                         {
                             break;
@@ -259,42 +277,58 @@ namespace Avalonia.Win32
                             timestamp,
                             _owner,
                             RawPointerEventType.Move,
-                            DipFromLParam(lParam), GetMouseModifiers(wParam));
+                            DipFromLParam(lParam),
+                            GetMouseModifiers(wParam));
 
                         break;
                     }
 
                 case WindowsMessage.WM_MOUSEWHEEL:
                     {
+                        if (IsMouseInPointerEnabled)
+                        {
+                            break;
+                        }
                         e = new RawMouseWheelEventArgs(
                             _mouseDevice,
                             timestamp,
                             _owner,
                             PointToClient(PointFromLParam(lParam)),
-                            new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta), GetMouseModifiers(wParam));
+                            new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta),
+                            GetMouseModifiers(wParam));
                         break;
                     }
 
                 case WindowsMessage.WM_MOUSEHWHEEL:
                     {
+                        if (IsMouseInPointerEnabled)
+                        {
+                            break;
+                        }
                         e = new RawMouseWheelEventArgs(
                             _mouseDevice,
                             timestamp,
                             _owner,
                             PointToClient(PointFromLParam(lParam)),
-                            new Vector(-(ToInt32(wParam) >> 16) / wheelDelta, 0), GetMouseModifiers(wParam));
+                            new Vector(-(ToInt32(wParam) >> 16) / wheelDelta, 0),
+                            GetMouseModifiers(wParam));
                         break;
                     }
 
                 case WindowsMessage.WM_MOUSELEAVE:
                     {
+                        if (IsMouseInPointerEnabled)
+                        {
+                            break;
+                        }
                         _trackingMouse = false;
                         e = new RawPointerEventArgs(
                             _mouseDevice,
                             timestamp,
                             _owner,
                             RawPointerEventType.LeaveWindow,
-                            new Point(-1, -1), WindowsKeyboardDevice.Instance.Modifiers);
+                            new Point(-1, -1),
+                            WindowsKeyboardDevice.Instance.Modifiers);
                         break;
                     }
 
@@ -303,11 +337,15 @@ namespace Avalonia.Win32
                 case WindowsMessage.WM_NCMBUTTONDOWN:
                 case WindowsMessage.WM_NCXBUTTONDOWN:
                     {
+                        if (IsMouseInPointerEnabled)
+                        {
+                            break;
+                        }
                         e = new RawPointerEventArgs(
                             _mouseDevice,
                             timestamp,
                             _owner,
-                            (WindowsMessage)msg switch
+                            message switch
                             {
                                 WindowsMessage.WM_NCLBUTTONDOWN => RawPointerEventType
                                     .NonClientLeftButtonDown,
@@ -323,6 +361,10 @@ namespace Avalonia.Win32
                     }
                 case WindowsMessage.WM_TOUCH:
                     {
+                        if (_wmPointerEnabled)
+                        {
+                            break;
+                        }
                         var touchInputCount = wParam.ToInt32();
 
                         var pTouchInputs = stackalloc TOUCHINPUT[touchInputCount];
@@ -348,6 +390,120 @@ namespace Avalonia.Win32
                             return IntPtr.Zero;
                         }
 
+                        break;
+                    }
+                case WindowsMessage.WM_NCPOINTERDOWN:
+                case WindowsMessage.WM_NCPOINTERUP:
+                case WindowsMessage.WM_POINTERDOWN:
+                case WindowsMessage.WM_POINTERUP:
+                case WindowsMessage.WM_POINTERUPDATE:
+                    {
+                        if (!_wmPointerEnabled)
+                        {
+                            break;
+                        }
+                        GetDevicePointerInfo(wParam, out var device, out var info, out var point, out var modifiers, ref timestamp);
+                        var eventType = GetEventType(message, info);
+
+                        var args = CreatePointerArgs(device, timestamp, eventType, point, modifiers, info.pointerId);
+                        args.IntermediatePoints = CreateLazyIntermediatePoints(info);
+                        e = args;
+                        break;
+                    }
+                case WindowsMessage.WM_POINTERDEVICEOUTOFRANGE:
+                case WindowsMessage.WM_POINTERLEAVE:
+                case WindowsMessage.WM_POINTERCAPTURECHANGED:
+                    {
+                        if (!_wmPointerEnabled)
+                        {
+                            break;
+                        }
+                        GetDevicePointerInfo(wParam, out var device, out var info, out var point, out var modifiers, ref timestamp);
+                        var eventType = device is TouchDevice ? RawPointerEventType.TouchCancel : RawPointerEventType.LeaveWindow;
+                        e = CreatePointerArgs(device, timestamp, eventType, point, modifiers, info.pointerId);
+                        break;
+                    }
+                case WindowsMessage.WM_POINTERWHEEL:
+                case WindowsMessage.WM_POINTERHWHEEL:
+                    {
+                        if (!_wmPointerEnabled)
+                        {
+                            break;
+                        }
+                        GetDevicePointerInfo(wParam, out var device, out var info, out var point, out var modifiers, ref timestamp);
+
+                        var val = (ToInt32(wParam) >> 16) / wheelDelta;
+                        var delta = message == WindowsMessage.WM_POINTERWHEEL ? new Vector(0, val) : new Vector(val, 0);
+                        e = new RawMouseWheelEventArgs(device, timestamp, _owner, point.Position, delta, modifiers)
+                        {
+                            RawPointerId = info.pointerId
+                        };
+                        break;
+                    }
+                case WindowsMessage.WM_POINTERDEVICEINRANGE:
+                    {
+                        if (!_wmPointerEnabled)
+                        {
+                            break;
+                        }
+
+                        // Do not generate events, but release mouse capture on any other device input.
+                        GetDevicePointerInfo(wParam, out var device, out var info, out var point, out var modifiers, ref timestamp);
+                        if (device != _mouseDevice)
+                        {
+                            _mouseDevice.Capture(null);
+                            return IntPtr.Zero;
+                        }
+                        break;
+                    }
+                case WindowsMessage.WM_POINTERACTIVATE:
+                    {
+                        //occurs when a pointer activates an inactive window.
+                        //we should handle this and return PA_ACTIVATE or PA_NOACTIVATE
+                        //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-pointeractivate
+                        break;
+                    }
+                case WindowsMessage.WM_POINTERDEVICECHANGE:
+                    {
+                        //notifies about changes in the settings of a monitor that has a digitizer attached to it.
+                        //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-pointerdevicechange
+                        break;
+                    }
+                case WindowsMessage.WM_NCPOINTERUPDATE:
+                    {
+                        //NC stands for non-client area - window header and window border
+                        //As I found above in an old message handling - we dont need to handle NC pointer move/updates.
+                        //All we need is pointer down and up. So this is skipped for now.
+                        break;
+                    }
+                case WindowsMessage.WM_POINTERENTER:
+                    {
+                        //this is not handled by WM_MOUSEENTER so I think there is no need to handle this too.
+                        //but we can detect a new pointer by this message and calling IS_POINTER_NEW_WPARAM
+
+                        //note: by using a pen there can be a pointer leave or enter inside a window coords
+                        //when you are just lift up the pen above the display
+                        break;
+                    }
+                case WindowsMessage.DM_POINTERHITTEST:
+                    {
+                        //DM stands for direct manipulation.
+                        //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/directmanipulation/direct-manipulation-portal
+                        break;
+                    }
+                case WindowsMessage.WM_TOUCHHITTESTING:
+                    {
+                        //This is to determine the most probable touch target.
+                        //provides an input bounding box and receives hit proximity
+                        //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-touchhittesting
+                        //https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-touch_hit_testing_input
+                        break;
+                    }
+                case WindowsMessage.WM_PARENTNOTIFY:
+                    {
+                        //This message is sent in a dialog scenarios. Contains mouse position.
+                        //Old message, but listed in the wm_pointer reference
+                        //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/inputmsg/wm-parentnotify
                         break;
                     }
                 case WindowsMessage.WM_NCPAINT:
@@ -446,7 +602,7 @@ namespace Avalonia.Win32
                 case WindowsMessage.WM_GETMINMAXINFO:
                     {
                         MINMAXINFO mmi = Marshal.PtrToStructure<MINMAXINFO>(lParam);
-                        
+
                         _maxTrackSize = mmi.ptMaxTrackSize;
 
                         if (_minSize.Width > 0)
@@ -530,7 +686,7 @@ namespace Avalonia.Win32
             if (_managedDrag.PreprocessInputEvent(ref e))
                 return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam);
 #endif
-            
+
             if(shouldTakeFocus)
             {
                 SetFocus(_hwnd);
@@ -540,7 +696,7 @@ namespace Avalonia.Win32
             {
                 Input(e);
 
-                if ((WindowsMessage)msg == WindowsMessage.WM_KEYDOWN)
+                if (message == WindowsMessage.WM_KEYDOWN)
                 {
                     // Handling a WM_KEYDOWN message should cause the subsequent WM_CHAR message to
                     // be ignored. This should be safe to do as WM_CHAR should only be produced in
@@ -549,6 +705,11 @@ namespace Avalonia.Win32
                     _ignoreWmChar = e.Handled;
                 }
 
+                if (s_intermediatePointsPooledList.Count > 0)
+                {
+                    s_intermediatePointsPooledList.Dispose();
+                }
+
                 if (e.Handled)
                 {
                     return IntPtr.Zero;
@@ -561,6 +722,196 @@ namespace Avalonia.Win32
             }
         }
 
+        private unsafe Lazy<IReadOnlyList<RawPointerPoint>> CreateLazyIntermediatePoints(POINTER_INFO info)
+        {
+            var historyCount = Math.Min((int)info.historyCount, MaxPointerHistorySize);
+            if (historyCount > 1)
+            {
+                return new Lazy<IReadOnlyList<RawPointerPoint>>(() =>
+                {
+                    s_intermediatePointsPooledList.Clear();
+                    s_intermediatePointsPooledList.Capacity = historyCount;
+
+                    // Pointers in history are ordered from newest to oldest, so we need to reverse iteration.
+                    // Also we skip the newest pointer, because original event arguments already contains it.
+
+                    if (info.pointerType == PointerInputType.PT_TOUCH)
+                    {
+                        if (GetPointerTouchInfoHistory(info.pointerId, ref historyCount, s_historyTouchInfos))
+                        {
+                            for (int i = historyCount - 1; i >= 1; i--)
+                            {
+                                var historyTouchInfo = s_historyTouchInfos[i];
+                                s_intermediatePointsPooledList.Add(CreateRawPointerPoint(historyTouchInfo));
+                            }
+                        }
+                    }
+                    else if (info.pointerType == PointerInputType.PT_PEN)
+                    {
+                        if (GetPointerPenInfoHistory(info.pointerId, ref historyCount, s_historyPenInfos))
+                        {
+                            for (int i = historyCount - 1; i >= 1; i--)
+                            {
+                                var historyPenInfo = s_historyPenInfos[i];
+                                s_intermediatePointsPooledList.Add(CreateRawPointerPoint(historyPenInfo));
+                            }
+                        }
+                    }
+                    else
+                    {
+                        // Currently Windows does not return history info for mouse input, but we handle it just for case.
+                        if (GetPointerInfoHistory(info.pointerId, ref historyCount, s_historyInfos))
+                        {
+                            for (int i = historyCount - 1; i >= 1; i--)
+                            {
+                                var historyInfo = s_historyInfos[i];
+                                s_intermediatePointsPooledList.Add(CreateRawPointerPoint(historyInfo));
+                            }
+                        }
+                    }
+                    return s_intermediatePointsPooledList;
+                });
+            }
+
+            return null;
+        }
+
+        private RawPointerEventArgs CreatePointerArgs(IInputDevice device, ulong timestamp, RawPointerEventType eventType, RawPointerPoint point, RawInputModifiers modifiers, uint rawPointerId)
+        {
+            return device is TouchDevice
+                ? new RawTouchEventArgs(device, timestamp, _owner, eventType, point, modifiers, rawPointerId)
+                : new RawPointerEventArgs(device, timestamp, _owner, eventType, point, modifiers)
+                {
+                    RawPointerId = rawPointerId
+                };
+        }
+
+        private void GetDevicePointerInfo(IntPtr wParam,
+            out IPointerDevice device, out POINTER_INFO info, out RawPointerPoint point,
+            out RawInputModifiers modifiers, ref uint timestamp)
+        {
+            var pointerId = (uint)(ToInt32(wParam) & 0xFFFF);
+            GetPointerType(pointerId, out var type);
+
+            modifiers = default;
+
+            switch (type)
+            {
+                case PointerInputType.PT_PEN:
+                    device = _penDevice;
+                    GetPointerPenInfo(pointerId, out var penInfo);
+                    info = penInfo.pointerInfo;
+                    point = CreateRawPointerPoint(penInfo);
+                    if (penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_BARREL))
+                    {
+                        modifiers |= RawInputModifiers.PenBarrelButton;
+                    }
+                    if (penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_ERASER))
+                    {
+                        modifiers |= RawInputModifiers.PenEraser;
+                    }
+                    if (penInfo.penFlags.HasFlag(PenFlags.PEN_FLAGS_INVERTED))
+                    {
+                        modifiers |= RawInputModifiers.PenInverted;
+                    }
+                    break;
+                case PointerInputType.PT_TOUCH:
+                    device = _touchDevice;
+                    GetPointerTouchInfo(pointerId, out var touchInfo);
+                    info = touchInfo.pointerInfo;
+                    point = CreateRawPointerPoint(touchInfo);
+                    break;
+                default:
+                    device = _mouseDevice;
+                    GetPointerInfo(pointerId, out info);
+                    point = CreateRawPointerPoint(info);
+                    break;
+            }
+
+            if (info.dwTime != 0)
+            {
+                timestamp = info.dwTime;
+            }
+
+            modifiers |= GetInputModifiers(info.pointerFlags);
+        }
+
+        private RawPointerPoint CreateRawPointerPoint(POINTER_INFO pointerInfo)
+        {
+            var point = PointToClient(new PixelPoint(pointerInfo.ptPixelLocationX, pointerInfo.ptPixelLocationY));
+            return new RawPointerPoint
+            {
+                Position = point
+            };
+        }
+        private RawPointerPoint CreateRawPointerPoint(POINTER_TOUCH_INFO info)
+        {
+            var pointerInfo = info.pointerInfo;
+            var point = PointToClient(new PixelPoint(pointerInfo.ptPixelLocationX, pointerInfo.ptPixelLocationY));
+            return new RawPointerPoint
+            {
+                Position = point,
+                // POINTER_PEN_INFO.pressure is normalized to a range between 0 and 1024, with 512 as a default.
+                // But in our API we use range from 0.0 to 1.0.
+                Pressure = info.pressure / 1024f
+            };
+        }
+        private RawPointerPoint CreateRawPointerPoint(POINTER_PEN_INFO info)
+        {
+            var pointerInfo = info.pointerInfo;
+            var point = PointToClient(new PixelPoint(pointerInfo.ptPixelLocationX, pointerInfo.ptPixelLocationY));
+            return new RawPointerPoint
+            {
+                Position = point,
+                // POINTER_PEN_INFO.pressure is normalized to a range between 0 and 1024, with 512 as a default.
+                // But in our API we use range from 0.0 to 1.0.
+                Pressure = info.pressure / 1024f,
+                Twist = info.rotation,
+                XTilt = info.tiltX,
+                YTilt = info.tiltY
+            };
+        }
+
+        private static RawPointerEventType GetEventType(WindowsMessage message, POINTER_INFO info)
+        {
+            var isTouch = info.pointerType == PointerInputType.PT_TOUCH;
+            if (info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CANCELED))
+            {
+                return isTouch ? RawPointerEventType.TouchCancel : RawPointerEventType.LeaveWindow;
+            }
+
+            var eventType = ToEventType(info.ButtonChangeType, isTouch);
+            if (eventType == RawPointerEventType.LeftButtonDown &&
+                message == WindowsMessage.WM_NCPOINTERDOWN)
+            {
+                eventType = RawPointerEventType.NonClientLeftButtonDown;
+            }
+
+            return eventType;
+        }
+
+        private static RawPointerEventType ToEventType(PointerButtonChangeType type, bool isTouch)
+        {
+            return type switch
+            {
+                PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_DOWN when isTouch => RawPointerEventType.TouchBegin,
+                PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_DOWN when !isTouch => RawPointerEventType.LeftButtonDown,
+                PointerButtonChangeType.POINTER_CHANGE_SECONDBUTTON_DOWN => RawPointerEventType.RightButtonDown,
+                PointerButtonChangeType.POINTER_CHANGE_THIRDBUTTON_DOWN => RawPointerEventType.MiddleButtonDown,
+                PointerButtonChangeType.POINTER_CHANGE_FOURTHBUTTON_DOWN => RawPointerEventType.XButton1Down,
+                PointerButtonChangeType.POINTER_CHANGE_FIFTHBUTTON_DOWN => RawPointerEventType.XButton2Down,
+
+                PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_UP when isTouch => RawPointerEventType.TouchEnd,
+                PointerButtonChangeType.POINTER_CHANGE_FIRSTBUTTON_UP when !isTouch => RawPointerEventType.LeftButtonUp,
+                PointerButtonChangeType.POINTER_CHANGE_SECONDBUTTON_UP => RawPointerEventType.RightButtonUp,
+                PointerButtonChangeType.POINTER_CHANGE_THIRDBUTTON_UP => RawPointerEventType.MiddleButtonUp,
+                PointerButtonChangeType.POINTER_CHANGE_FOURTHBUTTON_UP => RawPointerEventType.XButton1Up,
+                PointerButtonChangeType.POINTER_CHANGE_FIFTHBUTTON_UP => RawPointerEventType.XButton2Up,
+                _ when isTouch => RawPointerEventType.TouchUpdate,
+                _ => RawPointerEventType.Move
+            };
+        }
+
         private void UpdateInputMethod(IntPtr hkl)
         {
             // note: for non-ime language, also create it so that emoji panel tracks cursor
@@ -568,11 +919,11 @@ namespace Avalonia.Win32
             if (langid == _langid && Imm32InputMethod.Current.HWND == Hwnd)
             {
                 return;
-            } 
+            }
             _langid = langid;
 
             Imm32InputMethod.Current.SetLanguageAndWindow(this, Hwnd, hkl);
-            
+
         }
 
         private static int ToInt32(IntPtr ptr)
@@ -597,10 +948,7 @@ namespace Avalonia.Win32
 
         private bool ShouldIgnoreTouchEmulatedMessage()
         {
-            if (!_multitouch)
-            {
-                return false;
-            }
+            // Note: GetMessageExtraInfo doesn't work with WM_POINTER events.
 
             // MI_WP_SIGNATURE
             // https://docs.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages
@@ -613,6 +961,11 @@ namespace Avalonia.Win32
         private static RawInputModifiers GetMouseModifiers(IntPtr wParam)
         {
             var keys = (ModifierKeys)ToInt32(wParam);
+            return GetInputModifiers(keys);
+        }
+
+        private static RawInputModifiers GetInputModifiers(ModifierKeys keys)
+        {
             var modifiers = WindowsKeyboardDevice.Instance.Modifiers;
 
             if (keys.HasAllFlags(ModifierKeys.MK_LBUTTON))
@@ -642,5 +995,37 @@ namespace Avalonia.Win32
 
             return modifiers;
         }
+
+        private static RawInputModifiers GetInputModifiers(PointerFlags flags)
+        {
+            var modifiers = WindowsKeyboardDevice.Instance.Modifiers;
+
+            if (flags.HasAllFlags(PointerFlags.POINTER_FLAG_FIRSTBUTTON))
+            {
+                modifiers |= RawInputModifiers.LeftMouseButton;
+            }
+
+            if (flags.HasAllFlags(PointerFlags.POINTER_FLAG_SECONDBUTTON))
+            {
+                modifiers |= RawInputModifiers.RightMouseButton;
+            }
+
+            if (flags.HasAllFlags(PointerFlags.POINTER_FLAG_THIRDBUTTON))
+            {
+                modifiers |= RawInputModifiers.MiddleMouseButton;
+            }
+
+            if (flags.HasAllFlags(PointerFlags.POINTER_FLAG_FOURTHBUTTON))
+            {
+                modifiers |= RawInputModifiers.XButton1MouseButton;
+            }
+
+            if (flags.HasAllFlags(PointerFlags.POINTER_FLAG_FIFTHBUTTON))
+            {
+                modifiers |= RawInputModifiers.XButton2MouseButton;
+            }
+
+            return modifiers;
+        }
     }
 }

+ 16 - 8
src/Windows/Avalonia.Win32/WindowImpl.cs

@@ -22,6 +22,7 @@ using Avalonia.Win32.OpenGl;
 using Avalonia.Win32.WinRT;
 using Avalonia.Win32.WinRT.Composition;
 using static Avalonia.Win32.Interop.UnmanagedMethods;
+using Avalonia.Collections.Pooled;
 using Avalonia.Metadata;
 
 namespace Avalonia.Win32
@@ -69,18 +70,19 @@ namespace Avalonia.Win32
         private const WindowStyles WindowStateMask = (WindowStyles.WS_MAXIMIZE | WindowStyles.WS_MINIMIZE);
         private readonly TouchDevice _touchDevice;
         private readonly MouseDevice _mouseDevice;
+        private readonly PenDevice _penDevice;
         private readonly ManagedDeferredRendererLock _rendererLock;
         private readonly FramebufferManager _framebuffer;
         private readonly IGlPlatformSurface _gl;
+        private readonly bool _wmPointerEnabled;
 
         private Win32NativeControlHost _nativeControlHost;
         private WndProc _wndProcDelegate;
         private string _className;
         private IntPtr _hwnd;
-        private bool _multitouch;
         private IInputRoot _owner;
         private WindowProperties _windowProperties;
-        private bool _trackingMouse;
+        private bool _trackingMouse;//ToDo - there is something missed. Needs investigation @Steven Kirk
         private bool _topmost;
         private double _scaling = 1;
         private WindowState _showWindowState;
@@ -97,10 +99,17 @@ namespace Avalonia.Win32
         private uint _langid;
         private bool _ignoreWmChar;
 
+        private const int MaxPointerHistorySize = 512;
+        private static readonly PooledList<RawPointerPoint> s_intermediatePointsPooledList = new();
+        private static readonly POINTER_TOUCH_INFO[] s_historyTouchInfos = new POINTER_TOUCH_INFO[MaxPointerHistorySize];
+        private static readonly POINTER_PEN_INFO[] s_historyPenInfos = new POINTER_PEN_INFO[MaxPointerHistorySize];
+        private static readonly POINTER_INFO[] s_historyInfos = new POINTER_INFO[MaxPointerHistorySize];
+
         public WindowImpl()
         {
             _touchDevice = new TouchDevice();
             _mouseDevice = new WindowsMouseDevice();
+            _penDevice = new PenDevice();
 
 #if USE_MANAGED_DRAG
             _managedDrag = new ManagedWindowResizeDragHelper(this, capture =>
@@ -129,6 +138,8 @@ namespace Avalonia.Win32
                     egl.Display is AngleWin32EglDisplay angleDisplay &&
                     angleDisplay.PlatformApi == AngleOptions.PlatformApi.DirectX11;
 
+            _wmPointerEnabled = Win32Platform.WindowsVersion >= PlatformConstants.Windows8;
+
             CreateWindow();
             _framebuffer = new FramebufferManager(_hwnd);
             UpdateInputMethod(GetKeyboardLayout(0));
@@ -283,6 +294,8 @@ namespace Avalonia.Win32
 
         protected IntPtr Hwnd => _hwnd;
 
+        private bool IsMouseInPointerEnabled => _wmPointerEnabled && IsMouseInPointerEnabled();
+
         public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)
         {
             TransparencyLevel = EnableBlur(transparencyLevel);
@@ -815,12 +828,7 @@ namespace Avalonia.Win32
 
             Handle = new WindowImplPlatformHandle(this);
 
-            _multitouch = Win32Platform.Options.EnableMultitouch ?? true;
-
-            if (_multitouch)
-            {
-                RegisterTouchWindow(_hwnd, 0);
-            }
+            RegisterTouchWindow(_hwnd, 0);
 
             if (ShCoreAvailable && Win32Platform.WindowsVersion > PlatformConstants.Windows8)
             {

+ 15 - 9
tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs

@@ -219,30 +219,36 @@ namespace Avalonia.Input.UnitTests
         {
             for (int i = 0; i < touchPointIds.Length; i++)
             {
-                inputManager.ProcessInput(new RawTouchEventArgs(device, 0,
+                inputManager.ProcessInput(new RawPointerEventArgs(device, 0,
                                                               root,
                                                               type,
                                                               new Point(0, 0),
-                                                              RawInputModifiers.None,
-                                                              touchPointIds[i]));
+                                                              RawInputModifiers.None)
+                {
+                    RawPointerId = touchPointIds[i]
+                });
             }
         }
 
 
         private static void TapOnce(IInputManager inputManager, TouchDevice device, IInputRoot root, ulong timestamp = 0, long touchPointId = 0)
         {
-            inputManager.ProcessInput(new RawTouchEventArgs(device, timestamp,
+            inputManager.ProcessInput(new RawPointerEventArgs(device, timestamp,
                                                root,
                                                RawPointerEventType.TouchBegin,
                                                new Point(0, 0),
-                                               RawInputModifiers.None,
-                                               touchPointId));
-            inputManager.ProcessInput(new RawTouchEventArgs(device, timestamp,
+                                               RawInputModifiers.None)
+            {
+                RawPointerId = touchPointId
+            });
+            inputManager.ProcessInput(new RawPointerEventArgs(device, timestamp,
                                                 root,
                                                 RawPointerEventType.TouchEnd,
                                                 new Point(0, 0),
-                                                RawInputModifiers.None,
-                                                touchPointId));
+                                                RawInputModifiers.None)
+            {
+                RawPointerId = touchPointId
+            });
         }
     }
 }