Browse Source

Merge branch 'master' into Stylus

Stylus
Max Katz 5 years ago
committed by GitHub
parent
commit
1bb16e6950
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 220
      samples/ControlCatalog/Pages/PointersPage.cs
  2. 7
      src/Avalonia.Base/Threading/Dispatcher.cs
  3. 15
      src/Avalonia.Base/Threading/JobRunner.cs
  4. 11
      src/Avalonia.Input/MouseDevice.cs
  5. 44
      src/Avalonia.Input/PointerEventArgs.cs
  6. 2
      src/Avalonia.Input/Raw/RawInputEventArgs.cs
  7. 9
      src/Avalonia.Input/Raw/RawPointerEventArgs.cs
  8. 2
      src/Avalonia.Input/TouchDevice.cs
  9. 1
      src/Avalonia.X11/Avalonia.X11.csproj
  10. 99
      src/Avalonia.X11/X11Window.cs
  11. 1
      src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj
  12. 38
      src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs
  13. 31
      src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs
  14. 43
      src/Linux/Avalonia.LinuxFramebuffer/Input/RawEventGroupingThreadingHelper.cs
  15. 129
      src/Shared/RawEventGrouping.cs
  16. 137
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs

220
samples/ControlCatalog/Pages/PointersPage.cs

@ -1,15 +1,37 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace ControlCatalog.Pages namespace ControlCatalog.Pages;
public class PointersPage : Decorator
{ {
public class PointersPage : Control 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 class PointerInfo
{ {
@ -45,7 +67,7 @@ namespace ControlCatalog.Pages
private Dictionary<IPointer, PointerInfo> _pointers = new Dictionary<IPointer, PointerInfo>(); private Dictionary<IPointer, PointerInfo> _pointers = new Dictionary<IPointer, PointerInfo>();
public PointersPage() public PointerContactsTab()
{ {
ClipToBounds = true; ClipToBounds = true;
} }
@ -104,4 +126,196 @@ namespace ControlCatalog.Pages
} }
} }
} }
public class PointerIntermediatePointsTab : Decorator
{
public PointerIntermediatePointsTab()
{
this[TextBlock.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);
}
}
}
} }

7
src/Avalonia.Base/Threading/Dispatcher.cs

@ -74,6 +74,13 @@ namespace Avalonia.Threading
/// </summary> /// </summary>
/// <param name="minimumPriority"></param> /// <param name="minimumPriority"></param>
public void RunJobs(DispatcherPriority minimumPriority) => _jobRunner.RunJobs(minimumPriority); public void RunJobs(DispatcherPriority minimumPriority) => _jobRunner.RunJobs(minimumPriority);
/// <summary>
/// Use this method to check if there are more prioritized tasks
/// </summary>
/// <param name="minimumPriority"></param>
public bool HasJobsWithPriority(DispatcherPriority minimumPriority) =>
_jobRunner.HasJobsWithPriority(minimumPriority);
/// <inheritdoc/> /// <inheritdoc/>
public Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal) public Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal)

15
src/Avalonia.Base/Threading/JobRunner.cs

@ -121,6 +121,21 @@ namespace Avalonia.Threading
return null; return null;
} }
public bool HasJobsWithPriority(DispatcherPriority minimumPriority)
{
for (int c = (int)minimumPriority; c < (int)DispatcherPriority.MaxValue; c++)
{
var q = _queues[c];
lock (q)
{
if (q.Count > 0)
return true;
}
}
return false;
}
private interface IJob private interface IJob
{ {
/// <summary> /// <summary>

11
src/Avalonia.Input/MouseDevice.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
@ -159,7 +160,7 @@ namespace Avalonia.Input
case RawPointerEventType.XButton1Down: case RawPointerEventType.XButton1Down:
case RawPointerEventType.XButton2Down: case RawPointerEventType.XButton2Down:
if (ButtonCount(props) > 1) if (ButtonCount(props) > 1)
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints);
else else
e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position,
props, keyModifiers); props, keyModifiers);
@ -170,12 +171,12 @@ namespace Avalonia.Input
case RawPointerEventType.XButton1Up: case RawPointerEventType.XButton1Up:
case RawPointerEventType.XButton2Up: case RawPointerEventType.XButton2Up:
if (ButtonCount(props) != 0) if (ButtonCount(props) != 0)
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints);
else else
e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers);
break; break;
case RawPointerEventType.Move: case RawPointerEventType.Move:
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints);
break; break;
case RawPointerEventType.Wheel: case RawPointerEventType.Wheel:
e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers); e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers);
@ -272,7 +273,7 @@ namespace Avalonia.Input
} }
private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties,
KeyModifiers inputModifiers) KeyModifiers inputModifiers, IReadOnlyList<Point>? intermediatePoints)
{ {
device = device ?? throw new ArgumentNullException(nameof(device)); device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root)); root = root ?? throw new ArgumentNullException(nameof(root));
@ -292,7 +293,7 @@ namespace Avalonia.Input
if (source is object) if (source is object)
{ {
var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root,
p, timestamp, properties, inputModifiers); p, timestamp, properties, inputModifiers, intermediatePoints);
source.RaiseEvent(e); source.RaiseEvent(e);
return e.Handled; return e.Handled;

44
src/Avalonia.Input/PointerEventArgs.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -10,6 +11,7 @@ namespace Avalonia.Input
private readonly IVisual? _rootVisual; private readonly IVisual? _rootVisual;
private readonly Point _rootVisualPosition; private readonly Point _rootVisualPosition;
private readonly PointerPointProperties _properties; private readonly PointerPointProperties _properties;
private readonly IReadOnlyList<Point>? _previousPoints;
public PointerEventArgs(RoutedEvent routedEvent, public PointerEventArgs(RoutedEvent routedEvent,
IInteractive? source, IInteractive? source,
@ -28,6 +30,20 @@ namespace Avalonia.Input
Timestamp = timestamp; Timestamp = timestamp;
KeyModifiers = modifiers; KeyModifiers = modifiers;
} }
public PointerEventArgs(RoutedEvent routedEvent,
IInteractive? source,
IPointer pointer,
IVisual? rootVisual, Point rootVisualPosition,
ulong timestamp,
PointerPointProperties properties,
KeyModifiers modifiers,
IReadOnlyList<Point>? previousPoints)
: this(routedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers)
{
_previousPoints = previousPoints;
}
class EmulatedDevice : IPointerDevice class EmulatedDevice : IPointerDevice
{ {
@ -76,14 +92,16 @@ namespace Avalonia.Input
public KeyModifiers KeyModifiers { get; } public KeyModifiers KeyModifiers { get; }
public Point GetPosition(IVisual? relativeTo) private Point GetPosition(Point pt, IVisual? relativeTo)
{ {
if (_rootVisual == null) if (_rootVisual == null)
return default; return default;
if (relativeTo == null) if (relativeTo == null)
return _rootVisualPosition; return pt;
return _rootVisualPosition * _rootVisual.TransformToVisual(relativeTo) ?? default; return pt * _rootVisual.TransformToVisual(relativeTo) ?? default;
} }
public Point GetPosition(IVisual? relativeTo) => GetPosition(_rootVisualPosition, relativeTo);
[Obsolete("Use GetCurrentPoint")] [Obsolete("Use GetCurrentPoint")]
public PointerPoint GetPointerPoint(IVisual? relativeTo) => GetCurrentPoint(relativeTo); public PointerPoint GetPointerPoint(IVisual? relativeTo) => GetCurrentPoint(relativeTo);
@ -96,6 +114,26 @@ namespace Avalonia.Input
public PointerPoint GetCurrentPoint(IVisual? relativeTo) public PointerPoint GetCurrentPoint(IVisual? relativeTo)
=> new PointerPoint(Pointer, GetPosition(relativeTo), _properties); => new PointerPoint(Pointer, GetPosition(relativeTo), _properties);
/// <summary>
/// Returns the PointerPoint associated with the current event
/// </summary>
/// <param name="relativeTo">The visual which coordinate system to use. Pass null for toplevel coordinate system</param>
/// <returns></returns>
public IReadOnlyList<PointerPoint> GetIntermediatePoints(IVisual? relativeTo)
{
if (_previousPoints == null || _previousPoints.Count == 0)
return new[] { GetCurrentPoint(relativeTo) };
var points = new PointerPoint[_previousPoints.Count + 1];
for (var c = 0; c < _previousPoints.Count; c++)
{
var pt = _previousPoints[c];
points[c] = new PointerPoint(Pointer, GetPosition(pt, relativeTo), _properties);
}
points[points.Length - 1] = GetCurrentPoint(relativeTo);
return points;
}
/// <summary> /// <summary>
/// Returns the current pointer point properties /// Returns the current pointer point properties
/// </summary> /// </summary>

2
src/Avalonia.Input/Raw/RawInputEventArgs.cs

@ -51,6 +51,6 @@ namespace Avalonia.Input.Raw
/// <summary> /// <summary>
/// Gets the timestamp associated with the event. /// Gets the timestamp associated with the event.
/// </summary> /// </summary>
public ulong Timestamp { get; private set; } public ulong Timestamp { get; set; }
} }
} }

