From 9275e599090bb3a3428411ef8c8c4107f2624fa6 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 22 Dec 2021 21:36:48 -0500 Subject: [PATCH 01/50] Do not recalculate pointer popup position on parent moved --- src/Avalonia.Controls/Primitives/Popup.cs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index ffab7f86d1..a47149a9e0 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -429,16 +429,20 @@ namespace Avalonia.Controls.Primitives (x, handler) => x.LostFocus += handler, (x, handler) => x.LostFocus -= handler).DisposeWith(handlerCleanup); - SubscribeToEventHandler>(window.PlatformImpl, WindowPositionChanged, - (x, handler) => x.PositionChanged += handler, - (x, handler) => x.PositionChanged -= handler).DisposeWith(handlerCleanup); - - if (placementTarget is Layoutable layoutTarget) + // Recalculate popup position on parent moved/resized, but not if placement was on pointer + if (PlacementMode != PlacementMode.Pointer) { - // If the placement target is moved, update the popup position - SubscribeToEventHandler(layoutTarget, PlacementTargetLayoutUpdated, - (x, handler) => x.LayoutUpdated += handler, - (x, handler) => x.LayoutUpdated -= handler).DisposeWith(handlerCleanup); + SubscribeToEventHandler>(window.PlatformImpl, WindowPositionChanged, + (x, handler) => x.PositionChanged += handler, + (x, handler) => x.PositionChanged -= handler).DisposeWith(handlerCleanup); + + if (placementTarget is Layoutable layoutTarget) + { + // If the placement target is moved, update the popup position + SubscribeToEventHandler(layoutTarget, PlacementTargetLayoutUpdated, + (x, handler) => x.LayoutUpdated += handler, + (x, handler) => x.LayoutUpdated -= handler).DisposeWith(handlerCleanup); + } } } else if (topLevel is PopupRoot parentPopupRoot) From 50a97dc22f4f48090dd2a8f01e20aa71c8a586df Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 22 Dec 2021 21:37:05 -0500 Subject: [PATCH 02/50] Add more popup tests --- .../Primitives/PopupTests.cs | 237 +++++++++++++++++- 1 file changed, 224 insertions(+), 13 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 24e4631aff..9c1822ff5c 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -16,6 +16,8 @@ using Avalonia.VisualTree; using Xunit; using Avalonia.Input; using Avalonia.Rendering; +using System.Threading.Tasks; +using Avalonia.Threading; namespace Avalonia.Controls.UnitTests.Primitives { @@ -597,27 +599,44 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Popup_Should_Follow_Placement_Target_On_Window_Move() + public void Popup_Should_Not_Follow_Placement_Target_On_Window_Move_If_Pointer() { using (CreateServices()) { - var popup = new Popup { Width = 400, Height = 200 }; + var popup = new Popup + { + Width = 400, + Height = 200, + PlacementMode = PlacementMode.Pointer + }; var window = PreparedWindow(popup); + window.Show(); popup.Open(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + var raised = false; if (popup.Host is PopupRoot popupRoot) { - // Moving the window must move the popup (screen coordinates have changed) - var raised = false; popupRoot.PositionChanged += (_, args) => { - Assert.Equal(new PixelPoint(10, 10), args.Point); raised = true; }; - window.Position = new PixelPoint(10, 10); - Assert.True(raised); } + else if (popup.Host is OverlayPopupHost overlayPopupHost) + { + overlayPopupHost.PropertyChanged += (_, args) => + { + if (args.Property == Canvas.TopProperty + || args.Property == Canvas.LeftProperty) + { + raised = true; + } + }; + } + window.Position = new PixelPoint(10, 10); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + Assert.False(raised); } } @@ -634,30 +653,222 @@ namespace Avalonia.Controls.UnitTests.Primitives HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center }; - var popup = new Popup() { PlacementTarget = placementTarget, Width = 10, Height = 10 }; + var popup = new Popup() + { + PlacementTarget = placementTarget, + PlacementMode = PlacementMode.Bottom, + Width = 10, + Height = 10 + }; ((ISetLogicalParent)popup).SetParent(popup.PlacementTarget); var window = PreparedWindow(placementTarget); window.Show(); popup.Open(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); // The target's initial placement is (395,295) which is a 10x10 panel centered in a 800x600 window Assert.Equal(placementTarget.Bounds, new Rect(395D, 295D, 10, 10)); + var raised = false; + // Resizing the window to 700x500 must move the popup to (345,255) as this is the new + // location of the placement target if (popup.Host is PopupRoot popupRoot) { - // Resizing the window to 700x500 must move the popup to (345,255) as this is the new - // location of the placement target - var raised = false; popupRoot.PositionChanged += (_, args) => { Assert.Equal(new PixelPoint(345, 255), args.Point); raised = true; }; - window.PlatformImpl?.Resize(new Size(700D, 500D), PlatformResizeReason.Unspecified); - Assert.True(raised); } + else if (popup.Host is OverlayPopupHost overlayPopupHost) + { + overlayPopupHost.PropertyChanged += (_, args) => + { + if ((args.Property == Canvas.TopProperty + || args.Property == Canvas.LeftProperty) + && Canvas.GetLeft(overlayPopupHost) == 345 + && Canvas.GetTop(overlayPopupHost) == 255) + { + raised = true; + } + }; + } + window.PlatformImpl?.Resize(new Size(700D, 500D), PlatformResizeReason.Unspecified); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + Assert.True(raised); + } + } + + [Fact] + public void Popup_Should_Not_Follow_Placement_Target_On_Window_Resize_If_Pointer_If_Pointer() + { + using (CreateServices()) + { + + var placementTarget = new Panel() + { + Width = 10, + Height = 10, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + var popup = new Popup() + { + PlacementTarget = placementTarget, + PlacementMode = PlacementMode.Pointer, + Width = 10, + Height = 10 + }; + ((ISetLogicalParent)popup).SetParent(popup.PlacementTarget); + + var window = PreparedWindow(placementTarget); + window.Show(); + popup.Open(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + + // The target's initial placement is (395,295) which is a 10x10 panel centered in a 800x600 window + Assert.Equal(placementTarget.Bounds, new Rect(395D, 295D, 10, 10)); + + var raised = false; + if (popup.Host is PopupRoot popupRoot) + { + popupRoot.PositionChanged += (_, args) => + { + raised = true; + }; + + } + else if (popup.Host is OverlayPopupHost overlayPopupHost) + { + overlayPopupHost.PropertyChanged += (_, args) => + { + if (args.Property == Canvas.TopProperty + || args.Property == Canvas.LeftProperty) + { + raised = true; + } + }; + } + window.PlatformImpl?.Resize(new Size(700D, 500D), PlatformResizeReason.Unspecified); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + Assert.False(raised); + } + } + + [Fact] + public void Popup_Should_Follow_Placement_Target_On_Target_Moved() + { + using (CreateServices()) + { + var placementTarget = new Panel() + { + Width = 10, + Height = 10, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + var popup = new Popup() + { + PlacementTarget = placementTarget, + PlacementMode = PlacementMode.Bottom, + Width = 10, + Height = 10 + }; + ((ISetLogicalParent)popup).SetParent(popup.PlacementTarget); + + var window = PreparedWindow(placementTarget); + window.Show(); + popup.Open(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + + // The target's initial placement is (395,295) which is a 10x10 panel centered in a 800x600 window + Assert.Equal(placementTarget.Bounds, new Rect(395D, 295D, 10, 10)); + + var raised = false; + // Margin will move placement target + if (popup.Host is PopupRoot popupRoot) + { + popupRoot.PositionChanged += (_, args) => + { + Assert.Equal(new PixelPoint(400, 305), args.Point); + raised = true; + }; + + } + else if (popup.Host is OverlayPopupHost overlayPopupHost) + { + overlayPopupHost.PropertyChanged += (_, args) => + { + if ((args.Property == Canvas.TopProperty + || args.Property == Canvas.LeftProperty) + && Canvas.GetLeft(overlayPopupHost) == 400 + && Canvas.GetTop(overlayPopupHost) == 305) + { + raised = true; + } + }; + } + placementTarget.Margin = new Thickness(10, 0, 0, 0); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + Assert.True(raised); + } + } + + [Fact] + public void Popup_Should_Not_Follow_Placement_Target_On_Target_Moved_If_Pointer() + { + using (CreateServices()) + { + + var placementTarget = new Panel() + { + Width = 10, + Height = 10, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + var popup = new Popup() + { + PlacementTarget = placementTarget, + PlacementMode = PlacementMode.Pointer, + Width = 10, + Height = 10 + }; + ((ISetLogicalParent)popup).SetParent(popup.PlacementTarget); + + var window = PreparedWindow(placementTarget); + window.Show(); + popup.Open(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + + // The target's initial placement is (395,295) which is a 10x10 panel centered in a 800x600 window + Assert.Equal(placementTarget.Bounds, new Rect(395D, 295D, 10, 10)); + + var raised = false; + if (popup.Host is PopupRoot popupRoot) + { + popupRoot.PositionChanged += (_, args) => + { + raised = true; + }; + + } + else if (popup.Host is OverlayPopupHost overlayPopupHost) + { + overlayPopupHost.PropertyChanged += (_, args) => + { + if (args.Property == Canvas.TopProperty + || args.Property == Canvas.LeftProperty) + { + raised = true; + } + }; + } + placementTarget.Margin = new Thickness(10, 0, 0, 0); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + Assert.False(raised); } } From 9c0964adf56ab573daed6be6c88ba480db2a0ec0 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 23 Jan 2022 01:41:48 +0300 Subject: [PATCH 03/50] Added GetIntermediatePoints support for X11, libinput and evdev --- samples/ControlCatalog/Pages/PointersPage.cs | 220 +++++++++++++++++- src/Avalonia.Base/Threading/Dispatcher.cs | 7 + src/Avalonia.Base/Threading/JobRunner.cs | 15 ++ src/Avalonia.Input/MouseDevice.cs | 11 +- src/Avalonia.Input/PointerEventArgs.cs | 44 +++- src/Avalonia.Input/Raw/RawInputEventArgs.cs | 2 +- src/Avalonia.Input/Raw/RawPointerEventArgs.cs | 9 +- src/Avalonia.Input/TouchDevice.cs | 2 +- src/Avalonia.X11/Avalonia.X11.csproj | 1 + src/Avalonia.X11/X11Window.cs | 40 +--- .../Avalonia.LinuxFramebuffer.csproj | 1 + .../Input/EvDev/EvDevBackend.cs | 38 +-- .../Input/LibInput/LibInputBackend.cs | 31 +-- .../Input/RawEventGroupingThreadingHelper.cs | 43 ++++ src/Shared/RawEventGrouping.cs | 129 ++++++++++ 15 files changed, 489 insertions(+), 104 deletions(-) create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Input/RawEventGroupingThreadingHelper.cs create mode 100644 src/Shared/RawEventGrouping.cs diff --git a/samples/ControlCatalog/Pages/PointersPage.cs b/samples/ControlCatalog/Pages/PointersPage.cs index fddc503a90..2901013cea 100644 --- a/samples/ControlCatalog/Pages/PointersPage.cs +++ b/samples/ControlCatalog/Pages/PointersPage.cs @@ -1,15 +1,37 @@ 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.Input; +using Avalonia.Layout; using Avalonia.Media; 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 { @@ -45,7 +67,7 @@ namespace ControlCatalog.Pages private Dictionary _pointers = new Dictionary(); - public PointersPage() + public PointerContactsTab() { 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 _pointers = new(); + class PointerPoints + { + struct CanvasPoint + { + public IBrush Brush; + public Point Point; + public double Radius; + } + + readonly CanvasPoint[] _points = new CanvasPoint[1000]; + int _index; + + public void Render(DrawingContext context) + { + + CanvasPoint? prev = null; + for (var c = 0; c < _points.Length; c++) + { + var i = (c + _index) % _points.Length; + var pt = _points[i]; + if (prev.HasValue && prev.Value.Brush != null && pt.Brush != null) + context.DrawLine(new Pen(Brushes.Black), prev.Value.Point, pt.Point); + prev = pt; + if (pt.Brush != null) + context.DrawEllipse(pt.Brush, null, pt.Point, pt.Radius, pt.Radius); + + } + + } + + void AddPoint(Point pt, IBrush brush, double radius) + { + _points[_index] = new CanvasPoint { Point = pt, Brush = brush, Radius = radius }; + _index = (_index + 1) % _points.Length; + } + + public void HandleEvent(PointerEventArgs e, Visual v) + { + e.Handled = true; + if (e.RoutedEvent == PointerPressedEvent) + AddPoint(e.GetPosition(v), Brushes.Green, 10); + else if (e.RoutedEvent == PointerReleasedEvent) + AddPoint(e.GetPosition(v), Brushes.Red, 10); + else + { + var pts = e.GetIntermediatePoints(v); + for (var c = 0; c < pts.Count; c++) + { + var pt = pts[c]; + AddPoint(pt.Position, c == pts.Count - 1 ? Brushes.Blue : Brushes.Black, + c == pts.Count - 1 ? 5 : 2); + } + } + } + } + + public PointerCanvas(Slider slider, TextBlock status) + { + _slider = slider; + _status = status; + DispatcherTimer.Run(() => + { + if (_stopwatch.Elapsed.TotalSeconds > 1) + { + _status.Text = "Events per second: " + (_events / _stopwatch.Elapsed.TotalSeconds); + _stopwatch.Restart(); + _events = 0; + } + + return this.GetVisualRoot() != null; + }, TimeSpan.FromMilliseconds(10)); + } + + + void HandleEvent(PointerEventArgs e) + { + _events++; + Thread.Sleep((int)_slider.Value); + InvalidateVisual(); + + if (e.RoutedEvent == PointerReleasedEvent && e.Pointer.Type == PointerType.Touch) + { + _pointers.Remove(e.Pointer.Id); + return; + } + + if (!_pointers.TryGetValue(e.Pointer.Id, out var pt)) + _pointers[e.Pointer.Id] = pt = new PointerPoints(); + pt.HandleEvent(e, this); + + + } + + public override void Render(DrawingContext context) + { + context.FillRectangle(Brushes.White, Bounds); + foreach(var pt in _pointers.Values) + pt.Render(context); + base.Render(context); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + if (e.ClickCount == 2) + { + _pointers.Clear(); + InvalidateVisual(); + return; + } + + HandleEvent(e); + base.OnPointerPressed(e); + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + HandleEvent(e); + base.OnPointerMoved(e); + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + HandleEvent(e); + base.OnPointerReleased(e); + } + } + + } } diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index 908f431776..49cee441d0 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -74,6 +74,13 @@ namespace Avalonia.Threading /// /// public void RunJobs(DispatcherPriority minimumPriority) => _jobRunner.RunJobs(minimumPriority); + + /// + /// Use this method to check if there are more prioritized tasks + /// + /// + public bool HasJobsWithPriority(DispatcherPriority minimumPriority) => + _jobRunner.HasJobsWithPriority(minimumPriority); /// public Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal) diff --git a/src/Avalonia.Base/Threading/JobRunner.cs b/src/Avalonia.Base/Threading/JobRunner.cs index f2aef0414c..4b304d44f6 100644 --- a/src/Avalonia.Base/Threading/JobRunner.cs +++ b/src/Avalonia.Base/Threading/JobRunner.cs @@ -121,6 +121,21 @@ namespace Avalonia.Threading 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 { /// diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index a2c43013fd..166af44d04 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; using Avalonia.Input.Raw; @@ -159,7 +160,7 @@ namespace Avalonia.Input case RawPointerEventType.XButton1Down: case RawPointerEventType.XButton2Down: 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 e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); @@ -170,12 +171,12 @@ namespace Avalonia.Input case RawPointerEventType.XButton1Up: case RawPointerEventType.XButton2Up: 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 e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); break; 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; case RawPointerEventType.Wheel: e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers); @@ -263,7 +264,7 @@ namespace Avalonia.Input } private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, - KeyModifiers inputModifiers) + KeyModifiers inputModifiers, IReadOnlyList? intermediatePoints) { device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); @@ -283,7 +284,7 @@ namespace Avalonia.Input if (source is object) { var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, - p, timestamp, properties, inputModifiers); + p, timestamp, properties, inputModifiers, intermediatePoints); source.RaiseEvent(e); return e.Handled; diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index 8c86cd4637..40495a2f0a 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia.Input.Raw; using Avalonia.Interactivity; using Avalonia.VisualTree; @@ -10,6 +11,7 @@ namespace Avalonia.Input private readonly IVisual? _rootVisual; private readonly Point _rootVisualPosition; private readonly PointerPointProperties _properties; + private readonly IReadOnlyList? _previousPoints; public PointerEventArgs(RoutedEvent routedEvent, IInteractive? source, @@ -28,6 +30,20 @@ namespace Avalonia.Input Timestamp = timestamp; KeyModifiers = modifiers; } + + public PointerEventArgs(RoutedEvent routedEvent, + IInteractive? source, + IPointer pointer, + IVisual? rootVisual, Point rootVisualPosition, + ulong timestamp, + PointerPointProperties properties, + KeyModifiers modifiers, + IReadOnlyList? previousPoints) + : this(routedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers) + { + _previousPoints = previousPoints; + } + class EmulatedDevice : IPointerDevice { @@ -76,14 +92,16 @@ namespace Avalonia.Input public KeyModifiers KeyModifiers { get; } - public Point GetPosition(IVisual? relativeTo) + private Point GetPosition(Point pt, IVisual? relativeTo) { if (_rootVisual == null) return default; if (relativeTo == null) - return _rootVisualPosition; - return _rootVisualPosition * _rootVisual.TransformToVisual(relativeTo) ?? default; + return pt; + return pt * _rootVisual.TransformToVisual(relativeTo) ?? default; } + + public Point GetPosition(IVisual? relativeTo) => GetPosition(_rootVisualPosition, relativeTo); [Obsolete("Use GetCurrentPoint")] public PointerPoint GetPointerPoint(IVisual? relativeTo) => GetCurrentPoint(relativeTo); @@ -96,6 +114,26 @@ namespace Avalonia.Input public PointerPoint GetCurrentPoint(IVisual? relativeTo) => new PointerPoint(Pointer, GetPosition(relativeTo), _properties); + /// + /// Returns the PointerPoint associated with the current event + /// + /// The visual which coordinate system to use. Pass null for toplevel coordinate system + /// + public IReadOnlyList 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; + } + /// /// Returns the current pointer point properties /// diff --git a/src/Avalonia.Input/Raw/RawInputEventArgs.cs b/src/Avalonia.Input/Raw/RawInputEventArgs.cs index dcc5f27a79..3a5ae1340f 100644 --- a/src/Avalonia.Input/Raw/RawInputEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawInputEventArgs.cs @@ -51,6 +51,6 @@ namespace Avalonia.Input.Raw /// /// Gets the timestamp associated with the event. /// - public ulong Timestamp { get; private set; } + public ulong Timestamp { get; set; } } } diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index 62a1dd5d84..6cb8a10cf3 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Avalonia.Input.Raw { @@ -68,6 +69,12 @@ namespace Avalonia.Input.Raw /// /// Gets the input modifiers. /// - public RawInputModifiers InputModifiers { get; private set; } + public RawInputModifiers InputModifiers { get; set; } + + /// + /// Points that were traversed by a pointer since the previous relevant event, + /// only valid for Move and TouchUpdate + /// + public IReadOnlyList? IntermediatePoints { get; set; } } } diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs index 0069aa7961..12ad182bf8 100644 --- a/src/Avalonia.Input/TouchDevice.cs +++ b/src/Avalonia.Input/TouchDevice.cs @@ -104,7 +104,7 @@ namespace Avalonia.Input target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root, args.Position, ev.Timestamp, new PointerPointProperties(GetModifiers(args.InputModifiers, true), PointerUpdateKind.Other), - GetKeyModifiers(args.InputModifiers))); + GetKeyModifiers(args.InputModifiers), args.IntermediatePoints)); } diff --git a/src/Avalonia.X11/Avalonia.X11.csproj b/src/Avalonia.X11/Avalonia.X11.csproj index 9ba5c9d15f..45a76bc3d6 100644 --- a/src/Avalonia.X11/Avalonia.X11.csproj +++ b/src/Avalonia.X11/Avalonia.X11.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index d745b4765b..0f881eb91a 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -49,15 +49,10 @@ namespace Avalonia.X11 private double? _scalingOverride; private bool _disabled; private TransparencyHelper _transparencyHelper; - + private RawEventGrouper _rawEventGrouper; public object SyncRoot { get; } = new object(); - class InputEventContainer - { - public RawInputEventArgs Event; - } - private readonly Queue _inputQueue = new Queue(); - private InputEventContainer _lastEvent; + private bool _useRenderWindow = false; public X11Window(AvaloniaX11Platform platform, IWindowImpl popupParent) { @@ -181,6 +176,8 @@ namespace Avalonia.X11 UpdateMotifHints(); UpdateSizeHints(null); + _rawEventGrouper = new RawEventGrouper(e => Input?.Invoke(e)); + _transparencyHelper = new TransparencyHelper(_x11, _handle, platform.Globals); _transparencyHelper.SetTransparencyRequest(WindowTransparencyLevel.None); @@ -735,33 +732,14 @@ namespace Avalonia.X11 if (args is RawDragEvent drag) drag.Location = drag.Location / RenderScaling; - _lastEvent = new InputEventContainer() {Event = 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); - } + _rawEventGrouper.HandleEvent(args); } void MouseEvent(RawPointerEventType type, ref XEvent ev, XModifierMask mods) { var mev = new RawPointerEventArgs( _mouse, (ulong)ev.ButtonEvent.time.ToInt64(), _inputRoot, - 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; - } + type, new Point(ev.ButtonEvent.x, ev.ButtonEvent.y), TranslateModifiers(mods)); ScheduleInput(mev, ref ev); } @@ -789,6 +767,12 @@ namespace Avalonia.X11 void Cleanup() { + if (_rawEventGrouper != null) + { + _rawEventGrouper.Dispose(); + _rawEventGrouper = null; + } + if (_transparencyHelper != null) { _transparencyHelper.Dispose(); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj b/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj index 6a2eb18385..5cebbb6829 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj +++ b/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj @@ -7,5 +7,6 @@ + diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs index b3fc979fca..2eb10ae666 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs @@ -13,15 +13,16 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev private readonly EvDevDeviceDescription[] _deviceDescriptions; private readonly List _handlers = new List(); private int _epoll; - private Queue _inputQueue = new Queue(); private bool _isQueueHandlerTriggered; private object _lock = new object(); private Action _onInput; private IInputRoot _inputRoot; + private RawEventGroupingThreadingHelper _inputQueue; public EvDevBackend(EvDevDeviceDescription[] devices) { _deviceDescriptions = devices; + _inputQueue = new RawEventGroupingThreadingHelper(e => _onInput?.Invoke(e)); } unsafe void InputThread() @@ -49,42 +50,9 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev private void OnRawEvent(RawInputEventArgs obj) { - lock (_lock) - { - _inputQueue.Enqueue(obj); - TriggerQueueHandler(); - } - + _inputQueue.OnEvent(obj); } - 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 onInput) { diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index 432344955a..15d42789d4 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -17,15 +17,15 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput private TouchDevice _touch = new TouchDevice(); private MouseDevice _mouse = new MouseDevice(); private Point _mousePosition; - - private readonly Queue _inputQueue = new Queue(); + + private readonly RawEventGroupingThreadingHelper _inputQueue; private Action _onInput; private Dictionary _pointers = new Dictionary(); public LibInputBackend() { var ctx = libinput_path_create_context(); - + _inputQueue = new(e => _onInput?.Invoke(e)); new Thread(()=>InputThread(ctx)).Start(); } @@ -66,30 +66,7 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput } } - private void ScheduleInput(RawInputEventArgs 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 ScheduleInput(RawInputEventArgs ev) => _inputQueue.OnEvent(ev); private void HandleTouch(IntPtr ev, LibInputEventType type) { diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/RawEventGroupingThreadingHelper.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/RawEventGroupingThreadingHelper.cs new file mode 100644 index 0000000000..f706f18461 --- /dev/null +++ b/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 _rawQueue = new(); + private readonly Action _queueHandler; + + public RawEventGroupingThreadingHelper(Action 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); +} \ No newline at end of file diff --git a/src/Shared/RawEventGrouping.cs b/src/Shared/RawEventGrouping.cs new file mode 100644 index 0000000000..25b4b41e56 --- /dev/null +++ b/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 _eventCallback; + private readonly Queue _inputQueue = new(); + private readonly Action _dispatchFromQueue; + readonly Dictionary _lastTouchPoints = new(); + RawInputEventArgs? _lastEvent; + + public RawEventGrouper(Action 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 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(); + ((PooledList)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(); + } +} + From cd2c6b4859def31b9fcb8915515e2686330a48fa Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 23 Jan 2022 20:27:23 +0000 Subject: [PATCH 04/50] optimize inital setup and fix initial render pass. --- .../Avalonia.Web.Blazor/AvaloniaView.razor.cs | 137 +++++++++--------- 1 file changed, 65 insertions(+), 72 deletions(-) diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index dc8b091563..bb8ae9e119 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -26,8 +26,8 @@ namespace Avalonia.Web.Blazor private InputHelperInterop _canvasHelper = null!; private ElementReference _htmlCanvas; private ElementReference _inputElement; - private double _dpi; - private SKSize _canvasSize; + private double _dpi = 1; + private SKSize _canvasSize = new (100, 100); private GRContext? _context; private GRGlInterface? _glInterface; @@ -249,71 +249,55 @@ namespace Avalonia.Web.Blazor [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary? AdditionalAttributes { get; set; } - protected override void OnAfterRender(bool firstRender) + protected override async Task OnAfterRenderAsync(bool 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); - _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"); - - _sizeWatcher = await SizeWatcherInterop.ImportAsync(Js, _htmlCanvas, OnSizeChanged); - _dpiWatcher = await DpiWatcherInterop.ImportAsync(Js, OnDpiChanged); - - Console.WriteLine("watchers created."); - - // create the SkiaSharp context - if (_context == null) - { - Console.WriteLine("create glcontext"); - _glInterface = GRGlInterface.Create(); - _context = GRContext.CreateGl(_glInterface); - - var options = AvaloniaLocator.Current.GetService(); - // 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(); - }); + Console.WriteLine("create glcontext"); + _glInterface = GRGlInterface.Create(); + _context = GRContext.CreateGl(_glInterface); + + var options = AvaloniaLocator.Current.GetService(); + // 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(); + + Invalidate(); + + _sizeWatcher = await SizeWatcherInterop.ImportAsync(Js, _htmlCanvas, OnSizeChanged); + _dpiWatcher = await DpiWatcherInterop.ImportAsync(Js, OnDpiChanged); } } @@ -335,16 +319,28 @@ namespace Avalonia.Web.Blazor _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) { dr.Render(true); } + } + + private void OnDpiChanged(double newDpi) + { + _dpi = newDpi; + + _topLevelImpl.SetClientSize(_canvasSize, _dpi); + + ForceBlit(); Invalidate(); } @@ -357,17 +353,14 @@ namespace Avalonia.Web.Blazor _topLevelImpl.SetClientSize(_canvasSize, _dpi); - if (_topLevel.Renderer is DeferredRenderer dr) - { - dr.Render(true); - } + ForceBlit(); Invalidate(); } public void Invalidate() { - if (!_initialised || _canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0 || _jsGlInfo == null) + if (!_initialised || _jsGlInfo == null) { Console.WriteLine("invalidate ignored"); return; From 365a8250c7f63da22246e7c18a8d50b3d1e3501d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 24 Jan 2022 11:59:59 +0000 Subject: [PATCH 05/50] fix stability of wasm init. --- .../Avalonia.Web.Blazor/AvaloniaView.razor.cs | 56 +++++++++---------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index bb8ae9e119..07c776efb4 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -287,17 +287,22 @@ namespace Avalonia.Web.Blazor _topLevelImpl.SetSurface(_context, _jsGlInfo, ColorType, new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi); + + _interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _initialised = true; - _topLevel.Prepare(); - - _topLevel.Renderer.Start(); - - Invalidate(); - - _sizeWatcher = await SizeWatcherInterop.ImportAsync(Js, _htmlCanvas, OnSizeChanged); - _dpiWatcher = await DpiWatcherInterop.ImportAsync(Js, OnDpiChanged); + Threading.Dispatcher.UIThread.Post(async () => + { + _interop.RequestAnimationFrame(true); + + _sizeWatcher = await SizeWatcherInterop.ImportAsync(Js, _htmlCanvas, OnSizeChanged); + _dpiWatcher = await DpiWatcherInterop.ImportAsync(Js, OnDpiChanged); + + _topLevel.Prepare(); + + _topLevel.Renderer.Start(); + }); } } @@ -336,37 +341,30 @@ namespace Avalonia.Web.Blazor private void OnDpiChanged(double newDpi) { - _dpi = newDpi; + if (Math.Abs(_dpi - newDpi) > 0.0001) + { + _dpi = newDpi; + + _interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); - _topLevelImpl.SetClientSize(_canvasSize, _dpi); - - ForceBlit(); + _topLevelImpl.SetClientSize(_canvasSize, _dpi); - Invalidate(); + ForceBlit(); + } } private void OnSizeChanged(SKSize newSize) { - _canvasSize = newSize; - - _interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); - - _topLevelImpl.SetClientSize(_canvasSize, _dpi); + if (_canvasSize != newSize) + { + _canvasSize = newSize; - ForceBlit(); + _interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); - Invalidate(); - } + _topLevelImpl.SetClientSize(_canvasSize, _dpi); - public void Invalidate() - { - if (!_initialised || _jsGlInfo == null) - { - Console.WriteLine("invalidate ignored"); - return; + ForceBlit(); } - - _interop.RequestAnimationFrame(true); } public void SetActive(bool active) From 71c438a4a370c4429a94b1a8ae217aeeaa1ffd49 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Tue, 25 Jan 2022 10:43:51 +0200 Subject: [PATCH 06/50] fix --- src/Avalonia.Base/Collections/AvaloniaDictionary.cs | 1 + .../Collections/AvaloniaDictionaryTests.cs | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs index 0e027712e0..2fe68e824d 100644 --- a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs +++ b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs @@ -146,6 +146,7 @@ namespace Avalonia.Collections { if (_inner.TryGetValue(key, out var value)) { + _inner.Remove(key); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[{key}]")); diff --git a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaDictionaryTests.cs b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaDictionaryTests.cs index df3ca4e4dc..739c3fed79 100644 --- a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaDictionaryTests.cs +++ b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaDictionaryTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Collections.Specialized; using Avalonia.Collections; @@ -104,6 +105,16 @@ namespace Avalonia.Base.UnitTests.Collections Assert.Equal(new KeyValuePair("foo", "bar"), tracker.Args.OldItems[0]); } + [Fact] + public void Remove_Method_Should_Remove_Item_From_Collection() + { + var target = new AvaloniaDictionary() { { "foo", "bar" } }; + Assert.Equal(target.Count, 1); + + target.Remove("foo"); + Assert.Equal(target.Count, 0); + } + [Fact] public void Removing_Item_Should_Raise_PropertyChanged() { From 513cbadacb45b1a740e065c22b721d6a109a6428 Mon Sep 17 00:00:00 2001 From: Jeroen van Langen Date: Tue, 25 Jan 2022 13:03:44 +0100 Subject: [PATCH 07/50] Minor typo fixed --- samples/ControlCatalog.Android/Assets/AboutAssets.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/ControlCatalog.Android/Assets/AboutAssets.txt b/samples/ControlCatalog.Android/Assets/AboutAssets.txt index ee39886295..a9b0638eb1 100644 --- a/samples/ControlCatalog.Android/Assets/AboutAssets.txt +++ b/samples/ControlCatalog.Android/Assets/AboutAssets.txt @@ -1,7 +1,7 @@ Any raw assets you want to be deployed with your application can be placed in this directory (and child directories) and given a Build Action of "AndroidAsset". -These files will be deployed with you package and will be accessible using Android's +These files will be deployed with your package and will be accessible using Android's AssetManager, like this: public class ReadAsset : Activity @@ -16,4 +16,4 @@ public class ReadAsset : Activity Additionally, some Android functions will automatically load asset files: -Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); \ No newline at end of file +Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); From fd3c44bb8197524e2b8a79dc5c4f88448d2e89b4 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 25 Jan 2022 17:32:04 -0500 Subject: [PATCH 08/50] Ensure menu children are closed on menu flyout presenter detached --- .../Flyouts/MenuFlyoutPresenter.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs b/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs index 3a45c85c70..bcd859100a 100644 --- a/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs +++ b/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs @@ -43,5 +43,18 @@ namespace Avalonia.Controls { return new MenuItemContainerGenerator(this); } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + foreach (var i in LogicalChildren) + { + if (i is MenuItem menuItem) + { + menuItem.IsSubMenuOpen = false; + } + } + } } } From abfb2ed4629eb910fc6dd2cde682eb01df32ded1 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 26 Jan 2022 13:11:03 +0000 Subject: [PATCH 09/50] Trigger control inspection on key up --- src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 50082acca6..81865f7daf 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -27,7 +27,7 @@ namespace Avalonia.Diagnostics.Views _keySubscription = InputManager.Instance?.Process .OfType() - .Where(x => x.Type == RawKeyEventType.KeyDown) + .Where(x => x.Type == RawKeyEventType.KeyUp) .Subscribe(RawKeyDown); _frozenPopupStates = new Dictionary(); From 280eef2d239dcac0d8252712bdbc7d0d258ef35c Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 26 Jan 2022 16:41:20 +0000 Subject: [PATCH 10/50] maintain key down event, add extra cases to handle shift and control modifiers --- .../Diagnostics/Views/MainWindow.xaml.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 81865f7daf..7a894d96fb 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -27,7 +27,7 @@ namespace Avalonia.Diagnostics.Views _keySubscription = InputManager.Instance?.Process .OfType() - .Where(x => x.Type == RawKeyEventType.KeyUp) + .Where(x => x.Type == RawKeyEventType.KeyDown) .Subscribe(RawKeyDown); _frozenPopupStates = new Dictionary(); @@ -169,7 +169,9 @@ namespace Avalonia.Diagnostics.Views switch (e.Modifiers) { - case RawInputModifiers.Control | RawInputModifiers.Shift: + case RawInputModifiers.Control when (e.Key == Key.LeftShift || e.Key == Key.RightShift): + case RawInputModifiers.Shift when (e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl): + case RawInputModifiers.Shift | RawInputModifiers.Control: { IControl? control = null; From 1bbe38c34f0d41e524a39d226db7f8bdc8f3cb79 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 26 Jan 2022 19:16:46 +0000 Subject: [PATCH 11/50] update copyright year --- build/SharedVersion.props | 2 +- src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/SharedVersion.props b/build/SharedVersion.props index 7d75901288..7f24ef35bc 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -3,7 +3,7 @@ Avalonia 0.10.999 - Copyright 2021 © The AvaloniaUI Project + Copyright 2022 © The AvaloniaUI Project https://avaloniaui.net https://github.com/AvaloniaUI/Avalonia/ true diff --git a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml index c959fdc143..a1d06f800f 100644 --- a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml +++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml @@ -99,7 +99,7 @@ - + From 4bcd5f5dd8cb8f22ef906f8fa8582dc63902e83b Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Thu, 27 Jan 2022 14:22:57 +0000 Subject: [PATCH 12/50] add density style option in Fluent Theme provider --- src/Avalonia.Themes.Fluent/FluentTheme.cs | 53 ++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.cs b/src/Avalonia.Themes.Fluent/FluentTheme.cs index 53be41e4d1..8625eef102 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.cs @@ -15,6 +15,12 @@ namespace Avalonia.Themes.Fluent Dark, } + public enum DensityStyle + { + Normal, + Compact + } + /// /// Includes the fluent theme in an application. /// @@ -24,6 +30,7 @@ namespace Avalonia.Themes.Fluent private Styles _fluentDark = new(); private Styles _fluentLight = new(); private Styles _sharedStyles = new(); + private Styles _densityStyles = new(); private bool _isLoading; private IStyle? _loaded; @@ -47,7 +54,6 @@ namespace Avalonia.Themes.Fluent InitStyles(_baseUri); } - public static readonly StyledProperty ModeProperty = AvaloniaProperty.Register(nameof(Mode)); /// @@ -58,6 +64,18 @@ namespace Avalonia.Themes.Fluent get => GetValue(ModeProperty); set => SetValue(ModeProperty, value); } + + public static readonly StyledProperty DensityStyleProperty = + AvaloniaProperty.Register(nameof(DensityStyle)); + /// + /// Gets or sets the density style of the fluent theme (normal, compact). + /// + public DensityStyle DensityStyle + { + get => GetValue(DensityStyleProperty); + set => SetValue(DensityStyleProperty, value); + } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -74,6 +92,25 @@ namespace Avalonia.Themes.Fluent (Loaded as Styles)![2] = _fluentLight[1]; } } + + if (change.Property == DensityStyleProperty) + { + if (DensityStyle == DensityStyle.Compact) + { + if ((Loaded as Styles)!.Count > 3) + { + (Loaded as Styles)![3] = _densityStyles[0]; + } + else + { + (Loaded as Styles)!.Add( _densityStyles[0]); + } + } + else if(DensityStyle == DensityStyle.Normal) + { + (Loaded as Styles)!.Remove(_densityStyles[0]); + } + } } public IResourceHost? Owner => (Loaded as IResourceProvider)?.Owner; @@ -97,6 +134,12 @@ namespace Avalonia.Themes.Fluent { _loaded = new Styles() { _sharedStyles, _fluentDark[0], _fluentDark[1] }; } + + if (DensityStyle == DensityStyle.Compact) + { + (_loaded as Styles)!.Add(_densityStyles[0]); + } + _isLoading = false; } @@ -183,6 +226,14 @@ namespace Avalonia.Themes.Fluent Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml") } }; + + _densityStyles = new Styles + { + new StyleInclude(baseUri) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/DensityStyles/Compact.xaml") + } + }; } } } From fe11e4f2c746e373648021fdbd508f71d553c98b Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 24 Jan 2022 23:59:57 -0500 Subject: [PATCH 13/50] Minor refactoring to Button noticed when adding SplitButton --- src/Avalonia.Controls/Button.cs | 61 +++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 8537c9acbc..770eb63266 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -76,6 +76,9 @@ namespace Avalonia.Controls public static readonly RoutedEvent ClickEvent = RoutedEvent.Register(nameof(Click), RoutingStrategies.Bubble); + /// + /// Defines the property. + /// public static readonly StyledProperty IsPressedProperty = AvaloniaProperty.Register(nameof(IsPressed)); @@ -102,6 +105,9 @@ namespace Avalonia.Controls AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler public event EventHandler Click { - add { AddHandler(ClickEvent, value); } - remove { RemoveHandler(ClickEvent, value); } + add => AddHandler(ClickEvent, value); + remove => RemoveHandler(ClickEvent, value); } /// @@ -121,8 +127,8 @@ namespace Avalonia.Controls /// public ClickMode ClickMode { - get { return GetValue(ClickModeProperty); } - set { SetValue(ClickModeProperty, value); } + get => GetValue(ClickModeProperty); + set => SetValue(ClickModeProperty, value); } /// @@ -130,8 +136,8 @@ namespace Avalonia.Controls /// public ICommand Command { - get { return _command; } - set { SetAndRaise(CommandProperty, ref _command, value); } + get => _command; + set => SetAndRaise(CommandProperty, ref _command, value); } /// @@ -139,8 +145,8 @@ namespace Avalonia.Controls /// public KeyGesture HotKey { - get { return GetValue(HotKeyProperty); } - set { SetValue(HotKeyProperty, value); } + get => GetValue(HotKeyProperty); + set => SetValue(HotKeyProperty, value); } /// @@ -148,8 +154,8 @@ namespace Avalonia.Controls /// public object CommandParameter { - get { return GetValue(CommandParameterProperty); } - set { SetValue(CommandParameterProperty, value); } + get => GetValue(CommandParameterProperty); + set => SetValue(CommandParameterProperty, value); } /// @@ -158,8 +164,8 @@ namespace Avalonia.Controls /// public bool IsDefault { - get { return GetValue(IsDefaultProperty); } - set { SetValue(IsDefaultProperty, value); } + get => GetValue(IsDefaultProperty); + set => SetValue(IsDefaultProperty, value); } /// @@ -168,18 +174,21 @@ namespace Avalonia.Controls /// public bool IsCancel { - get { return GetValue(IsCancelProperty); } - set { SetValue(IsCancelProperty, value); } + get => GetValue(IsCancelProperty); + set => SetValue(IsCancelProperty, value); } + /// + /// Gets or sets a value indicating whether the button is currently pressed. + /// public bool IsPressed { - get { return GetValue(IsPressedProperty); } - private set { SetValue(IsPressedProperty, value); } + get => GetValue(IsPressedProperty); + private set => SetValue(IsPressedProperty, value); } /// - /// Gets or sets the Flyout that should be shown with this button + /// Gets or sets the Flyout that should be shown with this button. /// public FlyoutBase Flyout { @@ -187,7 +196,8 @@ namespace Avalonia.Controls set => SetValue(FlyoutProperty, value); } - protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; + /// + protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) @@ -224,6 +234,7 @@ namespace Avalonia.Controls } } + /// protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { if (_hotkey != null) // Control attached again, set Hotkey to create a hotkey manager for this control @@ -240,6 +251,7 @@ namespace Avalonia.Controls } } + /// protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { // This will cause the hotkey manager to dispose the observer and the reference to this control @@ -358,12 +370,14 @@ namespace Avalonia.Controls } } } - + + /// protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) { IsPressed = false; } + /// protected override void OnLostFocus(RoutedEventArgs e) { base.OnLostFocus(e); @@ -371,6 +385,7 @@ namespace Avalonia.Controls IsPressed = false; } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -391,6 +406,7 @@ namespace Avalonia.Controls } } + /// protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) { base.UpdateDataValidation(property, value); @@ -493,7 +509,7 @@ namespace Avalonia.Controls /// /// The event sender. /// The event args. - private void CanExecuteChanged(object sender, EventArgs e) + public void CanExecuteChanged(object sender, EventArgs e) { var canExecute = Command == null || Command.CanExecute(CommandParameter); @@ -566,11 +582,12 @@ namespace Avalonia.Controls } } + /// + /// Updates the visual state of the control by applying latest PseudoClasses. + /// private void UpdatePseudoClasses(bool isPressed) { PseudoClasses.Set(":pressed", isPressed); } - - void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); } } From d8e071039bf342781ed9ed1c08d1a849588f8371 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 25 Jan 2022 20:02:40 -0500 Subject: [PATCH 14/50] Move more Button property changed handling into OnPropertyChanged override --- src/Avalonia.Controls/Button.cs | 142 ++-- .../SplitButton/SplitButton.cs | 735 ++++++++++++++++++ 2 files changed, 790 insertions(+), 87 deletions(-) create mode 100644 src/Avalonia.Controls/SplitButton/SplitButton.cs diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 770eb63266..3735e6c010 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -59,13 +59,13 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(CommandParameter)); /// - /// Defines the property. + /// Defines the property. /// public static readonly StyledProperty IsDefaultProperty = AvaloniaProperty.Register(nameof(IsDefault)); /// - /// Defines the property. + /// Defines the property. /// public static readonly StyledProperty IsCancelProperty = AvaloniaProperty.Register(nameof(IsCancel)); @@ -98,10 +98,6 @@ namespace Avalonia.Controls static Button() { FocusableProperty.OverrideDefaultValue(typeof(Button), true); - CommandProperty.Changed.Subscribe(CommandChanged); - CommandParameterProperty.Changed.Subscribe(CommandParameterChanged); - IsDefaultProperty.Changed.Subscribe(IsDefaultChanged); - IsCancelProperty.Changed.Subscribe(IsCancelChanged); AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler