Browse Source

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

pull/8427/head
Max Katz 4 years ago
committed by GitHub
parent
commit
11b8c8f531
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .editorconfig
  2. 1
      samples/ControlCatalog.NetCore/Program.cs
  3. 2
      samples/ControlCatalog/MainView.xaml
  4. 235
      samples/ControlCatalog/Pages/PointerCanvas.cs
  5. 109
      samples/ControlCatalog/Pages/PointerContactsTab.cs
  6. 322
      samples/ControlCatalog/Pages/PointersPage.cs
  7. 66
      samples/ControlCatalog/Pages/PointersPage.xaml
  8. 78
      samples/ControlCatalog/Pages/PointersPage.xaml.cs
  9. 5
      src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
  10. 7
      src/Avalonia.Base/Input/IKeyboardDevice.cs
  11. 10
      src/Avalonia.Base/Input/IPenDevice.cs
  12. 43
      src/Avalonia.Base/Input/IPointer.cs
  13. 174
      src/Avalonia.Base/Input/PenDevice.cs
  14. 22
      src/Avalonia.Base/Input/PointerEventArgs.cs
  15. 125
      src/Avalonia.Base/Input/PointerPoint.cs
  16. 19
      src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
  17. 17
      src/Avalonia.Base/Input/Raw/RawTouchEventArgs.cs
  18. 13
      src/Avalonia.Base/Input/TouchDevice.cs
  19. 4
      src/Avalonia.Controls.DataGrid/DataGridCell.cs
  20. 28
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs
  21. 12
      src/Shared/RawEventGrouping.cs
  22. 217
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  23. 1
      src/Windows/Avalonia.Win32/Win32Platform.cs
  24. 429
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  25. 24
      src/Windows/Avalonia.Win32/WindowImpl.cs
  26. 24
      tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs

2
.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_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_between_query_expression_clauses = true csharp_new_line_between_query_expression_clauses = true
trim_trailing_whitespace = true # trim_trailing_whitespace = true
# Indentation preferences # Indentation preferences
csharp_indent_block_contents = true csharp_indent_block_contents = true

1
samples/ControlCatalog.NetCore/Program.cs

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

2
samples/ControlCatalog/MainView.xaml

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

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

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

5
src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs

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

7
src/Avalonia.Base/Input/IKeyboardDevice.cs

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

10
src/Avalonia.Base/Input/IPenDevice.cs

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

43
src/Avalonia.Base/Input/IPointer.cs

@ -2,20 +2,59 @@ using Avalonia.Metadata;
namespace Avalonia.Input 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] [NotClientImplementable]
public interface IPointer public interface IPointer
{ {
/// <summary>
/// Gets a unique identifier for the input pointer.
/// </summary>
int Id { get; } 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); 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; } IInputElement? Captured { get; }
/// <summary>
/// Gets the pointer device type.
/// </summary>
PointerType Type { get; } 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; } bool IsPrimary { get; }
} }
/// <summary>
/// Enumerates pointer device types.
/// </summary>
public enum PointerType public enum PointerType
{ {
Mouse, Mouse,
Touch Touch,
Pen
} }
} }

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

22
src/Avalonia.Base/Input/PointerEventArgs.cs

