From 02e30f80f5c2293e115ef8b0679e4ddd8537d166 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Wed, 8 Sep 2021 20:53:05 +0300 Subject: [PATCH 01/36] [OSX] Add Swipe, Rotate and Magnify PitchToZoom trackpad gestures as native mouse events, because they can be triggered from magic mouse AFAIK and creation of new TrackpadDevice is too big task for me now --- native/Avalonia.Native/src/OSX/window.mm | 31 ++++++++++++++++++++++++ src/Avalonia.Native/avn.idl | 5 +++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 14fe60ab0b..b1e923c434 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1625,6 +1625,19 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return; } } + else if (type == Magnify) + { + delta.X = delta.Y = [event magnification]; + } + else if (type == Rotate) + { + delta.X = delta.Y = [event rotation]; + } + else if (type == Swipe) + { + delta.X = [event deltaX]; + delta.Y = [event deltaY]; + } auto timestamp = [event timestamp] * 1000; auto modifiers = [self getModifiers:[event modifierFlags]]; @@ -1749,6 +1762,24 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [super scrollWheel:event]; } +- (void)magnifyWithEvent:(NSEvent *)event +{ + [self mouseEvent:event withType:Magnify]; + [super magnifyWithEvent:event]; +} + +- (void)rotateWithEvent:(NSEvent *)event +{ + [self mouseEvent:event withType:Rotate]; + [super rotateWithEvent:event]; +} + +- (void)swipeWithEvent:(NSEvent *)event +{ + [self mouseEvent:event withType:Swipe]; + [super swipeWithEvent:event]; +} + - (void)mouseEntered:(NSEvent *)event { _isMouseOver = true; diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 70d85dacdd..05fcdc71a5 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -295,7 +295,10 @@ enum AvnRawMouseEventType TouchBegin, TouchUpdate, TouchEnd, - TouchCancel + TouchCancel, + Magnify, + Rotate, + Swipe, } enum AvnRawKeyEventType From 33c22a952f13b7217cce887c1f4dfd739f98cce4 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Wed, 8 Sep 2021 21:51:50 +0300 Subject: [PATCH 02/36] [OSX] Add PointerMagnifyGesture, PointerRotateGesture, PointerSwipeGesture events --- src/Avalonia.Input/InputElement.cs | 83 ++++++++++++++++++- src/Avalonia.Input/MouseDevice.cs | 72 ++++++++++++++++ .../PointerMagnifyGestureEventArgs.cs | 19 +++++ .../PointerRotateGestureEventArgs.cs | 19 +++++ .../PointerSwipeGestureEventArgs.cs | 19 +++++ src/Avalonia.Input/Raw/RawPointerEventArgs.cs | 5 +- .../Raw/RawPointerGestureEventArgs.cs | 19 +++++ src/Avalonia.Native/WindowImplBase.cs | 21 ++++- 8 files changed, 253 insertions(+), 4 deletions(-) create mode 100644 src/Avalonia.Input/PointerMagnifyGestureEventArgs.cs create mode 100644 src/Avalonia.Input/PointerRotateGestureEventArgs.cs create mode 100644 src/Avalonia.Input/PointerSwipeGestureEventArgs.cs create mode 100644 src/Avalonia.Input/Raw/RawPointerGestureEventArgs.cs diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 63080e74e4..93434af4a1 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -186,6 +186,30 @@ namespace Avalonia.Input RoutedEvent.Register( "PointerWheelChanged", RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PointerMagnifyGestureEvent = + RoutedEvent.Register( + "PointerMagnifyGesture", + RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PointerRotateGestureEvent = + RoutedEvent.Register( + "PointerRotateGesture", + RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PointerSwipeGestureEvent = + RoutedEvent.Register( + "PointerSwipeGesture", + RoutingStrategies.Tunnel | RoutingStrategies.Bubble); /// /// Defines the event. @@ -223,6 +247,9 @@ namespace Avalonia.Input PointerReleasedEvent.AddClassHandler((x, e) => x.OnPointerReleased(e)); PointerCaptureLostEvent.AddClassHandler((x, e) => x.OnPointerCaptureLost(e)); PointerWheelChangedEvent.AddClassHandler((x, e) => x.OnPointerWheelChanged(e)); + PointerMagnifyGestureEvent.AddClassHandler((x, e) => x.OnPointerMagnifyGesture(e)); + PointerRotateGestureEvent.AddClassHandler((x, e) => x.OnPointerRotateGesture(e)); + PointerSwipeGestureEvent.AddClassHandler((x, e) => x.OnPointerSwipeGesture(e)); } public InputElement() @@ -349,13 +376,43 @@ namespace Avalonia.Input } /// - /// Occurs when the mouse wheen is scrolled over the control. + /// Occurs when the mouse is scrolled over the control. /// public event EventHandler PointerWheelChanged { add { AddHandler(PointerWheelChangedEvent, value); } remove { RemoveHandler(PointerWheelChangedEvent, value); } } + + /// + /// Occurs when the user uses magnify (Pitch to Zoom) gesture on a trackpad and pointer is over the control. + /// Works only on macOS. + /// + public event EventHandler PointerMagnifyGesture + { + add { AddHandler(PointerMagnifyGestureEvent, value); } + remove { RemoveHandler(PointerMagnifyGestureEvent, value); } + } + + /// + /// Occurs when the user uses rotate gesture on a trackpad and pointer is over the control. + /// Works only on macOS. + /// + public event EventHandler PointerRotateGesture + { + add { AddHandler(PointerRotateGestureEvent, value); } + remove { RemoveHandler(PointerRotateGestureEvent, value); } + } + + /// + /// Occurs when the user uses swipe gesture on a trackpad and pointer is over the control. + /// Works only on macOS. + /// + public event EventHandler PointerSwipeGesture + { + add { AddHandler(PointerSwipeGestureEvent, value); } + remove { RemoveHandler(PointerSwipeGestureEvent, value); } + } /// /// Occurs when a tap gesture occurs on the control. @@ -617,6 +674,30 @@ namespace Avalonia.Input protected virtual void OnPointerWheelChanged(PointerWheelEventArgs e) { } + + /// + /// Called before the trackpad event occurs. + /// + /// The event args. + protected virtual void OnPointerMagnifyGesture(PointerMagnifyGestureEventArgs e) + { + } + + /// + /// Called before the trackpad event occurs. + /// + /// The event args. + protected virtual void OnPointerRotateGesture(PointerRotateGestureEventArgs e) + { + } + + /// + /// Called before the trackpad event occurs. + /// + /// The event args. + protected virtual void OnPointerSwipeGesture(PointerSwipeGestureEventArgs e) + { + } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index cfa3690daf..745c0caafd 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -178,6 +178,15 @@ namespace Avalonia.Input case RawPointerEventType.Wheel: e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers); break; + case RawPointerEventType.Magnify: + e.Handled = GestureMagnify(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); + break; + case RawPointerEventType.Rotate: + e.Handled = GestureRotate(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); + break; + case RawPointerEventType.Swipe: + e.Handled = GestureSwipe(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); + break; } } @@ -331,6 +340,69 @@ namespace Avalonia.Input return false; } + + private bool GestureMagnify(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, + PointerPointProperties props, + Vector delta, KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var hit = HitTest(root, p); + + if (hit != null) + { + var source = GetSource(hit); + var e = new PointerMagnifyGestureEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta.X); + + source?.RaiseEvent(e); + return e.Handled; + } + + return false; + } + + private bool GestureRotate(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, + PointerPointProperties props, + Vector delta, KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var hit = HitTest(root, p); + + if (hit != null) + { + var source = GetSource(hit); + var e = new PointerRotateGestureEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta.X); + + source?.RaiseEvent(e); + return e.Handled; + } + + return false; + } + + private bool GestureSwipe(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, + PointerPointProperties props, + Vector delta, KeyModifiers inputModifiers) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var hit = HitTest(root, p); + + if (hit != null) + { + var source = GetSource(hit); + var e = new PointerSwipeGestureEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta); + + source?.RaiseEvent(e); + return e.Handled; + } + + return false; + } private IInteractive GetSource(IVisual hit) { diff --git a/src/Avalonia.Input/PointerMagnifyGestureEventArgs.cs b/src/Avalonia.Input/PointerMagnifyGestureEventArgs.cs new file mode 100644 index 0000000000..828307c5a7 --- /dev/null +++ b/src/Avalonia.Input/PointerMagnifyGestureEventArgs.cs @@ -0,0 +1,19 @@ +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace Avalonia.Input +{ + public class PointerMagnifyGestureEventArgs : PointerEventArgs + { + public double Delta { get; set; } + + public PointerMagnifyGestureEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, + Point rootVisualPosition, ulong timestamp, + PointerPointProperties properties, KeyModifiers modifiers, double delta) + : base(InputElement.PointerMagnifyGestureEvent, source, pointer, rootVisual, rootVisualPosition, + timestamp, properties, modifiers) + { + Delta = delta; + } + } +} diff --git a/src/Avalonia.Input/PointerRotateGestureEventArgs.cs b/src/Avalonia.Input/PointerRotateGestureEventArgs.cs new file mode 100644 index 0000000000..4bf1ffaaaa --- /dev/null +++ b/src/Avalonia.Input/PointerRotateGestureEventArgs.cs @@ -0,0 +1,19 @@ +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace Avalonia.Input +{ + public class PointerRotateGestureEventArgs : PointerEventArgs + { + public double Delta { get; set; } + + public PointerRotateGestureEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, + Point rootVisualPosition, ulong timestamp, + PointerPointProperties properties, KeyModifiers modifiers, double delta) + : base(InputElement.PointerRotateGestureEvent, source, pointer, rootVisual, rootVisualPosition, + timestamp, properties, modifiers) + { + Delta = delta; + } + } +} diff --git a/src/Avalonia.Input/PointerSwipeGestureEventArgs.cs b/src/Avalonia.Input/PointerSwipeGestureEventArgs.cs new file mode 100644 index 0000000000..c4994c8504 --- /dev/null +++ b/src/Avalonia.Input/PointerSwipeGestureEventArgs.cs @@ -0,0 +1,19 @@ +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace Avalonia.Input +{ + public class PointerSwipeGestureEventArgs : PointerEventArgs + { + public Vector Delta { get; set; } + + public PointerSwipeGestureEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, + Point rootVisualPosition, ulong timestamp, + PointerPointProperties properties, KeyModifiers modifiers, Vector delta) + : base(InputElement.PointerSwipeGestureEvent, source, pointer, rootVisual, rootVisualPosition, + timestamp, properties, modifiers) + { + Delta = delta; + } + } +} diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index 62a1dd5d84..d6406121c7 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -21,7 +21,10 @@ namespace Avalonia.Input.Raw TouchBegin, TouchUpdate, TouchEnd, - TouchCancel + TouchCancel, + Magnify, + Rotate, + Swipe } /// diff --git a/src/Avalonia.Input/Raw/RawPointerGestureEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerGestureEventArgs.cs new file mode 100644 index 0000000000..5a6dbda9a7 --- /dev/null +++ b/src/Avalonia.Input/Raw/RawPointerGestureEventArgs.cs @@ -0,0 +1,19 @@ +namespace Avalonia.Input.Raw +{ + public class RawPointerGestureEventArgs : RawPointerEventArgs + { + public RawPointerGestureEventArgs( + IInputDevice device, + ulong timestamp, + IInputRoot root, + RawPointerEventType gestureType, + Point position, + Vector delta, RawInputModifiers inputModifiers) + : base(device, timestamp, root, gestureType, position, inputModifiers) + { + Delta = delta; + } + + public Vector Delta { get; private set; } + } +} diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 4a3baa2788..f1c42efb7f 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -306,11 +306,28 @@ namespace Avalonia.Native switch (type) { case AvnRawMouseEventType.Wheel: - Input?.Invoke(new RawMouseWheelEventArgs(_mouse, timeStamp, _inputRoot, point.ToAvaloniaPoint(), new Vector(delta.X, delta.Y), (RawInputModifiers)modifiers)); + Input?.Invoke(new RawMouseWheelEventArgs(_mouse, timeStamp, _inputRoot, + point.ToAvaloniaPoint(), new Vector(delta.X, delta.Y), (RawInputModifiers)modifiers)); + break; + + case AvnRawMouseEventType.Magnify: + Input?.Invoke(new RawPointerGestureEventArgs(_mouse, timeStamp, _inputRoot, RawPointerEventType.Magnify, + point.ToAvaloniaPoint(), new Vector(delta.X, delta.Y), (RawInputModifiers)modifiers)); + break; + + case AvnRawMouseEventType.Rotate: + Input?.Invoke(new RawPointerGestureEventArgs(_mouse, timeStamp, _inputRoot, RawPointerEventType.Rotate, + point.ToAvaloniaPoint(), new Vector(delta.X, delta.Y), (RawInputModifiers)modifiers)); + break; + + case AvnRawMouseEventType.Swipe: + Input?.Invoke(new RawPointerGestureEventArgs(_mouse, timeStamp, _inputRoot, RawPointerEventType.Swipe, + point.ToAvaloniaPoint(), new Vector(delta.X, delta.Y), (RawInputModifiers)modifiers)); break; default: - var e = new RawPointerEventArgs(_mouse, timeStamp, _inputRoot, (RawPointerEventType)type, point.ToAvaloniaPoint(), (RawInputModifiers)modifiers); + var e = new RawPointerEventArgs(_mouse, timeStamp, _inputRoot, (RawPointerEventType)type, + point.ToAvaloniaPoint(), (RawInputModifiers)modifiers); if(!ChromeHitTest(e)) { From 17ec020239ca4b737c60195b0e039ea98f7b7ca1 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Sat, 27 Nov 2021 16:51:04 +0300 Subject: [PATCH 03/36] [OSX] Trackpad Gestures - code refactoring --- src/Avalonia.Input/Gestures.cs | 12 +++ src/Avalonia.Input/InputElement.cs | 85 ++++++------------- src/Avalonia.Input/MouseDevice.cs | 6 +- ...PointerTouchPadGestureMagnifyEventArgs.cs} | 6 +- ... PointerTouchPadGestureRotateEventArgs.cs} | 6 +- ...> PointerTouchPadGestureSwipeEventArgs.cs} | 6 +- 6 files changed, 50 insertions(+), 71 deletions(-) rename src/Avalonia.Input/{PointerRotateGestureEventArgs.cs => PointerTouchPadGestureMagnifyEventArgs.cs} (55%) rename src/Avalonia.Input/{PointerMagnifyGestureEventArgs.cs => PointerTouchPadGestureRotateEventArgs.cs} (55%) rename src/Avalonia.Input/{PointerSwipeGestureEventArgs.cs => PointerTouchPadGestureSwipeEventArgs.cs} (55%) diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index 639b4ef117..f218bcb9ce 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -28,6 +28,18 @@ namespace Avalonia.Input public static readonly RoutedEvent ScrollGestureEndedEvent = RoutedEvent.Register( "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); + + public static readonly RoutedEvent PointerTouchPadGestureMagnifyEvent = + RoutedEvent.Register( + "PointerMagnifyGesture", RoutingStrategies.Bubble, typeof(Gestures)); + + public static readonly RoutedEvent PointerTouchPadGestureRotateEvent = + RoutedEvent.Register( + "PointerRotateGesture", RoutingStrategies.Bubble, typeof(Gestures)); + + public static readonly RoutedEvent PointerTouchPadGestureSwipeEvent = + RoutedEvent.Register( + "PointerSwipeGesture", RoutingStrategies.Bubble, typeof(Gestures)); #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. private static readonly WeakReference s_lastPress = new WeakReference(null); diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 88a5b0e971..af135e3e9f 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -186,40 +186,34 @@ namespace Avalonia.Input RoutedEvent.Register( "PointerWheelChanged", RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - + /// - /// Defines the event. + /// Defines the event. /// - public static readonly RoutedEvent PointerMagnifyGestureEvent = - RoutedEvent.Register( - "PointerMagnifyGesture", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - + public static readonly RoutedEvent TappedEvent = Gestures.TappedEvent; + /// - /// Defines the event. + /// Defines the event. /// - public static readonly RoutedEvent PointerRotateGestureEvent = - RoutedEvent.Register( - "PointerRotateGesture", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + public static readonly RoutedEvent DoubleTappedEvent = Gestures.DoubleTappedEvent; /// - /// Defines the event. + /// Defines the event. /// - public static readonly RoutedEvent PointerSwipeGestureEvent = - RoutedEvent.Register( - "PointerSwipeGesture", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - + public static readonly RoutedEvent PointerTouchPadGestureMagnifyEvent + = Gestures.PointerTouchPadGestureMagnifyEvent; + /// - /// Defines the event. + /// Defines the event. /// - public static readonly RoutedEvent TappedEvent = Gestures.TappedEvent; - + public static readonly RoutedEvent PointerTouchPadGestureRotateEvent + = Gestures.PointerTouchPadGestureRotateEvent; + /// - /// Defines the event. + /// Defines the event. /// - public static readonly RoutedEvent DoubleTappedEvent = Gestures.DoubleTappedEvent; + public static readonly RoutedEvent PointerTouchPadGestureSwipeEvent + = Gestures.PointerTouchPadGestureSwipeEvent; private bool _isEffectivelyEnabled = true; private bool _isFocused; @@ -247,9 +241,6 @@ namespace Avalonia.Input PointerReleasedEvent.AddClassHandler((x, e) => x.OnPointerReleased(e)); PointerCaptureLostEvent.AddClassHandler((x, e) => x.OnPointerCaptureLost(e)); PointerWheelChangedEvent.AddClassHandler((x, e) => x.OnPointerWheelChanged(e)); - PointerMagnifyGestureEvent.AddClassHandler((x, e) => x.OnPointerMagnifyGesture(e)); - PointerRotateGestureEvent.AddClassHandler((x, e) => x.OnPointerRotateGesture(e)); - PointerSwipeGestureEvent.AddClassHandler((x, e) => x.OnPointerSwipeGesture(e)); } public InputElement() @@ -388,30 +379,30 @@ namespace Avalonia.Input /// Occurs when the user uses magnify (Pitch to Zoom) gesture on a trackpad and pointer is over the control. /// Works only on macOS. /// - public event EventHandler PointerMagnifyGesture + public event EventHandler PointerTouchPadGestureMagnify { - add { AddHandler(PointerMagnifyGestureEvent, value); } - remove { RemoveHandler(PointerMagnifyGestureEvent, value); } + add { AddHandler(PointerTouchPadGestureMagnifyEvent, value); } + remove { RemoveHandler(PointerTouchPadGestureMagnifyEvent, value); } } /// /// Occurs when the user uses rotate gesture on a trackpad and pointer is over the control. /// Works only on macOS. /// - public event EventHandler PointerRotateGesture + public event EventHandler PointerTouchPadGestureRotate { - add { AddHandler(PointerRotateGestureEvent, value); } - remove { RemoveHandler(PointerRotateGestureEvent, value); } + add { AddHandler(PointerTouchPadGestureRotateEvent, value); } + remove { RemoveHandler(PointerTouchPadGestureRotateEvent, value); } } /// /// Occurs when the user uses swipe gesture on a trackpad and pointer is over the control. /// Works only on macOS. /// - public event EventHandler PointerSwipeGesture + public event EventHandler PointerTouchPadGestureSwipe { - add { AddHandler(PointerSwipeGestureEvent, value); } - remove { RemoveHandler(PointerSwipeGestureEvent, value); } + add { AddHandler(PointerTouchPadGestureSwipeEvent, value); } + remove { RemoveHandler(PointerTouchPadGestureSwipeEvent, value); } } /// @@ -674,30 +665,6 @@ namespace Avalonia.Input protected virtual void OnPointerWheelChanged(PointerWheelEventArgs e) { } - - /// - /// Called before the trackpad event occurs. - /// - /// The event args. - protected virtual void OnPointerMagnifyGesture(PointerMagnifyGestureEventArgs e) - { - } - - /// - /// Called before the trackpad event occurs. - /// - /// The event args. - protected virtual void OnPointerRotateGesture(PointerRotateGestureEventArgs e) - { - } - - /// - /// Called before the trackpad event occurs. - /// - /// The event args. - protected virtual void OnPointerSwipeGesture(PointerSwipeGestureEventArgs e) - { - } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index c919242c78..7197397e99 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -355,7 +355,7 @@ namespace Avalonia.Input if (hit != null) { var source = GetSource(hit); - var e = new PointerMagnifyGestureEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta.X); + var e = new PointerTouchPadGestureMagnifyEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta.X); source?.RaiseEvent(e); return e.Handled; @@ -376,7 +376,7 @@ namespace Avalonia.Input if (hit != null) { var source = GetSource(hit); - var e = new PointerRotateGestureEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta.X); + var e = new PointerTouchPadGestureRotateEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta.X); source?.RaiseEvent(e); return e.Handled; @@ -397,7 +397,7 @@ namespace Avalonia.Input if (hit != null) { var source = GetSource(hit); - var e = new PointerSwipeGestureEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta); + var e = new PointerTouchPadGestureSwipeEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta); source?.RaiseEvent(e); return e.Handled; diff --git a/src/Avalonia.Input/PointerRotateGestureEventArgs.cs b/src/Avalonia.Input/PointerTouchPadGestureMagnifyEventArgs.cs similarity index 55% rename from src/Avalonia.Input/PointerRotateGestureEventArgs.cs rename to src/Avalonia.Input/PointerTouchPadGestureMagnifyEventArgs.cs index 4bf1ffaaaa..d55d23d2c8 100644 --- a/src/Avalonia.Input/PointerRotateGestureEventArgs.cs +++ b/src/Avalonia.Input/PointerTouchPadGestureMagnifyEventArgs.cs @@ -3,14 +3,14 @@ using Avalonia.VisualTree; namespace Avalonia.Input { - public class PointerRotateGestureEventArgs : PointerEventArgs + public class PointerTouchPadGestureMagnifyEventArgs : PointerEventArgs { public double Delta { get; set; } - public PointerRotateGestureEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, + public PointerTouchPadGestureMagnifyEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers, double delta) - : base(InputElement.PointerRotateGestureEvent, source, pointer, rootVisual, rootVisualPosition, + : base(InputElement.PointerTouchPadGestureMagnifyEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers) { Delta = delta; diff --git a/src/Avalonia.Input/PointerMagnifyGestureEventArgs.cs b/src/Avalonia.Input/PointerTouchPadGestureRotateEventArgs.cs similarity index 55% rename from src/Avalonia.Input/PointerMagnifyGestureEventArgs.cs rename to src/Avalonia.Input/PointerTouchPadGestureRotateEventArgs.cs index 828307c5a7..8563cf6609 100644 --- a/src/Avalonia.Input/PointerMagnifyGestureEventArgs.cs +++ b/src/Avalonia.Input/PointerTouchPadGestureRotateEventArgs.cs @@ -3,14 +3,14 @@ using Avalonia.VisualTree; namespace Avalonia.Input { - public class PointerMagnifyGestureEventArgs : PointerEventArgs + public class PointerTouchPadGestureRotateEventArgs : PointerEventArgs { public double Delta { get; set; } - public PointerMagnifyGestureEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, + public PointerTouchPadGestureRotateEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers, double delta) - : base(InputElement.PointerMagnifyGestureEvent, source, pointer, rootVisual, rootVisualPosition, + : base(InputElement.PointerTouchPadGestureRotateEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers) { Delta = delta; diff --git a/src/Avalonia.Input/PointerSwipeGestureEventArgs.cs b/src/Avalonia.Input/PointerTouchPadGestureSwipeEventArgs.cs similarity index 55% rename from src/Avalonia.Input/PointerSwipeGestureEventArgs.cs rename to src/Avalonia.Input/PointerTouchPadGestureSwipeEventArgs.cs index c4994c8504..1d9c0d6189 100644 --- a/src/Avalonia.Input/PointerSwipeGestureEventArgs.cs +++ b/src/Avalonia.Input/PointerTouchPadGestureSwipeEventArgs.cs @@ -3,14 +3,14 @@ using Avalonia.VisualTree; namespace Avalonia.Input { - public class PointerSwipeGestureEventArgs : PointerEventArgs + public class PointerTouchPadGestureSwipeEventArgs : PointerEventArgs { public Vector Delta { get; set; } - public PointerSwipeGestureEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, + public PointerTouchPadGestureSwipeEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers, Vector delta) - : base(InputElement.PointerSwipeGestureEvent, source, pointer, rootVisual, rootVisualPosition, + : base(InputElement.PointerTouchPadGestureSwipeEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers) { Delta = delta; From 9275e599090bb3a3428411ef8c8c4107f2624fa6 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 22 Dec 2021 21:36:48 -0500 Subject: [PATCH 04/36] 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 05/36] 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 0cd4b90bc734ee714ab11daa0e6d9ea342b4134e Mon Sep 17 00:00:00 2001 From: Lubomir Tetak Date: Fri, 7 Jan 2022 09:11:59 +0100 Subject: [PATCH 06/36] DataGrid - handle OSX key modifiers (ctrl vs cmd) --- .../DataGridColumn.cs | 2 +- .../Utils/KeyboardHelper.cs | 21 ++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index 8501ce3896..a77b482436 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -680,7 +680,7 @@ namespace Avalonia.Controls public void ClearSort() { //InvokeProcessSort is already validating if sorting is possible - _headerCell?.InvokeProcessSort(Input.KeyModifiers.Control); + _headerCell?.InvokeProcessSort(KeyboardHelper.GetPlatformCtrlOrCmdKeyModifier()); } /// diff --git a/src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs b/src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs index 351deceb48..d2b1fd4b8e 100644 --- a/src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs +++ b/src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs @@ -4,22 +4,29 @@ // All other rights reserved. using Avalonia.Input; +using Avalonia.Input.Platform; namespace Avalonia.Controls.Utils { internal static class KeyboardHelper { - public static void GetMetaKeyState(KeyModifiers modifiers, out bool ctrl, out bool shift) + public static void GetMetaKeyState(KeyModifiers modifiers, out bool ctrlOrCmd, out bool shift) { - ctrl = (modifiers & KeyModifiers.Control) == KeyModifiers.Control; - shift = (modifiers & KeyModifiers.Shift) == KeyModifiers.Shift; + ctrlOrCmd = modifiers.HasFlag(GetPlatformCtrlOrCmdKeyModifier()); + shift = modifiers.HasFlag(KeyModifiers.Shift); } - public static void GetMetaKeyState(KeyModifiers modifiers, out bool ctrl, out bool shift, out bool alt) + public static void GetMetaKeyState(KeyModifiers modifiers, out bool ctrlOrCmd, out bool shift, out bool alt) { - ctrl = (modifiers & KeyModifiers.Control) == KeyModifiers.Control; - shift = (modifiers & KeyModifiers.Shift) == KeyModifiers.Shift; - alt = (modifiers & KeyModifiers.Alt) == KeyModifiers.Alt; + ctrlOrCmd = modifiers.HasFlag(GetPlatformCtrlOrCmdKeyModifier()); + shift = modifiers.HasFlag(KeyModifiers.Shift); + alt = modifiers.HasFlag(KeyModifiers.Alt); + } + + public static KeyModifiers GetPlatformCtrlOrCmdKeyModifier() + { + var keymap = AvaloniaLocator.Current.GetService(); + return keymap?.CommandModifiers ?? KeyModifiers.Control; } } } From 185591471797ba1aae99f1ae62c1b22c3c09e3d4 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 13 Jan 2022 06:58:20 +0100 Subject: [PATCH 07/36] Introduce HarfBuzz platform implementations for unit tests --- .../Avalonia.UnitTests.csproj | 1 + .../HarfBuzzFontManagerImpl.cs | 96 +++++++++++ .../HarfBuzzGlyphTypefaceImpl.cs | 158 ++++++++++++++++++ .../HarfBuzzTextShaperImpl.cs | 147 ++++++++++++++++ tests/Avalonia.UnitTests/TestServices.cs | 6 + .../Media/FontManagerTests.cs | 11 +- .../Media/GlyphRunTests.cs | 6 + 7 files changed, 424 insertions(+), 1 deletion(-) create mode 100644 tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs create mode 100644 tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs create mode 100644 tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj index d4abf9416a..f5e502bca8 100644 --- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj +++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj @@ -29,4 +29,5 @@ + diff --git a/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs new file mode 100644 index 0000000000..002da66070 --- /dev/null +++ b/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Avalonia.Media; +using Avalonia.Media.Fonts; +using Avalonia.Platform; + +namespace Avalonia.UnitTests +{ + public class HarfBuzzFontManagerImpl : IFontManagerImpl + { + private readonly Typeface[] _customTypefaces; + private readonly string _defaultFamilyName; + + private static readonly Typeface _defaultTypeface = + new Typeface("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono"); + private static readonly Typeface _italicTypeface = + new Typeface("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Sans"); + private static readonly Typeface _emojiTypeface = + new Typeface("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Twitter Color Emoji"); + + public HarfBuzzFontManagerImpl(string defaultFamilyName = "resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono") + { + _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface }; + _defaultFamilyName = defaultFamilyName; + } + + public string GetDefaultFontFamilyName() + { + return _defaultFamilyName; + } + + public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + { + return _customTypefaces.Select(x => x.FontFamily!.Name); + } + + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, + CultureInfo culture, out Typeface fontKey) + { + foreach (var customTypeface in _customTypefaces) + { + var glyphTypeface = customTypeface.GlyphTypeface; + + if (!glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + { + continue; + } + + fontKey = customTypeface; + + return true; + } + + fontKey = default; + + return false; + } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + var fontFamily = typeface.FontFamily; + + if (fontFamily == null) + { + return null; + } + + if (fontFamily.IsDefault) + { + fontFamily = _defaultTypeface.FontFamily; + } + + if (fontFamily!.Key == null) + { + return null; + } + + var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamily.Key); + + var asset = fontAssets.First(); + + var assetLoader = AvaloniaLocator.Current.GetService(); + + if (assetLoader == null) + { + throw new NotSupportedException("IAssetLoader is not registered."); + } + + var stream = assetLoader.Open(asset); + + return new HarfBuzzGlyphTypefaceImpl(stream); + } + } +} diff --git a/tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs new file mode 100644 index 0000000000..32e0434cd4 --- /dev/null +++ b/tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs @@ -0,0 +1,158 @@ +using System; +using System.IO; +using Avalonia.Platform; +using HarfBuzzSharp; + +namespace Avalonia.UnitTests +{ + public class HarfBuzzGlyphTypefaceImpl : IGlyphTypefaceImpl + { + private bool _isDisposed; + private Blob _blob; + + public HarfBuzzGlyphTypefaceImpl(Stream data, bool isFakeBold = false, bool isFakeItalic = false) + { + _blob = Blob.FromStream(data); + + Face = new Face(_blob, 0); + + Font = new Font(Face); + + Font.SetFunctionsOpenType(); + + Font.GetScale(out var scale, out _); + + DesignEmHeight = (short)scale; + + var metrics = Font.OpenTypeMetrics; + + const double defaultFontRenderingEmSize = 12.0; + + Ascent = (int)(metrics.GetXVariation(OpenTypeMetricsTag.HorizontalAscender) / defaultFontRenderingEmSize * DesignEmHeight); + + Descent = (int)(metrics.GetXVariation(OpenTypeMetricsTag.HorizontalDescender) / defaultFontRenderingEmSize * DesignEmHeight); + + LineGap = (int)(metrics.GetXVariation(OpenTypeMetricsTag.HorizontalLineGap) / defaultFontRenderingEmSize * DesignEmHeight); + + UnderlinePosition = (int)(metrics.GetXVariation(OpenTypeMetricsTag.UnderlineOffset) / defaultFontRenderingEmSize * DesignEmHeight); + + UnderlineThickness = (int)(metrics.GetXVariation(OpenTypeMetricsTag.UnderlineSize) / defaultFontRenderingEmSize * DesignEmHeight); + + StrikethroughPosition = (int)(metrics.GetXVariation(OpenTypeMetricsTag.StrikeoutOffset) / defaultFontRenderingEmSize * DesignEmHeight); + + StrikethroughThickness = (int)(metrics.GetXVariation(OpenTypeMetricsTag.StrikeoutSize) / defaultFontRenderingEmSize * DesignEmHeight); + + IsFixedPitch = GetGlyphAdvance(GetGlyph('a')) == GetGlyphAdvance(GetGlyph('b')); + + IsFakeBold = isFakeBold; + + IsFakeItalic = isFakeItalic; + } + + public Face Face { get; } + + public Font Font { get; } + + /// + public short DesignEmHeight { get; } + + /// + public int Ascent { get; } + + /// + public int Descent { get; } + + /// + public int LineGap { get; } + + /// + public int UnderlinePosition { get; } + + /// + public int UnderlineThickness { get; } + + /// + public int StrikethroughPosition { get; } + + /// + public int StrikethroughThickness { get; } + + /// + public bool IsFixedPitch { get; } + + public bool IsFakeBold { get; } + + public bool IsFakeItalic { get; } + + /// + public ushort GetGlyph(uint codepoint) + { + if (Font.TryGetGlyph(codepoint, out var glyph)) + { + return (ushort)glyph; + } + + return 0; + } + + /// + public ushort[] GetGlyphs(ReadOnlySpan codepoints) + { + var glyphs = new ushort[codepoints.Length]; + + for (var i = 0; i < codepoints.Length; i++) + { + if (Font.TryGetGlyph(codepoints[i], out var glyph)) + { + glyphs[i] = (ushort)glyph; + } + } + + return glyphs; + } + + /// + public int GetGlyphAdvance(ushort glyph) + { + return Font.GetHorizontalGlyphAdvance(glyph); + } + + /// + public int[] GetGlyphAdvances(ReadOnlySpan glyphs) + { + var glyphIndices = new uint[glyphs.Length]; + + for (var i = 0; i < glyphs.Length; i++) + { + glyphIndices[i] = glyphs[i]; + } + + return Font.GetHorizontalGlyphAdvances(glyphIndices); + } + + private void Dispose(bool disposing) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + + if (!disposing) + { + return; + } + + Font?.Dispose(); + Face?.Dispose(); + _blob?.Dispose(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs new file mode 100644 index 0000000000..687fddd71a --- /dev/null +++ b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs @@ -0,0 +1,147 @@ +using System; +using System.Globalization; +using Avalonia.Media; +using Avalonia.Media.TextFormatting.Unicode; +using Avalonia.Platform; +using Avalonia.Utilities; +using HarfBuzzSharp; +using Buffer = HarfBuzzSharp.Buffer; + +namespace Avalonia.UnitTests +{ + public class HarfBuzzTextShaperImpl : ITextShaperImpl + { + public GlyphRun ShapeText(ReadOnlySlice text, Typeface typeface, double fontRenderingEmSize, + CultureInfo culture) + { + using (var buffer = new Buffer()) + { + FillBuffer(buffer, text); + + buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); + + buffer.GuessSegmentProperties(); + + var glyphTypeface = typeface.GlyphTypeface; + + var font = ((HarfBuzzGlyphTypefaceImpl)glyphTypeface.PlatformImpl).Font; + + font.Shape(buffer); + + font.GetScale(out var scaleX, out _); + + var textScale = fontRenderingEmSize / scaleX; + + var bufferLength = buffer.Length; + + var glyphInfos = buffer.GetGlyphInfoSpan(); + + var glyphPositions = buffer.GetGlyphPositionSpan(); + + var glyphIndices = new ushort[bufferLength]; + + var clusters = new ushort[bufferLength]; + + double[] glyphAdvances = null; + + Vector[] glyphOffsets = null; + + for (var i = 0; i < bufferLength; i++) + { + glyphIndices[i] = (ushort)glyphInfos[i].Codepoint; + + clusters[i] = (ushort)glyphInfos[i].Cluster; + + if (!glyphTypeface.IsFixedPitch) + { + SetAdvance(glyphPositions, i, textScale, ref glyphAdvances); + } + + SetOffset(glyphPositions, i, textScale, ref glyphOffsets); + } + + return new GlyphRun(glyphTypeface, fontRenderingEmSize, + new ReadOnlySlice(glyphIndices), + new ReadOnlySlice(glyphAdvances), + new ReadOnlySlice(glyphOffsets), + text, + new ReadOnlySlice(clusters), + buffer.Direction == Direction.LeftToRight ? 0 : 1); + } + } + + private static void FillBuffer(Buffer buffer, ReadOnlySlice text) + { + buffer.ContentType = ContentType.Unicode; + + var i = 0; + + while (i < text.Length) + { + var codepoint = Codepoint.ReadAt(text, i, out var count); + + var cluster = (uint)(text.Start + i); + + if (codepoint.IsBreakChar) + { + if (i + 1 < text.Length) + { + var nextCodepoint = Codepoint.ReadAt(text, i + 1, out _); + + if (nextCodepoint == '\n' && codepoint == '\r') + { + count++; + + buffer.Add('\u200C', cluster); + + buffer.Add('\u200D', cluster); + } + else + { + buffer.Add('\u200C', cluster); + } + } + else + { + buffer.Add('\u200C', cluster); + } + } + else + { + buffer.Add(codepoint, cluster); + } + + i += count; + } + } + + private static void SetOffset(ReadOnlySpan glyphPositions, int index, double textScale, + ref Vector[] offsetBuffer) + { + var position = glyphPositions[index]; + + if (position.XOffset == 0 && position.YOffset == 0) + { + return; + } + + offsetBuffer ??= new Vector[glyphPositions.Length]; + + var offsetX = position.XOffset * textScale; + + var offsetY = position.YOffset * textScale; + + offsetBuffer[index] = new Vector(offsetX, offsetY); + } + + private static void SetAdvance(ReadOnlySpan glyphPositions, int index, double textScale, + ref double[] advanceBuffer) + { + advanceBuffer ??= new double[glyphPositions.Length]; + + // Depends on direction of layout + // advanceBuffer[index] = buffer.GlyphPositions[index].YAdvance * textScale; + advanceBuffer[index] = glyphPositions[index].XAdvance * textScale; + } + } +} diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 8d27562146..2b57a6db01 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -58,6 +58,12 @@ namespace Avalonia.UnitTests public static readonly TestServices RealStyler = new TestServices( styler: new Styler()); + public static readonly TestServices TextServices = new TestServices( + assetLoader: new AssetLoader(), + renderInterface: new MockPlatformRenderInterface(), + fontManagerImpl: new HarfBuzzFontManagerImpl(), + textShaperImpl: new HarfBuzzTextShaperImpl()); + public TestServices( IAssetLoader assetLoader = null, IFocusManager focusManager = null, diff --git a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs index 6e5b8eb637..a48639a426 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs @@ -48,7 +48,16 @@ namespace Avalonia.Visuals.UnitTests.Media [Fact] public void Should_Use_FontManagerOptions_FontFallback() { - var options = new FontManagerOptions { FontFallbacks = new[] { new FontFallback { FontFamily = new FontFamily("MyFont"), UnicodeRange = UnicodeRange.Default} } }; + var options = new FontManagerOptions + { + FontFallbacks = new[] + { + new FontFallback + { + FontFamily = new FontFamily("MyFont"), UnicodeRange = UnicodeRange.Default + } + } + }; using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface .With(fontManagerImpl: new MockFontManagerImpl()))) diff --git a/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs index 58feb4714a..f52bdc39c8 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs @@ -22,6 +22,7 @@ namespace Avalonia.Visuals.UnitTests.Media [Theory] public void Should_Get_Distance_From_CharacterHit(double[] advances, ushort[] clusters, int start, int trailingLength, double expectedDistance) { + using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) using (var glyphRun = CreateGlyphRun(advances, clusters)) { var characterHit = new CharacterHit(start, trailingLength); @@ -40,6 +41,7 @@ namespace Avalonia.Visuals.UnitTests.Media public void Should_Get_CharacterHit_FromDistance(double[] advances, ushort[] clusters, double distance, int start, int trailingLengthExpected, bool isInsideExpected) { + using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) using (var glyphRun = CreateGlyphRun(advances, clusters)) { var textBounds = glyphRun.GetCharacterHitFromDistance(distance, out var isInside); @@ -63,6 +65,7 @@ namespace Avalonia.Visuals.UnitTests.Media public void Should_Find_Nearest_CharacterHit(double[] advances, ushort[] clusters, int bidiLevel, int index, int expectedIndex, int expectedLength, double expectedWidth) { + using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel)) { var textBounds = glyphRun.FindNearestCharacterHit(index, out var width); @@ -87,6 +90,7 @@ namespace Avalonia.Visuals.UnitTests.Media int nextIndex, int nextLength, int bidiLevel) { + using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel)) { var characterHit = glyphRun.GetNextCaretCharacterHit(new CharacterHit(currentIndex, currentLength)); @@ -109,6 +113,7 @@ namespace Avalonia.Visuals.UnitTests.Media int previousIndex, int previousLength, int bidiLevel) { + using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel)) { var characterHit = glyphRun.GetPreviousCaretCharacterHit(new CharacterHit(currentIndex, currentLength)); @@ -128,6 +133,7 @@ namespace Avalonia.Visuals.UnitTests.Media [Theory] public void Should_Find_Glyph_Index(double[] advances, ushort[] clusters, int bidiLevel) { + using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel)) { if (glyphRun.IsLeftToRight) From bb12a4c791d9cac66658097ebfe4c455f6174b0b Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Mon, 17 Jan 2022 12:00:10 +0200 Subject: [PATCH 08/36] Enable CompiledBindings for FluentTheme. --- src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml | 3 ++- .../Controls/CalendarDatePicker.xaml | 3 ++- src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml | 4 +++- .../Controls/DataValidationErrors.xaml | 8 +++++--- src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml | 3 ++- src/Avalonia.Themes.Fluent/Controls/Expander.xaml | 8 +++++--- src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml | 5 ++++- src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml | 3 ++- src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml | 8 +++++--- src/Avalonia.Themes.Fluent/Controls/TabItem.xaml | 3 ++- src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml | 3 ++- src/Avalonia.Themes.Fluent/Controls/TextBox.xaml | 2 +- src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml | 3 ++- src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml | 3 ++- .../Controls/WindowNotificationManager.xaml | 4 +++- 15 files changed, 42 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml index f2344ab380..836cc27db3 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml @@ -1,7 +1,8 @@ + xmlns:converters="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls" + x:CompileBindings="True"> diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml index 26c3bbc19f..ffd3972b66 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml @@ -7,7 +7,8 @@ + xmlns:sys="clr-namespace:System;assembly=netstandard" + x:CompileBindings="True"> + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:CompileBindings="True" + x:DataType="CalendarItem"> diff --git a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml index d2fab37206..12e148d2f9 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml @@ -1,6 +1,8 @@ + xmlns:sys="using:System" + x:CompileBindings="True" + x:DataType="DataValidationErrors"> @@ -27,7 +29,7 @@ diff --git a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml index 3320fc9a41..9aa73fc52e 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml @@ -7,7 +7,8 @@ + xmlns:sys="clr-namespace:System;assembly=netstandard" + x:CompileBindings="True"> 40 1 diff --git a/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml b/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml index debdfb2772..2d18be91cb 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml @@ -1,6 +1,7 @@ + xmlns:sys="clr-namespace:System;assembly=netstandard" + x:CompileBindings="True"> + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:DataType="WindowNotificationManager" + x:CompileBindings="True"> - \ No newline at end of file + From 36d6222c75048cf552b70ae10a7c883dc74fd650 Mon Sep 17 00:00:00 2001 From: Thehx Date: Wed, 19 Jan 2022 20:19:41 +0300 Subject: [PATCH 12/36] Fixed and exposed PreciseArcTo for ellipses with extreme width:height ratios --- .../Media/PreciseEllipticArcHelper.cs} | 45 ++++++++++---- .../Media/StreamGeometryContext.cs | 24 ++++++++ .../RenderHelpers/RenderHelpers.projitems | 1 - .../Media/StreamGeometryTests.cs | 56 ++++++++++++++++++ ...cs_In_All_Directions.deferred.expected.png | Bin 0 -> 6332 bytes ...s_In_All_Directions.immediate.expected.png | Bin 0 -> 6332 bytes ...cs_In_All_Directions.deferred.expected.png | Bin 0 -> 5828 bytes ...s_In_All_Directions.immediate.expected.png | Bin 0 -> 5828 bytes 8 files changed, 112 insertions(+), 14 deletions(-) rename src/{Shared/RenderHelpers/ArcToHelper.cs => Avalonia.Visuals/Media/PreciseEllipticArcHelper.cs} (97%) create mode 100644 tests/Avalonia.RenderTests/Media/StreamGeometryTests.cs create mode 100644 tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png create mode 100644 tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png create mode 100644 tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png create mode 100644 tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png diff --git a/src/Shared/RenderHelpers/ArcToHelper.cs b/src/Avalonia.Visuals/Media/PreciseEllipticArcHelper.cs similarity index 97% rename from src/Shared/RenderHelpers/ArcToHelper.cs rename to src/Avalonia.Visuals/Media/PreciseEllipticArcHelper.cs index 0bbf451970..5dd647e8ca 100644 --- a/src/Shared/RenderHelpers/ArcToHelper.cs +++ b/src/Avalonia.Visuals/Media/PreciseEllipticArcHelper.cs @@ -1,5 +1,6 @@ // Copyright © 2003-2004, Luc Maisonobe // 2015 - Alexey Rozanov - Adaptations for Avalonia and oval center computations +// 2022 - Alexey Rozanov - Fix for arcs sometimes drawn in inverted order. // All rights reserved. // // Redistribution and use in source and binary forms, with @@ -49,12 +50,10 @@ // Adapted from http://www.spaceroots.org/documents/ellipse/EllipticalArc.java using System; -using Avalonia.Media; -using Avalonia.Platform; -namespace Avalonia.RenderHelpers +namespace Avalonia.Media { - static class ArcToHelper + static class PreciseEllipticArcHelper { /// /// This class represents an elliptical arc on a 2D plane. @@ -292,6 +291,8 @@ namespace Avalonia.RenderHelpers /// internal double G2; + public bool DrawInOppositeDirection { get; set; } + /// /// Builds an elliptical arc composed of the full unit circle around (0,0) /// @@ -850,7 +851,7 @@ namespace Avalonia.RenderHelpers /// Builds the arc outline using given StreamGeometryContext and default (max) Bezier curve degree and acceptable error of half a pixel (0.5) /// /// A StreamGeometryContext to output the path commands to - public void BuildArc(IStreamGeometryContextImpl path) + public void BuildArc(StreamGeometryContext path) { BuildArc(path, _maxDegree, _defaultFlatness, true); } @@ -862,7 +863,7 @@ namespace Avalonia.RenderHelpers /// degree of the Bezier curve to use /// acceptable error /// if true, a new figure will be started in the specified StreamGeometryContext - public void BuildArc(IStreamGeometryContextImpl path, int degree, double threshold, bool openNewFigure) + public void BuildArc(StreamGeometryContext path, int degree, double threshold, bool openNewFigure) { if (degree < 1 || degree > _maxDegree) throw new ArgumentException($"degree should be between {1} and {_maxDegree}", nameof(degree)); @@ -888,8 +889,18 @@ namespace Avalonia.RenderHelpers } n = n << 1; } - dEta = (Eta2 - Eta1) / n; - etaB = Eta1; + if (!DrawInOppositeDirection) + { + dEta = (Eta2 - Eta1) / n; + etaB = Eta1; + } + else + { + dEta = (Eta1 - Eta2) / n; + etaB = Eta2; + } + + double cosEtaB = Math.Cos(etaB); double sinEtaB = Math.Sin(etaB); double aCosEtaB = A * cosEtaB; @@ -922,6 +933,7 @@ namespace Avalonia.RenderHelpers */ //otherwise we're supposed to be already at the (xB,yB) + double t = Math.Tan(0.5 * dEta); double alpha = Math.Sin(dEta) * (Math.Sqrt(4 + 3 * t * t) - 1) / 3; @@ -1012,7 +1024,7 @@ namespace Avalonia.RenderHelpers /// Ellipse theta (angle measured from the abscissa) /// Large Arc Indicator /// Clockwise direction flag - public static void BuildArc(IStreamGeometryContextImpl path, Point p1, Point p2, Size size, double theta, bool isLargeArc, bool clockwise) + public static void BuildArc(StreamGeometryContext path, Point p1, Point p2, Size size, double theta, bool isLargeArc, bool clockwise) { // var orthogonalizer = new RotateTransform(-theta); @@ -1058,7 +1070,7 @@ namespace Avalonia.RenderHelpers } - double multiplier = Math.Sqrt(numerator / denominator); + double multiplier = Math.Sqrt(Math.Abs(numerator / denominator)); Point mulVec = new Point(rx * p1S.Y / ry, -ry * p1S.X / rx); int sign = (clockwise != isLargeArc) ? 1 : -1; @@ -1104,9 +1116,16 @@ namespace Avalonia.RenderHelpers // path.LineTo(c, true, true); // path.LineTo(clockwise ? p1 : p2, true,true); - path.LineTo(clockwise ? p1 : p2); var arc = new EllipticalArc(c.X, c.Y, rx, ry, theta, thetaStart, thetaEnd, false); + + double ManhattanDistance(Point p1, Point p2) => Math.Abs(p1.X - p2.X) + Math.Abs(p1.Y - p2.Y); + if (ManhattanDistance(p2, new Point(arc.X2, arc.Y2)) > ManhattanDistance(p2, new Point(arc.X1, arc.Y1))) + { + arc.DrawInOppositeDirection = true; + } + arc.BuildArc(path, arc._maxDegree, arc._defaultFlatness, false); + //path.LineTo(p2); //uncomment this to draw a pie //path.LineTo(c, true, true); @@ -1136,9 +1155,9 @@ namespace Avalonia.RenderHelpers } } - public static void ArcTo(IStreamGeometryContextImpl streamGeometryContextImpl, Point currentPoint, Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) + public static void ArcTo(StreamGeometryContext streamGeometryContextImpl, Point currentPoint, Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) { - EllipticalArc.BuildArc(streamGeometryContextImpl, currentPoint, point, size, rotationAngle*Math.PI/180, + EllipticalArc.BuildArc(streamGeometryContextImpl, currentPoint, point, size, rotationAngle*(Math.PI/180), isLargeArc, sweepDirection == SweepDirection.Clockwise); } diff --git a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs index 0bfd774c79..88aba8365e 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs @@ -15,6 +15,8 @@ namespace Avalonia.Media { private readonly IStreamGeometryContextImpl _impl; + private Point _currentPoint; + /// /// Initializes a new instance of the class. /// @@ -47,6 +49,24 @@ namespace Avalonia.Media public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) { _impl.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection); + _currentPoint = point; + } + + + /// + /// Draws an arc to the specified point using polylines, quadratic or cubic Bezier curves + /// Significantly more precise when drawing elliptic arcs with extreme width:height ratios. + /// + /// The destination point. + /// The radii of an oval whose perimeter is used to draw the angle. + /// The rotation angle of the oval that specifies the curve. + /// true to draw the arc greater than 180 degrees; otherwise, false. + /// + /// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction. + /// + public void PreciseArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) + { + PreciseEllipticArcHelper.ArcTo(this, _currentPoint, point, size, rotationAngle, isLargeArc, sweepDirection); } /// @@ -57,6 +77,7 @@ namespace Avalonia.Media public void BeginFigure(Point startPoint, bool isFilled) { _impl.BeginFigure(startPoint, isFilled); + _currentPoint = startPoint; } /// @@ -68,6 +89,7 @@ namespace Avalonia.Media public void CubicBezierTo(Point point1, Point point2, Point point3) { _impl.CubicBezierTo(point1, point2, point3); + _currentPoint = point3; } /// @@ -78,6 +100,7 @@ namespace Avalonia.Media public void QuadraticBezierTo(Point control, Point endPoint) { _impl.QuadraticBezierTo(control, endPoint); + _currentPoint = endPoint; } /// @@ -87,6 +110,7 @@ namespace Avalonia.Media public void LineTo(Point point) { _impl.LineTo(point); + _currentPoint = point; } /// diff --git a/src/Shared/RenderHelpers/RenderHelpers.projitems b/src/Shared/RenderHelpers/RenderHelpers.projitems index c088097a9f..4c80ec50c4 100644 --- a/src/Shared/RenderHelpers/RenderHelpers.projitems +++ b/src/Shared/RenderHelpers/RenderHelpers.projitems @@ -9,7 +9,6 @@ Avalonia.RenderHelpers - \ No newline at end of file diff --git a/tests/Avalonia.RenderTests/Media/StreamGeometryTests.cs b/tests/Avalonia.RenderTests/Media/StreamGeometryTests.cs new file mode 100644 index 0000000000..fc9cbf6a7f --- /dev/null +++ b/tests/Avalonia.RenderTests/Media/StreamGeometryTests.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Xunit; + +#if AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else +namespace Avalonia.Direct2D1.RenderTests.Media +#endif +{ + public class StreamGeometryTests : TestBase + { + public StreamGeometryTests() + : base(@"Media\StreamGeometry") + { + } + + [Fact] + public async Task PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions() + { + var grid = new Avalonia.Controls.Primitives.UniformGrid() { Columns = 2, Rows = 4, Width = 320, Height = 400 }; + foreach (var sweepDirection in new[] { SweepDirection.Clockwise, SweepDirection.CounterClockwise }) + foreach (var isLargeArc in new[] { false, true }) + foreach (var isPrecise in new[] { false, true }) + { + Point Pt(double x, double y) => new Point(x, y); + Size Sz(double w, double h) => new Size(w, h); + var streamGeometry = new StreamGeometry(); + using (var context = streamGeometry.Open()) + { + context.BeginFigure(Pt(20, 20), true); + + if(isPrecise) + context.PreciseArcTo(Pt(40, 40), Sz(20, 20), 0, isLargeArc, sweepDirection); + else + context.ArcTo(Pt(40, 40), Sz(20, 20), 0, isLargeArc, sweepDirection); + context.LineTo(Pt(40, 20)); + context.LineTo(Pt(20, 20)); + context.EndFigure(true); + } + var pathShape = new Avalonia.Controls.Shapes.Path(); + pathShape.Data = streamGeometry; + pathShape.Stroke = new SolidColorBrush(Colors.CornflowerBlue); + pathShape.Fill = new SolidColorBrush(Colors.Gold); + pathShape.StrokeThickness = 2; + pathShape.Margin = new Thickness(20); + grid.Children.Add(pathShape); + } + await RenderToFile(grid); + } + } +} diff --git a/tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png b/tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..825b1d7ea7d2d1356f2798c162fed7572f8eb27b GIT binary patch literal 6332 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 H Date: Thu, 20 Jan 2022 10:23:35 +0000 Subject: [PATCH 13/36] Fix ClassicDesktop Lifetime so that ShutdownRequested event is raised even with programatic calls to Shutdown. --- .../ClassicDesktopStyleApplicationLifetime.cs | 71 +++++++++++-------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 3a2fd68af5..536e39cc34 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -83,29 +83,9 @@ namespace Avalonia.Controls.ApplicationLifetimes public void Shutdown(int exitCode = 0) { - if (_isShuttingDown) - throw new InvalidOperationException("Application is already shutting down."); - - _exitCode = exitCode; - _isShuttingDown = true; - - try - { - foreach (var w in Windows) - w.Close(); - var e = new ControlledApplicationLifetimeExitEventArgs(exitCode); - Exit?.Invoke(this, e); - _exitCode = e.ApplicationExitCode; - } - finally - { - _cts?.Cancel(); - _cts = null; - _isShuttingDown = false; - } + DoShutdown(new ShutdownRequestedEventArgs(), exitCode); } - public int Start(string[] args) { Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args)); @@ -145,23 +125,52 @@ namespace Avalonia.Controls.ApplicationLifetimes if (_activeLifetime == this) _activeLifetime = null; } - - private void OnShutdownRequested(object sender, ShutdownRequestedEventArgs e) + + private void DoShutdown(ShutdownRequestedEventArgs e, int exitCode = 0) { ShutdownRequested?.Invoke(this, e); if (e.Cancel) return; - // When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel - // shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their - // owners. - foreach (var w in Windows) - if (w.Owner is null) - w.Close(); - if (Windows.Count > 0) - e.Cancel = true; + if (_isShuttingDown) + throw new InvalidOperationException("Application is already shutting down."); + + _exitCode = exitCode; + _isShuttingDown = true; + + try + { + // When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel + // shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their + // owners. + foreach (var w in Windows) + { + if (w.Owner is null) + { + w.Close(); + } + } + + if (Windows.Count > 0) + { + e.Cancel = true; + return; + } + + var e = new ControlledApplicationLifetimeExitEventArgs(exitCode); + Exit?.Invoke(this, e); + _exitCode = e.ApplicationExitCode; + } + finally + { + _cts?.Cancel(); + _cts = null; + _isShuttingDown = false; + } } + + private void OnShutdownRequested(object sender, ShutdownRequestedEventArgs e) => DoShutdown(e); } public class ClassicDesktopStyleApplicationLifetimeOptions From aeee9d165fb8e7414bfe1b21f261639ad7b01b2c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 20 Jan 2022 10:35:07 +0000 Subject: [PATCH 14/36] add TryShutdown method. --- .../ClassicDesktopStyleApplicationLifetime.cs | 15 +++++++++++---- .../IClassicDesktopStyleApplicationLifetime.cs | 6 ++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 536e39cc34..8006149ee9 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -83,7 +83,12 @@ namespace Avalonia.Controls.ApplicationLifetimes public void Shutdown(int exitCode = 0) { - DoShutdown(new ShutdownRequestedEventArgs(), exitCode); + DoShutdown(new ShutdownRequestedEventArgs(), true, exitCode); + } + + public bool TryShutdown(int exitCode = 0) + { + return DoShutdown(new ShutdownRequestedEventArgs(), false, exitCode); } public int Start(string[] args) @@ -126,12 +131,12 @@ namespace Avalonia.Controls.ApplicationLifetimes _activeLifetime = null; } - private void DoShutdown(ShutdownRequestedEventArgs e, int exitCode = 0) + private bool DoShutdown(ShutdownRequestedEventArgs e, bool force = false, int exitCode = 0) { ShutdownRequested?.Invoke(this, e); if (e.Cancel) - return; + false; if (_isShuttingDown) throw new InvalidOperationException("Application is already shutting down."); @@ -155,7 +160,7 @@ namespace Avalonia.Controls.ApplicationLifetimes if (Windows.Count > 0) { e.Cancel = true; - return; + return false; } var e = new ControlledApplicationLifetimeExitEventArgs(exitCode); @@ -168,6 +173,8 @@ namespace Avalonia.Controls.ApplicationLifetimes _cts = null; _isShuttingDown = false; } + + return true; } private void OnShutdownRequested(object sender, ShutdownRequestedEventArgs e) => DoShutdown(e); diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs index a70d5dd2f1..a83229b732 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs @@ -9,6 +9,12 @@ namespace Avalonia.Controls.ApplicationLifetimes /// public interface IClassicDesktopStyleApplicationLifetime : IControlledApplicationLifetime { + /// + /// Tries to Shutdown the application. event can be used to cancel the shutdown. + /// + /// An integer exit code for an application. The default exit code is 0. + bool TryShutdown(int exitCode = 0); + /// /// Gets the arguments passed to the /// From 15def96af4bca61bffd415cd4af76223f35d3c0f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 20 Jan 2022 10:37:38 +0000 Subject: [PATCH 15/36] fix quit menu item osx. --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index de6ba30a85..09d7247527 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -133,9 +133,9 @@ namespace Avalonia.Native var quitItem = new NativeMenuItem("Quit") { Gesture = new KeyGesture(Key.Q, KeyModifiers.Meta) }; quitItem.Click += (_, _) => { - if (Application.Current is { ApplicationLifetime: IControlledApplicationLifetime lifetime }) + if (Application.Current is { ApplicationLifetime: IClassicDesktopStyleApplicationLifetime lifetime }) { - lifetime.Shutdown(); + lifetime.TryShutdown(); } }; From 2a4a1b28b1a65315caf42d8fec5e0b6e85cf8246 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 20 Jan 2022 10:38:11 +0000 Subject: [PATCH 16/36] add conditional force shutdown. --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 09d7247527..67a27d5500 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -137,6 +137,10 @@ namespace Avalonia.Native { lifetime.TryShutdown(); } + else + { + lifetime.Shutdown(); + } }; appMenu.Add(quitItem); From aae3b701807b44398b1a2dff180c62af3eabd6f4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 20 Jan 2022 12:02:01 +0000 Subject: [PATCH 17/36] fix build. --- src/Avalonia.Controls/ApiCompatBaseline.txt | 3 +- .../ClassicDesktopStyleApplicationLifetime.cs | 28 +++++++++++-------- .../AvaloniaNativeMenuExporter.cs | 4 +-- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 9b7d37e108..a7560c37f2 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -36,6 +36,7 @@ CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.WindowBase' does not i InterfacesShouldHaveSameMembers : Interface member 'public System.EventHandler Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.ShutdownRequested' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.add_ShutdownRequested(System.EventHandler)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.TryShutdown(System.Int32)' is present in the implementation but not in the contract. CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Embedding.EmbeddableControlRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. MembersMustExist : Member 'public System.Action Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action)' does not exist in the implementation but it does exist in the contract. @@ -62,4 +63,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract. -Total Issues: 63 +Total Issues: 64 diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 8006149ee9..9d30c529fb 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -99,7 +99,10 @@ namespace Avalonia.Controls.ApplicationLifetimes if(options != null && options.ProcessUrlActivationCommandLine && args.Length > 0) { - ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args); + if (Application.Current is IApplicationPlatformEvents events) + { + events.RaiseUrlsOpened(args); + } } var lifetimeEvents = AvaloniaLocator.Current.GetService(); @@ -133,14 +136,17 @@ namespace Avalonia.Controls.ApplicationLifetimes private bool DoShutdown(ShutdownRequestedEventArgs e, bool force = false, int exitCode = 0) { - ShutdownRequested?.Invoke(this, e); + if (!force) + { + ShutdownRequested?.Invoke(this, e); + + if (e.Cancel) + return false; - if (e.Cancel) - false; + if (_isShuttingDown) + throw new InvalidOperationException("Application is already shutting down."); + } - if (_isShuttingDown) - throw new InvalidOperationException("Application is already shutting down."); - _exitCode = exitCode; _isShuttingDown = true; @@ -157,15 +163,15 @@ namespace Avalonia.Controls.ApplicationLifetimes } } - if (Windows.Count > 0) + if (!force && Windows.Count > 0) { e.Cancel = true; return false; } - var e = new ControlledApplicationLifetimeExitEventArgs(exitCode); - Exit?.Invoke(this, e); - _exitCode = e.ApplicationExitCode; + var args = new ControlledApplicationLifetimeExitEventArgs(exitCode); + Exit?.Invoke(this, args); + _exitCode = args.ApplicationExitCode; } finally { diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 67a27d5500..d8753efe25 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -137,9 +137,9 @@ namespace Avalonia.Native { lifetime.TryShutdown(); } - else + else if(Application.Current is {ApplicationLifetime: IControlledApplicationLifetime controlledLifetime}) { - lifetime.Shutdown(); + controlledLifetime.Shutdown(); } }; From ed35eeeb69ef807628944624d4e6ede99620ddbc Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 20 Jan 2022 12:06:59 +0000 Subject: [PATCH 18/36] allow MainWindow close mode and LastWindowClose mode to be cancellable. --- .../ClassicDesktopStyleApplicationLifetime.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 9d30c529fb..edddf31d45 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -76,9 +76,9 @@ namespace Avalonia.Controls.ApplicationLifetimes return; if (ShutdownMode == ShutdownMode.OnLastWindowClose && _windows.Count == 0) - Shutdown(); - else if (ShutdownMode == ShutdownMode.OnMainWindowClose && window == MainWindow) - Shutdown(); + TryShutdown(); + else if (ShutdownMode == ShutdownMode.OnMainWindowClose && ReferenceEquals(window, MainWindow)) + TryShutdown(); } public void Shutdown(int exitCode = 0) From 00c633ab3d01c1f7b270a42d3d8ac044fc30e29f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 20 Jan 2022 12:20:04 +0000 Subject: [PATCH 19/36] Add a unit test to show we can now cancel window closing shutdown modes. --- .../DesktopStyleApplicationLifetimeTests.cs | 78 ++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs index f7a3bdea1c..6c68ab0249 100644 --- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform; -using Avalonia.Threading; using Avalonia.UnitTests; using Moq; using Xunit; @@ -238,5 +236,81 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new[] { window }, lifetime.Windows); } } + + [Fact] + public void MainWindow_Closed_Shutdown_Should_Be_Cancellable() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) + { + lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose; + + var hasExit = false; + + lifetime.Exit += (s, e) => hasExit = true; + + var mainWindow = new Window(); + + mainWindow.Show(); + + lifetime.MainWindow = mainWindow; + + var window = new Window(); + + window.Show(); + + var raised = 0; + + lifetime.ShutdownRequested += (s, e) => + { + e.Cancel = true; + ++raised; + }; + + mainWindow.Close(); + + Assert.Equal(1, raised); + Assert.False(hasExit); + } + } + + [Fact] + public void LastWindow_Closed_Shutdown_Should_Be_Cancellable() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) + { + lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose; + + var hasExit = false; + + lifetime.Exit += (s, e) => hasExit = true; + + var windowA = new Window(); + + windowA.Show(); + + var windowB = new Window(); + + windowB.Show(); + + var raised = 0; + + lifetime.ShutdownRequested += (s, e) => + { + e.Cancel = true; + ++raised; + }; + + windowA.Close(); + + Assert.False(hasExit); + + windowB.Close(); + + Assert.Equal(1, raised); + Assert.False(hasExit); + } + } } } From 79fdf5ee5f70f08def6007ec303900d03bbef160 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 20 Jan 2022 12:31:46 +0000 Subject: [PATCH 20/36] Add another unit test to show that windows can cancel the shutdown. --- .../DesktopStyleApplicationLifetimeTests.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs index 6c68ab0249..4d7a640420 100644 --- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs @@ -312,5 +312,38 @@ namespace Avalonia.Controls.UnitTests Assert.False(hasExit); } } + + [Fact] + public void TryShutdown_Cancellable_By_Preventing_Window_Close() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) + { + var hasExit = false; + + lifetime.Exit += (s, e) => hasExit = true; + + var windowA = new Window(); + + windowA.Show(); + + var windowB = new Window(); + + windowB.Show(); + + var raised = 0; + + windowA.Closing += (sender, e) => + { + e.Cancel = true; + ++raised; + }; + + lifetime.TryShutdown(); + + Assert.Equal(1, raised); + Assert.False(hasExit); + } + } } } From 262f520335b0a5107cd09bcf597b7e419eca6be8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 20 Jan 2022 12:37:20 +0000 Subject: [PATCH 21/36] Add more unit tests. --- .../DesktopStyleApplicationLifetimeTests.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs index 4d7a640420..470e24aea7 100644 --- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs @@ -345,5 +345,62 @@ namespace Avalonia.Controls.UnitTests Assert.False(hasExit); } } + + [Fact] + public void Shutdown_NotCancellable_By_Preventing_Window_Close() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) + { + var hasExit = false; + + lifetime.Exit += (s, e) => hasExit = true; + + var windowA = new Window(); + + windowA.Show(); + + var windowB = new Window(); + + windowB.Show(); + + var raised = 0; + + windowA.Closing += (sender, e) => + { + e.Cancel = true; + ++raised; + }; + + lifetime.Shutdown(); + + Assert.Equal(1, raised); + Assert.True(hasExit); + } + } + + [Fact] + public void Shutdown_Doesnt_Raise_Shutdown_Requested() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) + { + var hasExit = false; + + lifetime.Exit += (s, e) => hasExit = true; + + var raised = 0; + + lifetime.ShutdownRequested += (sender, e) => + { + ++raised; + }; + + lifetime.Shutdown(); + + Assert.Equal(0, raised); + Assert.True(hasExit); + } + } } } From 245d23e741b71bd5b0d44a4c37af652af60f50d8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 20 Jan 2022 13:09:50 +0000 Subject: [PATCH 22/36] use discards where possible. --- .../DesktopStyleApplicationLifetimeTests.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs index 470e24aea7..3a2e1c08bd 100644 --- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs @@ -55,7 +55,7 @@ namespace Avalonia.Controls.UnitTests var hasExit = false; - lifetime.Exit += (s, e) => hasExit = true; + lifetime.Exit += (_, _) => hasExit = true; var windowA = new Window(); @@ -89,7 +89,7 @@ namespace Avalonia.Controls.UnitTests var hasExit = false; - lifetime.Exit += (s, e) => hasExit = true; + lifetime.Exit += (_, _) => hasExit = true; var mainWindow = new Window(); @@ -117,7 +117,7 @@ namespace Avalonia.Controls.UnitTests var hasExit = false; - lifetime.Exit += (s, e) => hasExit = true; + lifetime.Exit += (_, _) => hasExit = true; var windowA = new Window(); @@ -224,7 +224,7 @@ namespace Avalonia.Controls.UnitTests window.Show(); - lifetime.ShutdownRequested += (s, e) => + lifetime.ShutdownRequested += (_, e) => { e.Cancel = true; ++raised; @@ -247,7 +247,7 @@ namespace Avalonia.Controls.UnitTests var hasExit = false; - lifetime.Exit += (s, e) => hasExit = true; + lifetime.Exit += (_, _) => hasExit = true; var mainWindow = new Window(); @@ -261,7 +261,7 @@ namespace Avalonia.Controls.UnitTests var raised = 0; - lifetime.ShutdownRequested += (s, e) => + lifetime.ShutdownRequested += (_, e) => { e.Cancel = true; ++raised; @@ -284,7 +284,7 @@ namespace Avalonia.Controls.UnitTests var hasExit = false; - lifetime.Exit += (s, e) => hasExit = true; + lifetime.Exit += (_, _) => hasExit = true; var windowA = new Window(); @@ -296,7 +296,7 @@ namespace Avalonia.Controls.UnitTests var raised = 0; - lifetime.ShutdownRequested += (s, e) => + lifetime.ShutdownRequested += (_, e) => { e.Cancel = true; ++raised; @@ -321,7 +321,7 @@ namespace Avalonia.Controls.UnitTests { var hasExit = false; - lifetime.Exit += (s, e) => hasExit = true; + lifetime.Exit += (_, _) => hasExit = true; var windowA = new Window(); @@ -333,7 +333,7 @@ namespace Avalonia.Controls.UnitTests var raised = 0; - windowA.Closing += (sender, e) => + windowA.Closing += (_, e) => { e.Cancel = true; ++raised; @@ -354,7 +354,7 @@ namespace Avalonia.Controls.UnitTests { var hasExit = false; - lifetime.Exit += (s, e) => hasExit = true; + lifetime.Exit += (_, _) => hasExit = true; var windowA = new Window(); @@ -366,7 +366,7 @@ namespace Avalonia.Controls.UnitTests var raised = 0; - windowA.Closing += (sender, e) => + windowA.Closing += (_, e) => { e.Cancel = true; ++raised; @@ -387,11 +387,11 @@ namespace Avalonia.Controls.UnitTests { var hasExit = false; - lifetime.Exit += (s, e) => hasExit = true; + lifetime.Exit += (_, _) => hasExit = true; var raised = 0; - lifetime.ShutdownRequested += (sender, e) => + lifetime.ShutdownRequested += (_, _) => { ++raised; }; From 21a76d476538183a69fff4a7e12e62a7ea3dba30 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 20 Jan 2022 22:03:24 +0000 Subject: [PATCH 23/36] fix render target corruption detection. --- .../BlazorSkiaGpuRenderTarget.cs | 16 +--------------- src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs | 2 -- .../Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs | 2 -- 3 files changed, 1 insertion(+), 19 deletions(-) diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs b/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs index 5df844b11d..fa6a39f210 100644 --- a/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs +++ b/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs @@ -34,20 +34,6 @@ namespace Avalonia.Web.Blazor return new BlazorSkiaGpuRenderSession(_blazorSkiaSurface, _renderTarget); } - public bool IsCorrupted - { - get - { - if( _blazorSkiaSurface.IsDirty ) - { - _blazorSkiaSurface.IsDirty = false; - return true; - } - - var result = _size.Width != _renderTarget.Width || _size.Height != _renderTarget.Height; - - return result; - } - } + public bool IsCorrupted => _blazorSkiaSurface.Size != _size; } } diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs b/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs index 3e5bc8934d..512309cfe3 100644 --- a/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs +++ b/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs @@ -26,7 +26,5 @@ namespace Avalonia.Web.Blazor public double Scaling { get; set; } public SKHtmlCanvasInterop.GLInfo GlInfo { get; set; } - - public bool IsDirty { get; set; } } } diff --git a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs index 0158dc4f31..1d667c0f0c 100644 --- a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs @@ -51,8 +51,6 @@ namespace Avalonia.Web.Blazor if (_currentSurface is { }) { _currentSurface.Size = new PixelSize((int)size.Width, (int)size.Height); - - _currentSurface.IsDirty = true; } Resized?.Invoke(newSize, PlatformResizeReason.User); From efb366c83859d931b1cddb1a3317f89a9c9d35b3 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 20 Jan 2022 23:09:38 +0000 Subject: [PATCH 24/36] fix scaling and resizing. --- src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs index 1d667c0f0c..1942e2d0d4 100644 --- a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs @@ -44,6 +44,16 @@ namespace Avalonia.Web.Blazor { var newSize = new Size(size.Width, size.Height); + if (RenderScaling != dpi) + { + if (_currentSurface is { }) + { + _currentSurface.Scaling = dpi; + } + + ScalingChanged?.Invoke(dpi); + } + if (newSize != _clientSize) { _clientSize = newSize; @@ -53,7 +63,7 @@ namespace Avalonia.Web.Blazor _currentSurface.Size = new PixelSize((int)size.Width, (int)size.Height); } - Resized?.Invoke(newSize, PlatformResizeReason.User); + Resized?.Invoke(newSize * dpi, PlatformResizeReason.User); } } From 2b263544e50aa0c3fa7f1742716aa8ed3b54e2aa Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 20 Jan 2022 23:14:09 +0000 Subject: [PATCH 25/36] correctly resize. --- src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs index 1942e2d0d4..3cadbfaa60 100644 --- a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs @@ -44,7 +44,7 @@ namespace Avalonia.Web.Blazor { var newSize = new Size(size.Width, size.Height); - if (RenderScaling != dpi) + if (Math.Abs(RenderScaling - dpi) > 0.0001) { if (_currentSurface is { }) { @@ -63,7 +63,7 @@ namespace Avalonia.Web.Blazor _currentSurface.Size = new PixelSize((int)size.Width, (int)size.Height); } - Resized?.Invoke(newSize * dpi, PlatformResizeReason.User); + Resized?.Invoke(newSize, PlatformResizeReason.User); } } From 1c3f2436012c8c7cfc16b5906bec33df2a23f7a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 21 Jan 2022 10:29:32 +0100 Subject: [PATCH 26/36] Fix UseRenderTransformProperty --- src/Avalonia.Controls/LayoutTransformControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index 83ad2b3638..a8e15ee463 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -18,7 +18,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(LayoutTransform)); public static readonly StyledProperty UseRenderTransformProperty = - AvaloniaProperty.Register(nameof(LayoutTransform)); + AvaloniaProperty.Register(nameof(UseRenderTransform)); static LayoutTransformControl() { From 3908e4d71d0b56bdd458edabd0ba2d12ac510175 Mon Sep 17 00:00:00 2001 From: ingen084 Date: Sat, 22 Jan 2022 01:52:34 +0900 Subject: [PATCH 27/36] Fix text wrapping behavior (#7397) * Allow one-character runs line breaks * Remove unuseful ignored nullable * Prevent redundant allocations * Fix empty line handling * Update TextFormatterImpl.cs Co-authored-by: Benedikt Stebner Co-authored-by: Benedikt Stebner --- .../Media/TextFormatting/TextFormatterImpl.cs | 23 +++++++++++-------- .../Media/TextFormatting/TextLayout.cs | 10 ++++---- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index 7c6af4eaa7..c97e36d5ff 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -193,7 +193,7 @@ namespace Avalonia.Media.TextFormatting { var currentRun = textRuns[i]; - if (currentLength + currentRun.GlyphRun.Characters.Length < length) + if (currentLength + currentRun.GlyphRun.Characters.Length <= length) { currentLength += currentRun.GlyphRun.Characters.Length; continue; @@ -283,26 +283,26 @@ namespace Avalonia.Media.TextFormatting { var shapedCharacters = previousLineBreak.RemainingCharacters[index]; - if (shapedCharacters == null) - { - continue; - } - textRuns.Add(shapedCharacters); if (TryGetLineBreak(shapedCharacters, out var runLineBreak)) { var splitResult = SplitTextRuns(textRuns, currentLength + runLineBreak.PositionWrap); + if (splitResult.Second == null) + { + return splitResult.First; + } + if (++index < previousLineBreak.RemainingCharacters.Count) { for (; index < previousLineBreak.RemainingCharacters.Count; index++) { - splitResult.Second!.Add(previousLineBreak.RemainingCharacters[index]); + splitResult.Second.Add(previousLineBreak.RemainingCharacters[index]); } } - nextLineBreak = new TextLineBreak(splitResult.Second!); + nextLineBreak = new TextLineBreak(splitResult.Second); return splitResult.First; } @@ -346,7 +346,10 @@ namespace Avalonia.Media.TextFormatting { var splitResult = SplitTextRuns(textRuns, currentLength + runLineBreak.PositionWrap); - nextLineBreak = new TextLineBreak(splitResult.Second!); + if (splitResult.Second != null) + { + nextLineBreak = new TextLineBreak(splitResult.Second); + } return splitResult.First; } @@ -532,7 +535,7 @@ namespace Avalonia.Media.TextFormatting /// The text range that is covered by the text runs. private static TextRange GetTextRange(IReadOnlyList textRuns) { - if (textRuns is null || textRuns.Count == 0) + if (textRuns.Count == 0) { return new TextRange(); } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index 40e12c8e99..0ed06e4e57 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -401,14 +401,12 @@ namespace Avalonia.Media.TextFormatting previousLine = textLine; - if (currentPosition != _text.Length || textLine.TextLineBreak?.RemainingCharacters == null) + if (currentPosition == _text.Length && textLine.NewLineLength > 0) { - continue; - } + var emptyTextLine = CreateEmptyTextLine(currentPosition); - var emptyTextLine = CreateEmptyTextLine(currentPosition); - - textLines.Add(emptyTextLine); + textLines.Add(emptyTextLine); + } } Size = new Size(width, height); From a11b3848b23dcf082f73157ab2e3444b863cfeec Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 21 Jan 2022 17:21:42 +0000 Subject: [PATCH 28/36] support resizing canvas seperately. --- .../Interop/SKHtmlCanvasInterop.cs | 8 +++- .../Interop/Typescript/SKHtmlCanvas.ts | 45 +++++++++++++------ 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs index 4f5d4cdf70..9cbbf24086 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs +++ b/src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs @@ -11,6 +11,7 @@ namespace Avalonia.Web.Blazor.Interop private const string InitRasterSymbol = "SKHtmlCanvas.initRaster"; private const string DeinitSymbol = "SKHtmlCanvas.deinit"; private const string RequestAnimationFrameSymbol = "SKHtmlCanvas.requestAnimationFrame"; + private const string SetCanvasSizeSymbol = "SKHtmlCanvas.setCanvasSize"; private const string PutImageDataSymbol = "SKHtmlCanvas.putImageData"; private readonly ElementReference htmlCanvas; @@ -68,8 +69,11 @@ namespace Avalonia.Web.Blazor.Interop callbackReference?.Dispose(); } - public void RequestAnimationFrame(bool enableRenderLoop, int rawWidth, int rawHeight) => - Invoke(RequestAnimationFrameSymbol, htmlCanvas, enableRenderLoop, rawWidth, rawHeight); + public void RequestAnimationFrame(bool enableRenderLoop) => + Invoke(RequestAnimationFrameSymbol, htmlCanvas, enableRenderLoop); + + public void SetCanvasSize(int rawWidth, int rawHeight) => + Invoke(SetCanvasSizeSymbol, htmlCanvas, rawWidth, rawHeight); public void PutImageData(IntPtr intPtr, SKSizeI rawSize) => Invoke(PutImageDataSymbol, htmlCanvas, intPtr.ToInt64(), rawSize.Width, rawSize.Height); diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts index 147e2a963f..2139fb298e 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts +++ b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts @@ -25,6 +25,8 @@ export class SKHtmlCanvas { renderFrameCallback: DotNet.DotNetObjectReference; renderLoopEnabled: boolean = false; renderLoopRequest: number = 0; + newWidth: number; + newHeight: number; public static initGL(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): SKGLViewInfo { var view = SKHtmlCanvas.init(true, element, elementId, callback); @@ -75,13 +77,22 @@ export class SKHtmlCanvas { htmlCanvas.SKHtmlCanvas = undefined; } - public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean, width?: number, height?: number) { + public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean) { const htmlCanvas = element as SKHtmlCanvasElement; if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) return; - htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop, width, height); + htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop); } + + public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number) + { + const htmlCanvas = element as SKHtmlCanvasElement; + if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) + return; + + htmlCanvas.SKHtmlCanvas.setCanvasSize(width, height); + } public static setEnableRenderLoop(element: HTMLCanvasElement, enable: boolean) { const htmlCanvas = element as SKHtmlCanvasElement; @@ -128,28 +139,34 @@ export class SKHtmlCanvas { public deinit() { this.setEnableRenderLoop(false); } - - public requestAnimationFrame(renderLoop?: boolean, width?: number, height?: number) { + + public setCanvasSize(width: number, height: number) + { + this.newWidth = width; + this.newHeight = height; + } + + public requestAnimationFrame(renderLoop?: boolean) { // optionally update the render loop if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop) this.setEnableRenderLoop(renderLoop); - // make sure the canvas is scaled correctly for the drawing - if (width && height) { - this.htmlCanvas.width = width; - this.htmlCanvas.height = height; - } - // skip because we have a render loop if (this.renderLoopRequest !== 0) return; // add the draw to the next frame this.renderLoopRequest = window.requestAnimationFrame(() => { - if (this.glInfo) { - // make current - GL.makeContextCurrent(this.glInfo.context); - } + if (this.glInfo) { + // make current + GL.makeContextCurrent(this.glInfo.context); + } + + if(this.htmlCanvas.width != this.newWidth || this.htmlCanvas.height != this.newHeight) + { + this.htmlCanvas.width = this.newWidth; + this.htmlCanvas.height = this.newHeight; + } this.renderFrameCallback.invokeMethod('Invoke'); this.renderLoopRequest = 0; From 9be730d69d8a8ef8c8b64a55a0bf1bd5feabeab5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 21 Jan 2022 18:42:09 +0000 Subject: [PATCH 29/36] butter smooth resizing. --- .../Properties/AssemblyInfo.cs | 1 + .../Rendering/DeferredRenderer.cs | 2 +- .../Avalonia.Web.Blazor/AvaloniaView.razor.cs | 15 ++++++++++++- .../Interop/Typescript/SKHtmlCanvas.ts | 21 ++++++++++++++++++- 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs index 881ddfd89f..ebff097199 100644 --- a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs @@ -11,4 +11,5 @@ using Avalonia.Metadata; [assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] +[assembly: InternalsVisibleTo("Avalonia.Web.Blazor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 7b9c515b97..c453181f65 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -304,7 +304,7 @@ namespace Avalonia.Rendering } } - private void Render(bool forceComposite) + internal void Render(bool forceComposite) { using (var l = _lock.TryLock()) { diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index 7644514687..58bd46f254 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -3,6 +3,7 @@ using Avalonia.Controls.Embedding; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; +using Avalonia.Rendering; using Avalonia.Web.Blazor.Interop; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; @@ -327,6 +328,11 @@ namespace Avalonia.Web.Blazor _dpi = newDpi; _topLevelImpl.SetClientSize(_canvasSize, _dpi); + + if (_topLevel.Renderer is DeferredRenderer dr) + { + dr.Render(true); + } Invalidate(); } @@ -334,9 +340,16 @@ namespace Avalonia.Web.Blazor private void OnSizeChanged(SKSize newSize) { _canvasSize = newSize; + + _interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); + if (_topLevel.Renderer is DeferredRenderer dr) + { + dr.Render(true); + } + Invalidate(); } @@ -348,7 +361,7 @@ namespace Avalonia.Web.Blazor return; } - _interop.RequestAnimationFrame(true, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + _interop.RequestAnimationFrame(true); } public void SetActive(bool active) diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts index 2139fb298e..04d57a7756 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts +++ b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts @@ -144,6 +144,21 @@ export class SKHtmlCanvas { { this.newWidth = width; this.newHeight = height; + + if(this.htmlCanvas.width != this.newWidth) + { + this.htmlCanvas.width = this.newWidth; + } + + if(this.htmlCanvas.height != this.newHeight) + { + this.htmlCanvas.height = this.newHeight; + } + + if (this.glInfo) { + // make current + GL.makeContextCurrent(this.glInfo.context); + } } public requestAnimationFrame(renderLoop?: boolean) { @@ -162,9 +177,13 @@ export class SKHtmlCanvas { GL.makeContextCurrent(this.glInfo.context); } - if(this.htmlCanvas.width != this.newWidth || this.htmlCanvas.height != this.newHeight) + if(this.htmlCanvas.width != this.newWidth) { this.htmlCanvas.width = this.newWidth; + } + + if(this.htmlCanvas.height != this.newHeight) + { this.htmlCanvas.height = this.newHeight; } From 41aa6328db56cbc53ef80d0e6410165af4b8f2d8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 21 Jan 2022 18:50:01 +0000 Subject: [PATCH 30/36] force initial render. --- src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index 58bd46f254..5971bc4de0 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -300,6 +300,12 @@ namespace Avalonia.Web.Blazor _topLevel.Prepare(); _topLevel.Renderer.Start(); + + if (_topLevel.Renderer is DeferredRenderer dr) + { + dr.Render(true); + } + Invalidate(); }); } From b3bcfa128a8e77bf4634d0fd110c016ae73472e7 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 21 Jan 2022 20:15:02 -0500 Subject: [PATCH 31/36] Update src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs --- src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index 5971bc4de0..dc8b091563 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -301,6 +301,12 @@ namespace Avalonia.Web.Blazor _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); From 90c846007aad2a414a33bdf47208bd68ab081a1f Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sat, 22 Jan 2022 14:18:34 +0300 Subject: [PATCH 32/36] Requested Changes --- src/Avalonia.Input/Gestures.cs | 12 ++--- src/Avalonia.Input/InputElement.cs | 48 ------------------- src/Avalonia.Input/MouseDevice.cs | 18 +++---- src/Avalonia.Input/PointerDeltaEventArgs.cs | 19 ++++++++ .../PointerTouchPadGestureMagnifyEventArgs.cs | 19 -------- .../PointerTouchPadGestureRotateEventArgs.cs | 19 -------- .../PointerTouchPadGestureSwipeEventArgs.cs | 19 -------- 7 files changed, 34 insertions(+), 120 deletions(-) create mode 100644 src/Avalonia.Input/PointerDeltaEventArgs.cs delete mode 100644 src/Avalonia.Input/PointerTouchPadGestureMagnifyEventArgs.cs delete mode 100644 src/Avalonia.Input/PointerTouchPadGestureRotateEventArgs.cs delete mode 100644 src/Avalonia.Input/PointerTouchPadGestureSwipeEventArgs.cs diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index 6e7b74911d..86e6e96a71 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -30,16 +30,16 @@ namespace Avalonia.Input RoutedEvent.Register( "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); - public static readonly RoutedEvent PointerTouchPadGestureMagnifyEvent = - RoutedEvent.Register( + public static readonly RoutedEvent PointerTouchPadGestureMagnifyEvent = + RoutedEvent.Register( "PointerMagnifyGesture", RoutingStrategies.Bubble, typeof(Gestures)); - public static readonly RoutedEvent PointerTouchPadGestureRotateEvent = - RoutedEvent.Register( + public static readonly RoutedEvent PointerTouchPadGestureRotateEvent = + RoutedEvent.Register( "PointerRotateGesture", RoutingStrategies.Bubble, typeof(Gestures)); - public static readonly RoutedEvent PointerTouchPadGestureSwipeEvent = - RoutedEvent.Register( + public static readonly RoutedEvent PointerTouchPadGestureSwipeEvent = + RoutedEvent.Register( "PointerSwipeGesture", RoutingStrategies.Bubble, typeof(Gestures)); #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 6ac3a98b70..5aa1f1ff44 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -197,24 +197,6 @@ namespace Avalonia.Input /// public static readonly RoutedEvent DoubleTappedEvent = Gestures.DoubleTappedEvent; - /// - /// Defines the event. - /// - public static readonly RoutedEvent PointerTouchPadGestureMagnifyEvent - = Gestures.PointerTouchPadGestureMagnifyEvent; - - /// - /// Defines the event. - /// - public static readonly RoutedEvent PointerTouchPadGestureRotateEvent - = Gestures.PointerTouchPadGestureRotateEvent; - - /// - /// Defines the event. - /// - public static readonly RoutedEvent PointerTouchPadGestureSwipeEvent - = Gestures.PointerTouchPadGestureSwipeEvent; - private bool _isEffectivelyEnabled = true; private bool _isFocused; private bool _isKeyboardFocusWithin; @@ -375,36 +357,6 @@ namespace Avalonia.Input remove { RemoveHandler(PointerWheelChangedEvent, value); } } - /// - /// Occurs when the user uses magnify (Pitch to Zoom) gesture on a trackpad and pointer is over the control. - /// Works only on macOS. - /// - public event EventHandler PointerTouchPadGestureMagnify - { - add { AddHandler(PointerTouchPadGestureMagnifyEvent, value); } - remove { RemoveHandler(PointerTouchPadGestureMagnifyEvent, value); } - } - - /// - /// Occurs when the user uses rotate gesture on a trackpad and pointer is over the control. - /// Works only on macOS. - /// - public event EventHandler PointerTouchPadGestureRotate - { - add { AddHandler(PointerTouchPadGestureRotateEvent, value); } - remove { RemoveHandler(PointerTouchPadGestureRotateEvent, value); } - } - - /// - /// Occurs when the user uses swipe gesture on a trackpad and pointer is over the control. - /// Works only on macOS. - /// - public event EventHandler PointerTouchPadGestureSwipe - { - add { AddHandler(PointerTouchPadGestureSwipeEvent, value); } - remove { RemoveHandler(PointerTouchPadGestureSwipeEvent, value); } - } - /// /// Occurs when a tap gesture occurs on the control. /// diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index e856488b43..087a806f77 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -345,8 +345,7 @@ namespace Avalonia.Input } private bool GestureMagnify(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, - Vector delta, KeyModifiers inputModifiers) + PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) { device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); @@ -356,7 +355,8 @@ namespace Avalonia.Input if (hit != null) { var source = GetSource(hit); - var e = new PointerTouchPadGestureMagnifyEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta.X); + var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureMagnifyEvent, source, + _pointer, root, p, timestamp, props, inputModifiers, delta); source?.RaiseEvent(e); return e.Handled; @@ -366,8 +366,7 @@ namespace Avalonia.Input } private bool GestureRotate(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, - Vector delta, KeyModifiers inputModifiers) + PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) { device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); @@ -377,7 +376,8 @@ namespace Avalonia.Input if (hit != null) { var source = GetSource(hit); - var e = new PointerTouchPadGestureRotateEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta.X); + var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureRotateEvent, source, + _pointer, root, p, timestamp, props, inputModifiers, delta); source?.RaiseEvent(e); return e.Handled; @@ -387,8 +387,7 @@ namespace Avalonia.Input } private bool GestureSwipe(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, - Vector delta, KeyModifiers inputModifiers) + PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) { device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); @@ -398,7 +397,8 @@ namespace Avalonia.Input if (hit != null) { var source = GetSource(hit); - var e = new PointerTouchPadGestureSwipeEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta); + var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureSwipeEvent, source, + _pointer, root, p, timestamp, props, inputModifiers, delta); source?.RaiseEvent(e); return e.Handled; diff --git a/src/Avalonia.Input/PointerDeltaEventArgs.cs b/src/Avalonia.Input/PointerDeltaEventArgs.cs new file mode 100644 index 0000000000..b3085a038d --- /dev/null +++ b/src/Avalonia.Input/PointerDeltaEventArgs.cs @@ -0,0 +1,19 @@ +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace Avalonia.Input +{ + public class PointerDeltaEventArgs : PointerEventArgs + { + public Vector Delta { get; set; } + + public PointerDeltaEventArgs(RoutedEvent routedEvent, IInteractive? source, + IPointer pointer, IVisual rootVisual, Point rootVisualPosition, ulong timestamp, + PointerPointProperties properties, KeyModifiers modifiers, Vector delta) + : base(routedEvent, source, pointer, rootVisual, rootVisualPosition, + timestamp, properties, modifiers) + { + Delta = delta; + } + } +} diff --git a/src/Avalonia.Input/PointerTouchPadGestureMagnifyEventArgs.cs b/src/Avalonia.Input/PointerTouchPadGestureMagnifyEventArgs.cs deleted file mode 100644 index d55d23d2c8..0000000000 --- a/src/Avalonia.Input/PointerTouchPadGestureMagnifyEventArgs.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Avalonia.Interactivity; -using Avalonia.VisualTree; - -namespace Avalonia.Input -{ - public class PointerTouchPadGestureMagnifyEventArgs : PointerEventArgs - { - public double Delta { get; set; } - - public PointerTouchPadGestureMagnifyEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, - Point rootVisualPosition, ulong timestamp, - PointerPointProperties properties, KeyModifiers modifiers, double delta) - : base(InputElement.PointerTouchPadGestureMagnifyEvent, source, pointer, rootVisual, rootVisualPosition, - timestamp, properties, modifiers) - { - Delta = delta; - } - } -} diff --git a/src/Avalonia.Input/PointerTouchPadGestureRotateEventArgs.cs b/src/Avalonia.Input/PointerTouchPadGestureRotateEventArgs.cs deleted file mode 100644 index 8563cf6609..0000000000 --- a/src/Avalonia.Input/PointerTouchPadGestureRotateEventArgs.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Avalonia.Interactivity; -using Avalonia.VisualTree; - -namespace Avalonia.Input -{ - public class PointerTouchPadGestureRotateEventArgs : PointerEventArgs - { - public double Delta { get; set; } - - public PointerTouchPadGestureRotateEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, - Point rootVisualPosition, ulong timestamp, - PointerPointProperties properties, KeyModifiers modifiers, double delta) - : base(InputElement.PointerTouchPadGestureRotateEvent, source, pointer, rootVisual, rootVisualPosition, - timestamp, properties, modifiers) - { - Delta = delta; - } - } -} diff --git a/src/Avalonia.Input/PointerTouchPadGestureSwipeEventArgs.cs b/src/Avalonia.Input/PointerTouchPadGestureSwipeEventArgs.cs deleted file mode 100644 index 1d9c0d6189..0000000000 --- a/src/Avalonia.Input/PointerTouchPadGestureSwipeEventArgs.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Avalonia.Interactivity; -using Avalonia.VisualTree; - -namespace Avalonia.Input -{ - public class PointerTouchPadGestureSwipeEventArgs : PointerEventArgs - { - public Vector Delta { get; set; } - - public PointerTouchPadGestureSwipeEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, - Point rootVisualPosition, ulong timestamp, - PointerPointProperties properties, KeyModifiers modifiers, Vector delta) - : base(InputElement.PointerTouchPadGestureSwipeEvent, source, pointer, rootVisual, rootVisualPosition, - timestamp, properties, modifiers) - { - Delta = delta; - } - } -} From cff0c680ac7d51919c61e4c7323c7895ba896f63 Mon Sep 17 00:00:00 2001 From: ahopper Date: Sat, 22 Jan 2022 17:24:42 +0000 Subject: [PATCH 33/36] remove lock from X11Window --- src/Avalonia.X11/X11Window.cs | 60 ++++++++++++++--------------------- 1 file changed, 23 insertions(+), 37 deletions(-) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index d745b4765b..07469b7362 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reactive.Disposables; using System.Text; using System.Threading.Tasks; +using System.Threading; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives.PopupPositioning; @@ -50,8 +51,6 @@ namespace Avalonia.X11 private bool _disabled; private TransparencyHelper _transparencyHelper; - public object SyncRoot { get; } = new object(); - class InputEventContainer { public RawInputEventArgs Event; @@ -317,13 +316,8 @@ namespace Avalonia.X11 public double RenderScaling { - get - { - lock (SyncRoot) - return _scaling; - - } - private set => _scaling = value; + get => Interlocked.CompareExchange(ref _scaling, 0.0, 0.0); + private set => Interlocked.Exchange(ref _scaling, value); } public double DesktopScaling => RenderScaling; @@ -378,11 +372,6 @@ namespace Avalonia.X11 } void OnEvent(ref XEvent ev) - { - lock (SyncRoot) - OnEventSync(ref ev); - } - void OnEventSync(ref XEvent ev) { if (ev.type == XEventName.MapNotify) { @@ -544,32 +533,29 @@ namespace Avalonia.X11 private bool UpdateScaling(bool skipResize = false) { - lock (SyncRoot) + double newScaling; + if (_scalingOverride.HasValue) + newScaling = _scalingOverride.Value; + else { - double newScaling; - if (_scalingOverride.HasValue) - newScaling = _scalingOverride.Value; - 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; - } + var monitor = _platform.X11Screens.Screens.OrderBy(x => x.PixelDensity) + .FirstOrDefault(m => m.Bounds.Contains(Position)); + newScaling = monitor?.PixelDensity ?? RenderScaling; + } - 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; From 9c0964adf56ab573daed6be6c88ba480db2a0ec0 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 23 Jan 2022 01:41:48 +0300 Subject: [PATCH 34/36] 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 35/36] 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 36/36] 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)