committed by
GitHub
26 changed files with 1574 additions and 415 deletions
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
} |
|
||||
@ -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> |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
namespace Avalonia.Input |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents a pen/stylus device.
|
||||
|
/// </summary>
|
||||
|
public interface IPenDevice : IPointerDevice |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
} |
} |
||||
} |
} |
||||
|
|||||
Loading…
Reference in new issue