@ -67,7 +67,14 @@ namespace Avalonia.Input
public IPointer? TryGetPointer(RawPointerEventArgs ev) => _ev.Pointer; public IPointer? TryGetPointer(RawPointerEventArgs ev) => _ev.Pointer;
} }
/// <summary>
/// Gets specific pointer generated by input device.
/// </summary>
public IPointer Pointer { get; } public IPointer Pointer { get; }
/// <summary>
/// Gets the time when the input occurred.
/// </summary>
public ulong Timestamp { get; } public ulong Timestamp { get; }
private IPointerDevice? _device; private IPointerDevice? _device;
@ -91,7 +98,10 @@ namespace Avalonia.Input
return mods; 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; } public KeyModifiers KeyModifiers { get; }
private Point GetPosition(Point pt, IVisual? relativeTo) private Point GetPosition(Point pt, IVisual? relativeTo)
@ -102,7 +112,12 @@ namespace Avalonia.Input
return pt; return pt;
return pt * _rootVisual.TransformToVisual(relativeTo) ?? default; 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); public Point GetPosition(IVisual? relativeTo) => GetPosition(_rootVisualPosition, relativeTo);
[Obsolete("Use GetCurrentPoint")] [Obsolete("Use GetCurrentPoint")]
@ -130,7 +145,8 @@ namespace Avalonia.Input
for (var c = 0; c < previousPoints.Count; c++) for (var c = 0; c < previousPoints.Count; c++)
{ {
var pt = previousPoints[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); points[points.Length - 1] = GetCurrentPoint(relativeTo);

125
src/Avalonia.Base/Input/PointerPoint.cs

@ -1,5 +1,10 @@
using Avalonia.Input.Raw;
namespace Avalonia.Input 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 sealed class PointerPoint
{ {
public PointerPoint(IPointer pointer, Point position, PointerPointProperties properties) public PointerPoint(IPointer pointer, Point position, PointerPointProperties properties)
@ -8,25 +13,109 @@ namespace Avalonia.Input
Position = position; Position = position;
Properties = properties; Properties = properties;
} }
/// <summary>
/// Gets specific pointer generated by input device.
/// </summary>
public IPointer Pointer { get; } public IPointer Pointer { get; }
/// <summary>
/// Gets extended information about the input pointer.
/// </summary>
public PointerPointProperties Properties { get; } public PointerPointProperties Properties { get; }
/// <summary>
/// Gets the location of the pointer input in client coordinates.
/// </summary>
public Point Position { get; } public Point Position { get; }
} }
/// <summary>
/// Provides extended properties for a PointerPoint object.
/// </summary>
public sealed class PointerPointProperties 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; } 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; } 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; } 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; } 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; } 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; } public PointerUpdateKind PointerUpdateKind { get; }
private PointerPointProperties() private PointerPointProperties()
{ {
} }
public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind) public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind)
{ {
PointerUpdateKind = kind; PointerUpdateKind = kind;
@ -36,10 +125,13 @@ namespace Avalonia.Input
IsRightButtonPressed = modifiers.HasAllFlags(RawInputModifiers.RightMouseButton); IsRightButtonPressed = modifiers.HasAllFlags(RawInputModifiers.RightMouseButton);
IsXButton1Pressed = modifiers.HasAllFlags(RawInputModifiers.XButton1MouseButton); IsXButton1Pressed = modifiers.HasAllFlags(RawInputModifiers.XButton1MouseButton);
IsXButton2Pressed = modifiers.HasAllFlags(RawInputModifiers.XButton2MouseButton); 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, // The underlying input source might be reporting the previous state,
// so make sure that we reflect the current state // so make sure that we reflect the current state
if (kind == PointerUpdateKind.LeftButtonPressed) if (kind == PointerUpdateKind.LeftButtonPressed)
IsLeftButtonPressed = true; IsLeftButtonPressed = true;
if (kind == PointerUpdateKind.LeftButtonReleased) if (kind == PointerUpdateKind.LeftButtonReleased)
@ -62,6 +154,33 @@ namespace Avalonia.Input
IsXButton2Pressed = false; 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(); public static PointerPointProperties None { get; } = new PointerPointProperties();
} }

19
src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs

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

17
src/Avalonia.Base/Input/Raw/RawTouchEventArgs.cs

@ -1,15 +1,26 @@
using System;
namespace Avalonia.Input.Raw namespace Avalonia.Input.Raw
{ {
public class RawTouchEventArgs : RawPointerEventArgs public class RawTouchEventArgs : RawPointerEventArgs
{ {
public RawTouchEventArgs(IInputDevice device, ulong timestamp, IInputRoot root, public RawTouchEventArgs(IInputDevice device, ulong timestamp, IInputRoot root,
RawPointerEventType type, Point position, RawInputModifiers inputModifiers, RawPointerEventType type, Point position, RawInputModifiers inputModifiers,
long touchPointId) long rawPointerId)
: base(device, timestamp, root, type, position, inputModifiers) : 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; }
} }
} }