9
src/Avalonia.Input/Raw/RawPointerEventArgs.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
namespace Avalonia.Input.Raw namespace Avalonia.Input.Raw
{ {
@ -71,6 +72,12 @@ namespace Avalonia.Input.Raw
/// <summary> /// <summary>
/// Gets the input modifiers. /// Gets the input modifiers.
/// </summary> /// </summary>
public RawInputModifiers InputModifiers { get; private set; } public RawInputModifiers InputModifiers { get; set; }
/// <summary>
/// Points that were traversed by a pointer since the previous relevant event,
/// only valid for Move and TouchUpdate
/// </summary>
public IReadOnlyList<Point>? IntermediatePoints { get; set; }
} }
} }

2
src/Avalonia.Input/TouchDevice.cs

@ -104,7 +104,7 @@ namespace Avalonia.Input
target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root, target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root,
args.Position, ev.Timestamp, args.Position, ev.Timestamp,
new PointerPointProperties(GetModifiers(args.InputModifiers, true), PointerUpdateKind.Other), new PointerPointProperties(GetModifiers(args.InputModifiers, true), PointerUpdateKind.Other),
GetKeyModifiers(args.InputModifiers))); GetKeyModifiers(args.InputModifiers), args.IntermediatePoints));
} }

1
src/Avalonia.X11/Avalonia.X11.csproj

@ -9,6 +9,7 @@
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" /> <ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" /> <ProjectReference Include="..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
<ProjectReference Include="..\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj" /> <ProjectReference Include="..\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj" />
<Compile Include="..\Shared\RawEventGrouping.cs" />
</ItemGroup> </ItemGroup>
</Project> </Project>

99
src/Avalonia.X11/X11Window.cs

