diff --git a/.editorconfig b/.editorconfig
index cb589a5ce1..30edee1633 100644
--- a/.editorconfig
+++ b/.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
diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs
index fd080cfc5b..13751b56b5 100644
--- a/samples/ControlCatalog.NetCore/Program.cs
+++ b/samples/ControlCatalog.NetCore/Program.cs
@@ -115,7 +115,6 @@ namespace ControlCatalog.NetCore
})
.With(new Win32PlatformOptions
{
- EnableMultitouch = true
})
.UseSkia()
.AfterSetup(builder =>
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index d8dc3bad2d..7676de54a6 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -109,7 +109,7 @@
-
+
diff --git a/samples/ControlCatalog/Pages/PointerCanvas.cs b/samples/ControlCatalog/Pages/PointerCanvas.cs
new file mode 100644
index 0000000000..5843b13a0c
--- /dev/null
+++ b/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 _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 ThreadSleepProperty =
+ AvaloniaProperty.RegisterDirect(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 DrawOnlyPointsProperty =
+ AvaloniaProperty.RegisterDirect(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 StatusProperty =
+ AvaloniaProperty.RegisterDirect(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);
+ }
+}
diff --git a/samples/ControlCatalog/Pages/PointerContactsTab.cs b/samples/ControlCatalog/Pages/PointerContactsTab.cs
new file mode 100644
index 0000000000..b6aabebf99
--- /dev/null
+++ b/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 _pointers = new Dictionary();
+
+ 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);
+ }
+ }
+}
diff --git a/samples/ControlCatalog/Pages/PointersPage.cs b/samples/ControlCatalog/Pages/PointersPage.cs
deleted file mode 100644
index 0377993d2c..0000000000
--- a/samples/ControlCatalog/Pages/PointersPage.cs
+++ /dev/null
@@ -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 _pointers = new Dictionary();
-
- 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 _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);
- }
- }
-
- }
-}
diff --git a/samples/ControlCatalog/Pages/PointersPage.xaml b/samples/ControlCatalog/Pages/PointersPage.xaml
new file mode 100644
index 0000000000..c39106f29e
--- /dev/null
+++ b/samples/ControlCatalog/Pages/PointersPage.xaml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Capture 1
+
+
+ Capture 2
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/PointersPage.xaml.cs b/samples/ControlCatalog/Pages/PointersPage.xaml.cs
new file mode 100644
index 0000000000..6fc468e37f
--- /dev/null
+++ b/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("BorderCapture1");
+ var border2 = this.Get("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);
+ }
+}
diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
index f8cedb636f..889b7e3b82 100644
--- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
+++ b/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;
diff --git a/src/Avalonia.Base/Input/IKeyboardDevice.cs b/src/Avalonia.Base/Input/IKeyboardDevice.cs
index c8db8bf16f..80aebc02bc 100644
--- a/src/Avalonia.Base/Input/IKeyboardDevice.cs
+++ b/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]
diff --git a/src/Avalonia.Base/Input/IPenDevice.cs b/src/Avalonia.Base/Input/IPenDevice.cs
new file mode 100644
index 0000000000..1cc0fcf76d
--- /dev/null
+++ b/src/Avalonia.Base/Input/IPenDevice.cs
@@ -0,0 +1,10 @@
+namespace Avalonia.Input
+{
+ ///
+ /// Represents a pen/stylus device.
+ ///
+ public interface IPenDevice : IPointerDevice
+ {
+
+ }
+}
diff --git a/src/Avalonia.Base/Input/IPointer.cs b/src/Avalonia.Base/Input/IPointer.cs
index 66aeacadc9..52605bb6ae 100644
--- a/src/Avalonia.Base/Input/IPointer.cs
+++ b/src/Avalonia.Base/Input/IPointer.cs
@@ -2,20 +2,59 @@ using Avalonia.Metadata;
namespace Avalonia.Input
{
+ ///
+ /// Identifies specific pointer generated by input device.
+ ///
+ ///
+ /// Some devices, for instance, touchscreen might generate a pointer on each physical contact.
+ ///
[NotClientImplementable]
public interface IPointer
{
+ ///
+ /// Gets a unique identifier for the input pointer.
+ ///
int Id { get; }
+
+ ///
+ /// Captures pointer input to the specified control.
+ ///
+ /// The control.
+ ///
+ /// 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 property.
+ ///
void Capture(IInputElement? control);
+
+ ///
+ /// Gets the control that is currently capturing by the pointer, if any.
+ ///
+ ///
+ /// 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
+ /// method.
+ ///
IInputElement? Captured { get; }
+
+ ///
+ /// Gets the pointer device type.
+ ///
PointerType Type { get; }
+
+ ///
+ /// Gets a value that indicates whether the input is from the primary pointer when multiple pointers are registered.
+ ///
bool IsPrimary { get; }
-
}
+ ///
+ /// Enumerates pointer device types.
+ ///
public enum PointerType
{
Mouse,
- Touch
+ Touch,
+ Pen
}
}
diff --git a/src/Avalonia.Base/Input/PenDevice.cs b/src/Avalonia.Base/Input/PenDevice.cs
new file mode 100644
index 0000000000..d22b48562c
--- /dev/null
+++ b/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
+{
+ ///
+ /// Represents a pen/stylus device.
+ ///
+ public class PenDevice : IPenDevice, IDisposable
+ {
+ private readonly Dictionary _pointers = new();
+ private readonly Dictionary _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();
+ 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?>? 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;
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs
index 5495802920..058c2f9cc1 100644
--- a/src/Avalonia.Base/Input/PointerEventArgs.cs
+++ b/src/Avalonia.Base/Input/PointerEventArgs.cs
@@ -67,7 +67,14 @@ namespace Avalonia.Input
public IPointer? TryGetPointer(RawPointerEventArgs ev) => _ev.Pointer;
}
+ ///
+ /// Gets specific pointer generated by input device.
+ ///
public IPointer Pointer { get; }
+
+ ///
+ /// Gets the time when the input occurred.
+ ///
public ulong Timestamp { get; }
private IPointerDevice? _device;
@@ -91,7 +98,10 @@ namespace Avalonia.Input
return mods;
}
}
-
+
+ ///
+ /// Gets a value that indicates which key modifiers were active at the time that the pointer event was initiated.
+ ///
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;
}
-
+
+ ///
+ /// Gets the pointer position relative to a control.
+ ///
+ /// The control.
+ /// The pointer position in the control's coordinates.
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);
diff --git a/src/Avalonia.Base/Input/PointerPoint.cs b/src/Avalonia.Base/Input/PointerPoint.cs
index 9f8285a8e1..71145b5cb0 100644
--- a/src/Avalonia.Base/Input/PointerPoint.cs
+++ b/src/Avalonia.Base/Input/PointerPoint.cs
@@ -1,5 +1,10 @@
+using Avalonia.Input.Raw;
+
namespace Avalonia.Input
{
+ ///
+ /// Provides basic properties for the input pointer associated with a single mouse, pen/stylus, or touch contact.
+ ///
public sealed class PointerPoint
{
public PointerPoint(IPointer pointer, Point position, PointerPointProperties properties)
@@ -8,25 +13,109 @@ namespace Avalonia.Input
Position = position;
Properties = properties;
}
+
+ ///
+ /// Gets specific pointer generated by input device.
+ ///
public IPointer Pointer { get; }
+
+ ///
+ /// Gets extended information about the input pointer.
+ ///
public PointerPointProperties Properties { get; }
+
+ ///
+ /// Gets the location of the pointer input in client coordinates.
+ ///
public Point Position { get; }
}
+ ///
+ /// Provides extended properties for a PointerPoint object.
+ ///
public sealed class PointerPointProperties
{
+ ///
+ /// Gets a value that indicates whether the pointer input was triggered by the primary action mode of an input device.
+ ///
public bool IsLeftButtonPressed { get; }
+
+ ///
+ /// Gets a value that indicates whether the pointer input was triggered by the tertiary action mode of an input device.
+ ///
public bool IsMiddleButtonPressed { get; }
+
+ ///
+ /// Gets a value that indicates whether the pointer input was triggered by the secondary action mode (if supported) of an input device.
+ ///
public bool IsRightButtonPressed { get; }
+
+ ///
+ /// Gets a value that indicates whether the pointer input was triggered by the first extended mouse button (XButton1).
+ ///
public bool IsXButton1Pressed { get; }
+
+ ///
+ /// Gets a value that indicates whether the pointer input was triggered by the second extended mouse button (XButton2).
+ ///
public bool IsXButton2Pressed { get; }
+ ///
+ /// Gets a value that indicates whether the barrel button of the pen/stylus device is pressed.
+ ///
+ public bool IsBarrelButtonPressed { get; }
+
+ ///
+ /// Gets a value that indicates whether the input is from a pen eraser.
+ ///
+ public bool IsEraser { get; }
+
+ ///
+ /// Gets a value that indicates whether the digitizer pen is inverted.
+ ///
+ public bool IsInverted { get; }
+
+ ///
+ /// 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).
+ ///
+ ///
+ /// A value between 0.0 and 359.0 in degrees of rotation. The default value is 0.0.
+ ///
+ public float Twist { get; }
+
+ ///
+ /// Gets a value that indicates the force that the pointer device (typically a pen/stylus) exerts on the surface of the digitizer.
+ ///
+ ///
+ /// A value from 0 to 1.0. The default value is 0.5.
+ ///
+ public float Pressure { get; } = 0.5f;
+
+ ///
+ /// 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).
+ ///
+ ///
+ /// 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.
+ ///
+ public float XTilt { get; }
+
+ ///
+ /// 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).
+ ///
+ ///
+ /// 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.
+ ///
+ public float YTilt { get; }
+
+ ///
+ /// Gets the kind of pointer state change.
+ ///
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();
}
diff --git a/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
index 8b9d7c161d..0e4e0ed3e2 100644
--- a/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
+++ b/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
@@ -56,11 +56,12 @@ namespace Avalonia.Input.Raw
Contract.Requires(device != null);
Contract.Requires(root != null);
+ Point = new RawPointerPoint();
Position = position;
Type = type;
InputModifiers = inputModifiers;
}
-
+
///
/// Initializes a new instance of the class.
///
@@ -87,6 +88,11 @@ namespace Avalonia.Input.Raw
InputModifiers = inputModifiers;
}
+ ///
+ /// Gets the raw pointer identifier.
+ ///
+ public long RawPointerId { get; set; }
+
///
/// Gets the pointer properties and position, in client DIPs.
///
@@ -130,10 +136,17 @@ namespace Avalonia.Input.Raw
/// Pointer position, in client DIPs.
///
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;
}
}
}
diff --git a/src/Avalonia.Base/Input/Raw/RawTouchEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawTouchEventArgs.cs
index 020b40e55b..6706a45f48 100644
--- a/src/Avalonia.Base/Input/Raw/RawTouchEventArgs.cs
+++ b/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; }
}
}
diff --git a/src/Avalonia.Base/Input/TouchDevice.cs b/src/Avalonia.Base/Input/TouchDevice.cs
index 54dcc4051e..e914d860fd 100644
--- a/src/Avalonia.Base/Input/TouchDevice.cs
+++ b/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;
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs
index 2a51f45896..f9685b3682 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs
+++ b/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;
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs
index 0c0c005122..a453fef212 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs
+++ b/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();
diff --git a/src/Shared/RawEventGrouping.cs b/src/Shared/RawEventGrouping.cs
index 084593ffc6..966744888c 100644
--- a/src/Shared/RawEventGrouping.cs
+++ b/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 _eventCallback;
private readonly Queue _inputQueue = new();
private readonly Action _dispatchFromQueue;
- readonly Dictionary _lastTouchPoints = new();
+ readonly Dictionary _lastTouchPoints = new();
RawInputEventArgs? _lastEvent;
public RawEventGrouper(Action 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);
}
diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
index b1e4d8ca01..ea01d5cbdf 100644
--- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
+++ b/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);
diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs
index dc5e5324c4..8f6993c040 100644
--- a/src/Windows/Avalonia.Win32/Win32Platform.cs
+++ b/src/Windows/Avalonia.Win32/Win32Platform.cs
@@ -68,6 +68,7 @@ namespace Avalonia
///
/// 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.
///
+ [Obsolete("Multitouch is always enabled on supported Windows versions")]
public bool? EnableMultitouch { get; set; } = true;
///
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
index cae8834550..4c7b9a0348 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
+++ b/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(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(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> CreateLazyIntermediatePoints(POINTER_INFO info)
+ {
+ var historyCount = Math.Min((int)info.historyCount, MaxPointerHistorySize);
+ if (historyCount > 1)
+ {
+ return new Lazy>(() =>
+ {
+ 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;
+ }
}
}
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs
index a233dc9693..31f30a6e47 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/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 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)
{
diff --git a/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs b/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs
index 80c5a45c1a..7b7d547346 100644
--- a/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs
+++ b/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
+ });
}
}
}