13
src/Avalonia.Base/Input/TouchDevice.cs

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

4
src/Avalonia.Controls.DataGrid/DataGridCell.cs

@ -178,9 +178,9 @@ namespace Avalonia.Controls
{ {
var handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled); 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. // 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; e.Handled = handled;
} }

28
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs

@ -30,20 +30,28 @@ namespace Avalonia.Diagnostics.ViewModels
if (control is AvaloniaObject ao) if (control is AvaloniaObject ao)
{ {
MarginThickness = ao.GetValue(Layoutable.MarginProperty); try
if (HasPadding)
{ {
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(); UpdateSize();

12
src/Shared/RawEventGrouping.cs

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

217
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -225,20 +225,17 @@ namespace Avalonia.Win32.Interop
[Flags] [Flags]
public enum ModifierKeys public enum ModifierKeys
{ {
MK_CONTROL = 0x0008, MK_NONE = 0x0000,
MK_LBUTTON = 0x0001, MK_LBUTTON = 0x0001,
MK_MBUTTON = 0x0010,
MK_RBUTTON = 0x0002, MK_RBUTTON = 0x0002,
MK_SHIFT = 0x0004, MK_SHIFT = 0x0004,
MK_CONTROL = 0x0008,
MK_ALT = 0x0020,
MK_MBUTTON = 0x0010,
MK_ALT = 0x0020,
MK_XBUTTON1 = 0x0020, MK_XBUTTON1 = 0x0020,
MK_XBUTTON2 = 0x0040 MK_XBUTTON2 = 0x0040
} }
@ -514,6 +511,33 @@ namespace Avalonia.Win32.Interop
CS_DROPSHADOW = 0x00020000 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 public enum WindowsMessage : uint
{ {
WM_NULL = 0x0000, WM_NULL = 0x0000,
@ -689,6 +713,25 @@ namespace Avalonia.Win32.Interop
WM_EXITSIZEMOVE = 0x0232, WM_EXITSIZEMOVE = 0x0232,
WM_DROPFILES = 0x0233, WM_DROPFILES = 0x0233,
WM_MDIREFRESHMENU = 0x0234, 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_SETCONTEXT = 0x0281,
WM_IME_NOTIFY = 0x0282, WM_IME_NOTIFY = 0x0282,
WM_IME_CONTROL = 0x0283, WM_IME_CONTROL = 0x0283,
@ -844,6 +887,134 @@ namespace Avalonia.Win32.Interop
SCF_ISSECURE = 0x00000001, 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)] [StructLayout(LayoutKind.Sequential)]
public struct RGBQUAD public struct RGBQUAD
{ {
@ -911,6 +1082,36 @@ namespace Avalonia.Win32.Interop
public const int SizeOf_BITMAPINFOHEADER = 40; 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")] [DllImport("user32.dll")]
public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip,
MonitorEnumDelegate lpfnEnum, IntPtr dwData); MonitorEnumDelegate lpfnEnum, IntPtr dwData);

1
src/Windows/Avalonia.Win32/Win32Platform.cs

@ -68,6 +68,7 @@ namespace Avalonia
/// <remarks> /// <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. /// 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> /// </remarks>
[Obsolete("Multitouch is always enabled on supported Windows versions")]
public bool? EnableMultitouch { get; set; } = true; public bool? EnableMultitouch { get; set; } = true;
/// <summary> /// <summary>

429
src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs

@ -1,4 +1,6 @@
using System; using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
@ -26,8 +28,8 @@ namespace Avalonia.Win32
uint timestamp = unchecked((uint)GetMessageTime()); uint timestamp = unchecked((uint)GetMessageTime());
RawInputEventArgs e = null; RawInputEventArgs e = null;
var shouldTakeFocus = false; var shouldTakeFocus = false;
var message = (WindowsMessage)msg;
switch ((WindowsMessage)msg) switch (message)
{ {
case WindowsMessage.WM_ACTIVATE: case WindowsMessage.WM_ACTIVATE:
{ {
@ -82,7 +84,7 @@ namespace Avalonia.Win32
case WindowsMessage.WM_DESTROY: case WindowsMessage.WM_DESTROY:
{ {
UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, IntPtr.Zero, IntPtr.Zero, null); UiaCoreProviderApi.UiaReturnRawElementProvider(_hwnd, IntPtr.Zero, IntPtr.Zero, null);
// We need to release IMM context and state to avoid leaks. // We need to release IMM context and state to avoid leaks.
if (Imm32InputMethod.Current.HWND == _hwnd) if (Imm32InputMethod.Current.HWND == _hwnd)
{ {
@ -108,9 +110,9 @@ namespace Avalonia.Win32
var newDisplayRect = Marshal.PtrToStructure<RECT>(lParam); var newDisplayRect = Marshal.PtrToStructure<RECT>(lParam);
_scaling = dpi / 96.0; _scaling = dpi / 96.0;
ScalingChanged?.Invoke(_scaling); ScalingChanged?.Invoke(_scaling);
using (SetResizeReason(PlatformResizeReason.DpiChange)) using (SetResizeReason(PlatformResizeReason.DpiChange))
{ {
SetWindowPos(hWnd, SetWindowPos(hWnd,
IntPtr.Zero, IntPtr.Zero,
newDisplayRect.left, newDisplayRect.left,
@ -178,6 +180,10 @@ namespace Avalonia.Win32
case WindowsMessage.WM_MBUTTONDOWN: case WindowsMessage.WM_MBUTTONDOWN:
case WindowsMessage.WM_XBUTTONDOWN: case WindowsMessage.WM_XBUTTONDOWN:
{ {
if (IsMouseInPointerEnabled)
{
break;
}
shouldTakeFocus = ShouldTakeFocusOnClick; shouldTakeFocus = ShouldTakeFocusOnClick;
if (ShouldIgnoreTouchEmulatedMessage()) if (ShouldIgnoreTouchEmulatedMessage())
{ {
@ -188,7 +194,7 @@ namespace Avalonia.Win32
_mouseDevice, _mouseDevice,
timestamp, timestamp,
_owner, _owner,
(WindowsMessage)msg switch message switch
{ {
WindowsMessage.WM_LBUTTONDOWN => RawPointerEventType.LeftButtonDown, WindowsMessage.WM_LBUTTONDOWN => RawPointerEventType.LeftButtonDown,
WindowsMessage.WM_RBUTTONDOWN => RawPointerEventType.RightButtonDown, WindowsMessage.WM_RBUTTONDOWN => RawPointerEventType.RightButtonDown,
@ -207,6 +213,10 @@ namespace Avalonia.Win32
case WindowsMessage.WM_MBUTTONUP: case WindowsMessage.WM_MBUTTONUP:
case WindowsMessage.WM_XBUTTONUP: case WindowsMessage.WM_XBUTTONUP:
{ {
if (IsMouseInPointerEnabled)
{
break;
}
if (ShouldIgnoreTouchEmulatedMessage()) if (ShouldIgnoreTouchEmulatedMessage())
{ {
break; break;
@ -216,7 +226,7 @@ namespace Avalonia.Win32
_mouseDevice, _mouseDevice,
timestamp, timestamp,
_owner, _owner,
(WindowsMessage)msg switch message switch
{ {
WindowsMessage.WM_LBUTTONUP => RawPointerEventType.LeftButtonUp, WindowsMessage.WM_LBUTTONUP => RawPointerEventType.LeftButtonUp,
WindowsMessage.WM_RBUTTONUP => RawPointerEventType.RightButtonUp, WindowsMessage.WM_RBUTTONUP => RawPointerEventType.RightButtonUp,
@ -231,11 +241,19 @@ namespace Avalonia.Win32
} }
// Mouse capture is lost // Mouse capture is lost
case WindowsMessage.WM_CANCELMODE: case WindowsMessage.WM_CANCELMODE:
_mouseDevice.Capture(null); if (!IsMouseInPointerEnabled)
{
_mouseDevice.Capture(null);
}
break; break;
case WindowsMessage.WM_MOUSEMOVE: case WindowsMessage.WM_MOUSEMOVE:
{ {
if (IsMouseInPointerEnabled)
{
break;
}
if (ShouldIgnoreTouchEmulatedMessage()) if (ShouldIgnoreTouchEmulatedMessage())
{ {
break; break;
@ -259,42 +277,58 @@ namespace Avalonia.Win32
timestamp, timestamp,
_owner, _owner,
RawPointerEventType.Move, RawPointerEventType.Move,
DipFromLParam(lParam), GetMouseModifiers(wParam)); DipFromLParam(lParam),
GetMouseModifiers(wParam));
break; break;
} }
case WindowsMessage.WM_MOUSEWHEEL: case WindowsMessage.WM_MOUSEWHEEL:
{ {
if (IsMouseInPointerEnabled)
{
break;
}
e = new RawMouseWheelEventArgs( e = new RawMouseWheelEventArgs(
_mouseDevice, _mouseDevice,
timestamp, timestamp,
_owner, _owner,
PointToClient(PointFromLParam(lParam)), PointToClient(PointFromLParam(lParam)),
new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta), GetMouseModifiers(wParam)); new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta),
GetMouseModifiers(wParam));
break; break;
} }
case WindowsMessage.WM_MOUSEHWHEEL: case WindowsMessage.WM_MOUSEHWHEEL:
{ {
if (IsMouseInPointerEnabled)
{
break;
}
e = new RawMouseWheelEventArgs( e = new RawMouseWheelEventArgs(
_mouseDevice, _mouseDevice,
timestamp, timestamp,
_owner, _owner,
PointToClient(PointFromLParam(lParam)), PointToClient(PointFromLParam(lParam)),
new Vector(-(ToInt32(wParam) >> 16) / wheelDelta, 0), GetMouseModifiers(wParam)); new Vector(-(ToInt32(wParam) >> 16) / wheelDelta, 0),
GetMouseModifiers(wParam));
break; break;
} }
case WindowsMessage.WM_MOUSELEAVE: case WindowsMessage.WM_MOUSELEAVE:
{ {
if (IsMouseInPointerEnabled)
{
break;
}
_trackingMouse = false; _trackingMouse = false;
e = new RawPointerEventArgs( e = new RawPointerEventArgs(
_mouseDevice, _mouseDevice,
timestamp, timestamp,
_owner, _owner,
RawPointerEventType.LeaveWindow, RawPointerEventType.LeaveWindow,
new Point(-1, -1), WindowsKeyboardDevice.Instance.Modifiers); new Point(-1, -1),
WindowsKeyboardDevice.Instance.Modifiers);
break; break;
} }
@ -303,11 +337,15 @@ namespace Avalonia.Win32
case WindowsMessage.WM_NCMBUTTONDOWN: case WindowsMessage.WM_NCMBUTTONDOWN:
case WindowsMessage.WM_NCXBUTTONDOWN: case WindowsMessage.WM_NCXBUTTONDOWN:
{ {
if (IsMouseInPointerEnabled)
{
break;
}
e = new RawPointerEventArgs( e = new RawPointerEventArgs(
_mouseDevice, _mouseDevice,
timestamp, timestamp,
_owner, _owner,
(WindowsMessage)msg switch message switch
{ {
WindowsMessage.WM_NCLBUTTONDOWN => RawPointerEventType WindowsMessage.WM_NCLBUTTONDOWN => RawPointerEventType
.NonClientLeftButtonDown, .NonClientLeftButtonDown,
@ -323,6 +361,10 @@ namespace Avalonia.Win32
} }
case WindowsMessage.WM_TOUCH: case WindowsMessage.WM_TOUCH:
{ {
if (_wmPointerEnabled)
{
break;
}
var touchInputCount = wParam.ToInt32(); var touchInputCount = wParam.ToInt32();
var pTouchInputs = stackalloc TOUCHINPUT[touchInputCount]; var pTouchInputs = stackalloc TOUCHINPUT[touchInputCount];
@ -348,6 +390,120 @@ namespace Avalonia.Win32
return IntPtr.Zero; 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; break;
} }
case WindowsMessage.WM_NCPAINT: case WindowsMessage.WM_NCPAINT:
@ -446,7 +602,7 @@ namespace Avalonia.Win32
case WindowsMessage.WM_GETMINMAXINFO: case WindowsMessage.WM_GETMINMAXINFO:
{ {
MINMAXINFO mmi = Marshal.PtrToStructure<MINMAXINFO>(lParam); MINMAXINFO mmi = Marshal.PtrToStructure<MINMAXINFO>(lParam);
_maxTrackSize = mmi.ptMaxTrackSize; _maxTrackSize = mmi.ptMaxTrackSize;
if (_minSize.Width > 0) if (_minSize.Width > 0)
@ -530,7 +686,7 @@ namespace Avalonia.Win32
if (_managedDrag.PreprocessInputEvent(ref e)) if (_managedDrag.PreprocessInputEvent(ref e))
return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam);
#endif #endif
if(shouldTakeFocus) if(shouldTakeFocus)
{ {
SetFocus(_hwnd); SetFocus(_hwnd);
@ -540,7 +696,7 @@ namespace Avalonia.Win32
{ {
Input(e); 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 // 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 // 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; _ignoreWmChar = e.Handled;
} }
if (s_intermediatePointsPooledList.Count > 0)
{
s_intermediatePointsPooledList.Dispose();
}
if (e.Handled) if (e.Handled)
{ {
return IntPtr.Zero; 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) private void UpdateInputMethod(IntPtr hkl)
{ {
// note: for non-ime language, also create it so that emoji panel tracks cursor // 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) if (langid == _langid && Imm32InputMethod.Current.HWND == Hwnd)
{ {
return; return;
} }
_langid = langid; _langid = langid;
Imm32InputMethod.Current.SetLanguageAndWindow(this, Hwnd, hkl); Imm32InputMethod.Current.SetLanguageAndWindow(this, Hwnd, hkl);
} }
private static int ToInt32(IntPtr ptr) private static int ToInt32(IntPtr ptr)
@ -597,10 +948,7 @@ namespace Avalonia.Win32
private bool ShouldIgnoreTouchEmulatedMessage() private bool ShouldIgnoreTouchEmulatedMessage()
{ {
if (!_multitouch) // Note: GetMessageExtraInfo doesn't work with WM_POINTER events.
{
return false;
}
// MI_WP_SIGNATURE // MI_WP_SIGNATURE
// https://docs.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages // 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) private static RawInputModifiers GetMouseModifiers(IntPtr wParam)
{ {
var keys = (ModifierKeys)ToInt32(wParam); var keys = (ModifierKeys)ToInt32(wParam);
return GetInputModifiers(keys);
}
private static RawInputModifiers GetInputModifiers(ModifierKeys keys)
{
var modifiers = WindowsKeyboardDevice.Instance.Modifiers; var modifiers = WindowsKeyboardDevice.Instance.Modifiers;
if (keys.HasAllFlags(ModifierKeys.MK_LBUTTON)) if (keys.HasAllFlags(ModifierKeys.MK_LBUTTON))
@ -642,5 +995,37 @@ namespace Avalonia.Win32
return modifiers; 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;
}
} }
} }

24
src/Windows/Avalonia.Win32/WindowImpl.cs

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

24
tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs

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

Loading…
Cancel
Save