From 02e30f80f5c2293e115ef8b0679e4ddd8537d166 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaitis Date: Wed, 8 Sep 2021 20:53:05 +0300 Subject: [PATCH 01/26] [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/26] [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/26] [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 0cd4b90bc734ee714ab11daa0e6d9ea342b4134e Mon Sep 17 00:00:00 2001 From: Lubomir Tetak Date: Fri, 7 Jan 2022 09:11:59 +0100 Subject: [PATCH 04/26] 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 05/26] 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 b12c0bce3d09622a790f35adfa427066209451ac Mon Sep 17 00:00:00 2001 From: Dave Higgins Date: Wed, 19 Jan 2022 16:29:33 +0000 Subject: [PATCH 06/26] fix strange happengings in IsCorrupted, sizes being updated without any IsCorrupted true returned. --- src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs | 6 ++++++ src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs | 2 ++ src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs | 2 ++ 3 files changed, 10 insertions(+) diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs b/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs index ee7374634f..5df844b11d 100644 --- a/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs +++ b/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs @@ -38,6 +38,12 @@ namespace Avalonia.Web.Blazor { get { + if( _blazorSkiaSurface.IsDirty ) + { + _blazorSkiaSurface.IsDirty = false; + return true; + } + var result = _size.Width != _renderTarget.Width || _size.Height != _renderTarget.Height; return result; diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs b/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs index 512309cfe3..3e5bc8934d 100644 --- a/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs +++ b/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs @@ -26,5 +26,7 @@ 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 1d667c0f0c..0158dc4f31 100644 --- a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs @@ -51,6 +51,8 @@ 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 eb6750822c75fb4e1be85c154515c70b4c3377c4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 20 Jan 2022 10:23:35 +0000 Subject: [PATCH 07/26] 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 08/26] 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 09/26] 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 10/26] 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 11/26] 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 12/26] 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 13/26] 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 14/26] 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 15/26] 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 16/26] 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 17/26] 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 18/26] 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 19/26] 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 20/26] 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 21/26] 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 22/26] 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 23/26] 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 24/26] 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 25/26] 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 26/26] 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; - } - } -}