@ -6,6 +6,7 @@ using System.Linq;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Threading;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Controls.Primitives.PopupPositioning;
@ -49,15 +50,7 @@ namespace Avalonia.X11
private double? _scalingOverride; private double? _scalingOverride;
private bool _disabled; private bool _disabled;
private TransparencyHelper _transparencyHelper; private TransparencyHelper _transparencyHelper;
private RawEventGrouper _rawEventGrouper;
public object SyncRoot { get; } = new object();
class InputEventContainer
{
public RawInputEventArgs Event;
}
private readonly Queue<InputEventContainer> _inputQueue = new Queue<InputEventContainer>();
private InputEventContainer _lastEvent;
private bool _useRenderWindow = false; private bool _useRenderWindow = false;
public X11Window(AvaloniaX11Platform platform, IWindowImpl popupParent) public X11Window(AvaloniaX11Platform platform, IWindowImpl popupParent)
{ {
@ -181,6 +174,8 @@ namespace Avalonia.X11
UpdateMotifHints(); UpdateMotifHints();
UpdateSizeHints(null); UpdateSizeHints(null);
_rawEventGrouper = new RawEventGrouper(e => Input?.Invoke(e));
_transparencyHelper = new TransparencyHelper(_x11, _handle, platform.Globals); _transparencyHelper = new TransparencyHelper(_x11, _handle, platform.Globals);
_transparencyHelper.SetTransparencyRequest(WindowTransparencyLevel.None); _transparencyHelper.SetTransparencyRequest(WindowTransparencyLevel.None);
@ -317,13 +312,8 @@ namespace Avalonia.X11
public double RenderScaling public double RenderScaling
{ {
get get => Interlocked.CompareExchange(ref _scaling, 0.0, 0.0);
{ private set => Interlocked.Exchange(ref _scaling, value);
lock (SyncRoot)
return _scaling;
}
private set => _scaling = value;
} }
public double DesktopScaling => RenderScaling; public double DesktopScaling => RenderScaling;
@ -378,11 +368,6 @@ namespace Avalonia.X11
} }
void OnEvent(ref XEvent ev) void OnEvent(ref XEvent ev)
{
lock (SyncRoot)
OnEventSync(ref ev);
}
void OnEventSync(ref XEvent ev)
{ {
if (ev.type == XEventName.MapNotify) if (ev.type == XEventName.MapNotify)
{ {
@ -544,32 +529,29 @@ namespace Avalonia.X11
private bool UpdateScaling(bool skipResize = false) private bool UpdateScaling(bool skipResize = false)
{ {
lock (SyncRoot) double newScaling;
if (_scalingOverride.HasValue)
newScaling = _scalingOverride.Value;
else
{ {
double newScaling; var monitor = _platform.X11Screens.Screens.OrderBy(x => x.PixelDensity)
if (_scalingOverride.HasValue) .FirstOrDefault(m => m.Bounds.Contains(Position));
newScaling = _scalingOverride.Value; newScaling = monitor?.PixelDensity ?? RenderScaling;
else }
{
var monitor = _platform.X11Screens.Screens.OrderBy(x => x.PixelDensity)
.FirstOrDefault(m => m.Bounds.Contains(Position));
newScaling = monitor?.PixelDensity ?? RenderScaling;
}
if (RenderScaling != newScaling)
{
var oldScaledSize = ClientSize;
RenderScaling = newScaling;
ScalingChanged?.Invoke(RenderScaling);
UpdateImePosition();
SetMinMaxSize(_scaledMinMaxSize.minSize, _scaledMinMaxSize.maxSize);
if(!skipResize)
Resize(oldScaledSize, true, PlatformResizeReason.DpiChange);
return true;
}
return false; if (RenderScaling != newScaling)
{
var oldScaledSize = ClientSize;
RenderScaling = newScaling;
ScalingChanged?.Invoke(RenderScaling);
UpdateImePosition();
SetMinMaxSize(_scaledMinMaxSize.minSize, _scaledMinMaxSize.maxSize);
if(!skipResize)
Resize(oldScaledSize, true, PlatformResizeReason.DpiChange);
return true;
} }
return false;
} }
private WindowState _lastWindowState; private WindowState _lastWindowState;
@ -735,33 +717,14 @@ namespace Avalonia.X11
if (args is RawDragEvent drag) if (args is RawDragEvent drag)
drag.Location = drag.Location / RenderScaling; drag.Location = drag.Location / RenderScaling;
_lastEvent = new InputEventContainer() {Event = args}; _rawEventGrouper.HandleEvent(args);
_inputQueue.Enqueue(_lastEvent);
if (_inputQueue.Count == 1)
{
Dispatcher.UIThread.Post(() =>
{
while (_inputQueue.Count > 0)
{
Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
var ev = _inputQueue.Dequeue();
Input?.Invoke(ev.Event);
}
}, DispatcherPriority.Input);
}
} }
void MouseEvent(RawPointerEventType type, ref XEvent ev, XModifierMask mods) void MouseEvent(RawPointerEventType type, ref XEvent ev, XModifierMask mods)
{ {
var mev = new RawPointerEventArgs( var mev = new RawPointerEventArgs(
_mouse, (ulong)ev.ButtonEvent.time.ToInt64(), _inputRoot, _mouse, (ulong)ev.ButtonEvent.time.ToInt64(), _inputRoot,
type, new Point(ev.ButtonEvent.x, ev.ButtonEvent.y), TranslateModifiers(mods)); type, new Point(ev.ButtonEvent.x, ev.ButtonEvent.y), TranslateModifiers(mods));
if(type == RawPointerEventType.Move && _inputQueue.Count>0 && _lastEvent.Event is RawPointerEventArgs ma)
if (ma.Type == RawPointerEventType.Move)
{
_lastEvent.Event = mev;
return;
}
ScheduleInput(mev, ref ev); ScheduleInput(mev, ref ev);
} }
@ -789,6 +752,12 @@ namespace Avalonia.X11
void Cleanup() void Cleanup()
{ {
if (_rawEventGrouper != null)
{
_rawEventGrouper.Dispose();
_rawEventGrouper = null;
}
if (_transparencyHelper != null) if (_transparencyHelper != null)
{ {
_transparencyHelper.Dispose(); _transparencyHelper.Dispose();

1
src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj

@ -7,5 +7,6 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" /> <ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" /> <ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
<Compile Include="..\..\Shared\RawEventGrouping.cs" />
</ItemGroup> </ItemGroup>
</Project> </Project>

38
src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs

@ -13,15 +13,16 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev
private readonly EvDevDeviceDescription[] _deviceDescriptions; private readonly EvDevDeviceDescription[] _deviceDescriptions;
private readonly List<EvDevDeviceHandler> _handlers = new List<EvDevDeviceHandler>(); private readonly List<EvDevDeviceHandler> _handlers = new List<EvDevDeviceHandler>();
private int _epoll; private int _epoll;
private Queue<RawInputEventArgs> _inputQueue = new Queue<RawInputEventArgs>();
private bool _isQueueHandlerTriggered; private bool _isQueueHandlerTriggered;
private object _lock = new object(); private object _lock = new object();
private Action<RawInputEventArgs> _onInput; private Action<RawInputEventArgs> _onInput;
private IInputRoot _inputRoot; private IInputRoot _inputRoot;
private RawEventGroupingThreadingHelper _inputQueue;
public EvDevBackend(EvDevDeviceDescription[] devices) public EvDevBackend(EvDevDeviceDescription[] devices)
{ {
_deviceDescriptions = devices; _deviceDescriptions = devices;
_inputQueue = new RawEventGroupingThreadingHelper(e => _onInput?.Invoke(e));
} }
unsafe void InputThread() unsafe void InputThread()
@ -49,42 +50,9 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev
private void OnRawEvent(RawInputEventArgs obj) private void OnRawEvent(RawInputEventArgs obj)
{ {
lock (_lock) _inputQueue.OnEvent(obj);
{
_inputQueue.Enqueue(obj);
TriggerQueueHandler();
}
} }
void TriggerQueueHandler()
{
if (_isQueueHandlerTriggered)
return;
_isQueueHandlerTriggered = true;
Dispatcher.UIThread.Post(InputQueueHandler, DispatcherPriority.Input);
}
void InputQueueHandler()
{
RawInputEventArgs ev;
lock (_lock)
{
_isQueueHandlerTriggered = false;
if(_inputQueue.Count == 0)
return;
ev = _inputQueue.Dequeue();
}
_onInput?.Invoke(ev);
lock (_lock)
{
if (_inputQueue.Count > 0)
TriggerQueueHandler();
}
}
public void Initialize(IScreenInfoProvider info, Action<RawInputEventArgs> onInput) public void Initialize(IScreenInfoProvider info, Action<RawInputEventArgs> onInput)
{ {

31
src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs

@ -17,15 +17,15 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput
private TouchDevice _touch = new TouchDevice(); private TouchDevice _touch = new TouchDevice();
private MouseDevice _mouse = new MouseDevice(); private MouseDevice _mouse = new MouseDevice();
private Point _mousePosition; private Point _mousePosition;
private readonly Queue<RawInputEventArgs> _inputQueue = new Queue<RawInputEventArgs>(); private readonly RawEventGroupingThreadingHelper _inputQueue;
private Action<RawInputEventArgs> _onInput; private Action<RawInputEventArgs> _onInput;
private Dictionary<int, Point> _pointers = new Dictionary<int, Point>(); private Dictionary<int, Point> _pointers = new Dictionary<int, Point>();
public LibInputBackend() public LibInputBackend()
{ {
var ctx = libinput_path_create_context(); var ctx = libinput_path_create_context();
_inputQueue = new(e => _onInput?.Invoke(e));
new Thread(()=>InputThread(ctx)).Start(); new Thread(()=>InputThread(ctx)).Start();
} }
@ -66,30 +66,7 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput
} }
} }
private void ScheduleInput(RawInputEventArgs ev) private void ScheduleInput(RawInputEventArgs ev) => _inputQueue.OnEvent(ev);
{
lock (_inputQueue)
{
_inputQueue.Enqueue(ev);
if (_inputQueue.Count == 1)
{
Dispatcher.UIThread.Post(() =>
{
while (true)
{
Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
RawInputEventArgs dequeuedEvent = null;
lock(_inputQueue)
if (_inputQueue.Count != 0)
dequeuedEvent = _inputQueue.Dequeue();
if (dequeuedEvent == null)
return;
_onInput?.Invoke(dequeuedEvent);
}
}, DispatcherPriority.Input);
}
}
}
private void HandleTouch(IntPtr ev, LibInputEventType type) private void HandleTouch(IntPtr ev, LibInputEventType type)
{ {

43
src/Linux/Avalonia.LinuxFramebuffer/Input/RawEventGroupingThreadingHelper.cs

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using Avalonia.Input.Raw;
using Avalonia.Threading;
namespace Avalonia.LinuxFramebuffer.Input;
internal class RawEventGroupingThreadingHelper : IDisposable
{
private readonly RawEventGrouper _grouper;
private readonly Queue<RawInputEventArgs> _rawQueue = new();
private readonly Action _queueHandler;
public RawEventGroupingThreadingHelper(Action<RawInputEventArgs> eventCallback)
{
_grouper = new RawEventGrouper(eventCallback);
_queueHandler = QueueHandler;
}
private void QueueHandler()
{
lock (_rawQueue)
{
while (_rawQueue.Count > 0)
_grouper.HandleEvent(_rawQueue.Dequeue());
}
}
public void OnEvent(RawInputEventArgs args)
{
lock (_rawQueue)
{
_rawQueue.Enqueue(args);
if (_rawQueue.Count == 1)
{
Dispatcher.UIThread.Post(_queueHandler, DispatcherPriority.Input);
}
}
}
public void Dispose() =>
Dispatcher.UIThread.Post(() => _grouper.Dispose(), DispatcherPriority.Input + 1);
}

129
src/Shared/RawEventGrouping.cs

@ -0,0 +1,129 @@
#nullable enable
using System;
using System.Collections.Generic;
using Avalonia.Collections.Pooled;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Threading;
using JetBrains.Annotations;
namespace Avalonia;
/*
This helper maintains an input queue for backends that handle input asynchronously.
While doing that it groups Move and TouchUpdate events so we could provide GetIntermediatePoints API
*/
internal class RawEventGrouper : IDisposable
{
private readonly Action<RawInputEventArgs> _eventCallback;
private readonly Queue<RawInputEventArgs> _inputQueue = new();
private readonly Action _dispatchFromQueue;
readonly Dictionary<long, RawTouchEventArgs> _lastTouchPoints = new();
RawInputEventArgs? _lastEvent;
public RawEventGrouper(Action<RawInputEventArgs> eventCallback)
{
_eventCallback = eventCallback;
_dispatchFromQueue = DispatchFromQueue;
}
private void AddToQueue(RawInputEventArgs args)
{
_lastEvent = args;
_inputQueue.Enqueue(args);
if (_inputQueue.Count == 1)
Dispatcher.UIThread.Post(_dispatchFromQueue, DispatcherPriority.Input);
}
private void DispatchFromQueue()
{
while (true)
{
if(_inputQueue.Count == 0)
return;
var ev = _inputQueue.Dequeue();
if (_lastEvent == ev)
_lastEvent = null;
if (ev is RawTouchEventArgs { Type: RawPointerEventType.TouchUpdate } touchUpdate)
_lastTouchPoints.Remove(touchUpdate.TouchPointId);
_eventCallback?.Invoke(ev);
if (ev is RawPointerEventArgs { IntermediatePoints: PooledList<Point> list })
list.Dispose();
if (Dispatcher.UIThread.HasJobsWithPriority(DispatcherPriority.Input + 1))
{
Dispatcher.UIThread.Post(_dispatchFromQueue, DispatcherPriority.Input);
return;
}
}
}
public void HandleEvent(RawInputEventArgs args)
{
/*
Try to update already enqueued events if
1) they are still not handled (_lastEvent and _lastTouchPoints shouldn't contain said event in that case)
2) previous event belongs to the same "event block", events in the same block:
- belong from the same device
- are pointer move events (Move/TouchUpdate)
- have the same type
- have same modifiers
Even if nothing is updated and the event is actually enqueued, we need to update the relevant tracking info
*/
if (
args is RawPointerEventArgs pointerEvent
&& _lastEvent != null
&& _lastEvent.Device == args.Device
&& _lastEvent is RawPointerEventArgs lastPointerEvent
&& lastPointerEvent.InputModifiers == pointerEvent.InputModifiers
&& lastPointerEvent.Type == pointerEvent.Type
&& lastPointerEvent.Type is RawPointerEventType.Move or RawPointerEventType.TouchUpdate)
{
if (args is RawTouchEventArgs touchEvent)
{
if (_lastTouchPoints.TryGetValue(touchEvent.TouchPointId, out var lastTouchEvent))
MergeEvents(lastTouchEvent, touchEvent);
else
{
_lastTouchPoints[touchEvent.TouchPointId] = touchEvent;
AddToQueue(touchEvent);
}
}
else
MergeEvents(lastPointerEvent, pointerEvent);
return;
}
else
{
_lastTouchPoints.Clear();
if (args is RawTouchEventArgs { Type: RawPointerEventType.TouchUpdate } touchEvent)
_lastTouchPoints[touchEvent.TouchPointId] = touchEvent;
}
AddToQueue(args);
}
private static void MergeEvents(RawPointerEventArgs last, RawPointerEventArgs current)
{
last.IntermediatePoints ??= new PooledList<Point>();
((PooledList<Point>)last.IntermediatePoints).Add(last.Position);
last.Position = current.Position;
last.Timestamp = current.Timestamp;
last.InputModifiers = current.InputModifiers;
}
public void Dispose()
{
_inputQueue.Clear();
_lastEvent = null;
_lastTouchPoints.Clear();
}
}

137
src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs

@ -26,8 +26,8 @@ namespace Avalonia.Web.Blazor
private InputHelperInterop _canvasHelper = null!; private InputHelperInterop _canvasHelper = null!;
private ElementReference _htmlCanvas; private ElementReference _htmlCanvas;
private ElementReference _inputElement; private ElementReference _inputElement;
private double _dpi; private double _dpi = 1;
private SKSize _canvasSize; private SKSize _canvasSize = new (100, 100);
private GRContext? _context; private GRContext? _context;
private GRGlInterface? _glInterface; private GRGlInterface? _glInterface;
@ -249,71 +249,55 @@ namespace Avalonia.Web.Blazor
[Parameter(CaptureUnmatchedValues = true)] [Parameter(CaptureUnmatchedValues = true)]
public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; } public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }
protected override void OnAfterRender(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if (firstRender) if (firstRender)
{ {
Threading.Dispatcher.UIThread.Post(async () => _inputHelper = await InputHelperInterop.ImportAsync(Js, _inputElement);
_canvasHelper = await InputHelperInterop.ImportAsync(Js, _htmlCanvas);
_inputHelper.Hide();
_canvasHelper.SetCursor("default");
_topLevelImpl.SetCssCursor = x =>
{
_inputHelper.SetCursor(x); //macOS
_canvasHelper.SetCursor(x); //windows
};
Console.WriteLine("starting html canvas setup");
_interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame);
Console.WriteLine("Interop created");
_jsGlInfo = _interop.InitGL();
Console.WriteLine("jsglinfo created - init gl");
// create the SkiaSharp context
if (_context == null)
{ {
_inputHelper = await InputHelperInterop.ImportAsync(Js, _inputElement); Console.WriteLine("create glcontext");
_canvasHelper = await InputHelperInterop.ImportAsync(Js, _htmlCanvas); _glInterface = GRGlInterface.Create();
_context = GRContext.CreateGl(_glInterface);
_inputHelper.Hide();
_canvasHelper.SetCursor("default"); var options = AvaloniaLocator.Current.GetService<SkiaOptions>();
_topLevelImpl.SetCssCursor = x => // bump the default resource cache limit
{ _context.SetResourceCacheLimit(options?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024);
_inputHelper.SetCursor(x);//macOS Console.WriteLine("glcontext created and resource limit set");
_canvasHelper.SetCursor(x);//windows }
};
_topLevelImpl.SetSurface(_context, _jsGlInfo, ColorType,
Console.WriteLine("starting html canvas setup"); new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi);
_interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame);
_initialised = true;
Console.WriteLine("Interop created");
_jsGlInfo = _interop.InitGL(); _topLevel.Prepare();
Console.WriteLine("jsglinfo created - init gl"); _topLevel.Renderer.Start();
_sizeWatcher = await SizeWatcherInterop.ImportAsync(Js, _htmlCanvas, OnSizeChanged); Invalidate();
_dpiWatcher = await DpiWatcherInterop.ImportAsync(Js, OnDpiChanged);
_sizeWatcher = await SizeWatcherInterop.ImportAsync(Js, _htmlCanvas, OnSizeChanged);
Console.WriteLine("watchers created."); _dpiWatcher = await DpiWatcherInterop.ImportAsync(Js, OnDpiChanged);
// create the SkiaSharp context
if (_context == null)
{
Console.WriteLine("create glcontext");
_glInterface = GRGlInterface.Create();
_context = GRContext.CreateGl(_glInterface);
var options = AvaloniaLocator.Current.GetService<SkiaOptions>();
// bump the default resource cache limit
_context.SetResourceCacheLimit(options?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024);
Console.WriteLine("glcontext created and resource limit set");
}
_topLevelImpl.SetSurface(_context, _jsGlInfo, ColorType,
new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi);
_initialised = true;
_topLevel.Prepare();
_topLevel.Renderer.Start();
// Note: this is technically a hack, but it's a kinda unique use case when
// we want to blit the previous frame
// renderer doesn't have much control over the render target
// we render on the UI thread
// We also don't want to have it as a meaningful public API.
// Therefore we have InternalsVisibleTo hack here.
if (_topLevel.Renderer is DeferredRenderer dr)
{
dr.Render(true);
}
Invalidate();
});
} }
} }
@ -335,16 +319,28 @@ namespace Avalonia.Web.Blazor
_interop.Dispose(); _interop.Dispose();
} }
private void OnDpiChanged(double newDpi) private void ForceBlit()
{ {
_dpi = newDpi; // Note: this is technically a hack, but it's a kinda unique use case when
// we want to blit the previous frame
// renderer doesn't have much control over the render target
// we render on the UI thread
// We also don't want to have it as a meaningful public API.
// Therefore we have InternalsVisibleTo hack here.
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
if (_topLevel.Renderer is DeferredRenderer dr) if (_topLevel.Renderer is DeferredRenderer dr)
{ {
dr.Render(true); dr.Render(true);
} }
}
private void OnDpiChanged(double newDpi)
{
_dpi = newDpi;
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
ForceBlit();
Invalidate(); Invalidate();
} }
@ -357,17 +353,14 @@ namespace Avalonia.Web.Blazor
_topLevelImpl.SetClientSize(_canvasSize, _dpi); _topLevelImpl.SetClientSize(_canvasSize, _dpi);
if (_topLevel.Renderer is DeferredRenderer dr) ForceBlit();
{
dr.Render(true);
}
Invalidate(); Invalidate();
} }
public void Invalidate() public void Invalidate()
{ {
if (!_initialised || _canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0 || _jsGlInfo == null) if (!_initialised || _jsGlInfo == null)
{ {
Console.WriteLine("invalidate ignored"); Console.WriteLine("invalidate ignored");
return; return;

Loading…
Cancel
Save