From 52e6b9a6d1ac48f71de073d9cdc53bbddb5c1124 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 5 Jun 2021 22:28:20 -0400 Subject: [PATCH 01/32] Make Animation.RunAsync cancellable --- src/Avalonia.Animation/Animation.cs | 14 +++++++++----- src/Avalonia.Animation/ApiCompatBaseline.txt | 6 ++++++ src/Avalonia.Animation/IAnimation.cs | 3 ++- 3 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 src/Avalonia.Animation/ApiCompatBaseline.txt diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index c42153ec4f..a170456854 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -3,10 +3,11 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; +using System.Threading; using System.Threading.Tasks; + using Avalonia.Animation.Animators; using Avalonia.Animation.Easings; -using Avalonia.Collections; using Avalonia.Data; using Avalonia.Metadata; @@ -292,7 +293,7 @@ namespace Avalonia.Animation return (newAnimatorInstances, subscriptions); } - /// + /// public IDisposable Apply(Animatable control, IClock clock, IObservable match, Action onComplete) { var (animators, subscriptions) = InterpretKeyframes(control); @@ -323,21 +324,24 @@ namespace Avalonia.Animation return new CompositeDisposable(subscriptions); } - /// - public Task RunAsync(Animatable control, IClock clock = null) + /// + public Task RunAsync(Animatable control, IClock clock = null, CancellationToken cancellationToken = default) { var run = new TaskCompletionSource(); if (this.IterationCount == IterationCount.Infinite) run.SetException(new InvalidOperationException("Looping animations must not use the Run method.")); - IDisposable subscriptions = null; + IDisposable subscriptions = null, cancellation = null; subscriptions = this.Apply(control, clock, Observable.Return(true), () => { run.SetResult(null); subscriptions?.Dispose(); + cancellation?.Dispose(); }); + cancellation = cancellationToken.Register(state => ((IDisposable)state).Dispose(), subscriptions); + return run.Task; } } diff --git a/src/Avalonia.Animation/ApiCompatBaseline.txt b/src/Avalonia.Animation/ApiCompatBaseline.txt new file mode 100644 index 0000000000..58cb7830e7 --- /dev/null +++ b/src/Avalonia.Animation/ApiCompatBaseline.txt @@ -0,0 +1,6 @@ +Compat issues with assembly Avalonia.Animation: +MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.Animation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock, System.Threading.CancellationToken)' is present in the implementation but not in the contract. +Total Issues: 4 diff --git a/src/Avalonia.Animation/IAnimation.cs b/src/Avalonia.Animation/IAnimation.cs index ff85535d8a..5844ba5688 100644 --- a/src/Avalonia.Animation/IAnimation.cs +++ b/src/Avalonia.Animation/IAnimation.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; namespace Avalonia.Animation @@ -16,6 +17,6 @@ namespace Avalonia.Animation /// /// Run the animation on the specified control. /// - Task RunAsync(Animatable control, IClock clock); + Task RunAsync(Animatable control, IClock clock, CancellationToken cancellationToken); } } From 4bbedf581562fcf14a5a4227f135876db3f8856f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 5 Jun 2021 22:28:45 -0400 Subject: [PATCH 02/32] Make PageTransition.Start cancellable --- .../Animation/CompositePageTransition.cs | 21 ++---- src/Avalonia.Visuals/Animation/CrossFade.cs | 64 ++++++++----------- .../Animation/IPageTransition.cs | 6 +- src/Avalonia.Visuals/Animation/PageSlide.cs | 30 ++++----- src/Avalonia.Visuals/ApiCompatBaseline.txt | 9 ++- 5 files changed, 55 insertions(+), 75 deletions(-) diff --git a/src/Avalonia.Visuals/Animation/CompositePageTransition.cs b/src/Avalonia.Visuals/Animation/CompositePageTransition.cs index 9489914c97..2deebd7792 100644 --- a/src/Avalonia.Visuals/Animation/CompositePageTransition.cs +++ b/src/Avalonia.Visuals/Animation/CompositePageTransition.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Avalonia.Metadata; @@ -35,25 +36,11 @@ namespace Avalonia.Animation [Content] public List PageTransitions { get; set; } = new List(); - /// - /// Starts the animation. - /// - /// - /// The control that is being transitioned away from. May be null. - /// - /// - /// The control that is being transitioned to. May be null. - /// - /// - /// Defines the direction of the transition. - /// - /// - /// A that tracks the progress of the animation. - /// - public Task Start(Visual from, Visual to, bool forward) + /// + public Task Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken) { var transitionTasks = PageTransitions - .Select(transition => transition.Start(from, to, forward)) + .Select(transition => transition.Start(from, to, forward, cancellationToken)) .ToList(); return Task.WhenAll(transitionTasks); } diff --git a/src/Avalonia.Visuals/Animation/CrossFade.cs b/src/Avalonia.Visuals/Animation/CrossFade.cs index 0615b854da..9ff0d99b23 100644 --- a/src/Avalonia.Visuals/Animation/CrossFade.cs +++ b/src/Avalonia.Visuals/Animation/CrossFade.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Reactive.Disposables; +using System.Threading; using System.Threading.Tasks; using Avalonia.Animation.Easings; using Avalonia.Styling; @@ -97,49 +99,39 @@ namespace Avalonia.Animation set => _fadeOutAnimation.Easing = value; } - /// - /// Starts the animation. - /// - /// - /// The control that is being transitioned away from. May be null. - /// - /// - /// The control that is being transitioned to. May be null. - /// - /// - /// A that tracks the progress of the animation. - /// - public async Task Start(Visual from, Visual to) + /// + public async Task Start(Visual from, Visual to, CancellationToken cancellationToken) { - var tasks = new List(); - - if (to != null) - { - to.Opacity = 0; - } - - if (from != null) + if (cancellationToken.IsCancellationRequested) { - tasks.Add(_fadeOutAnimation.RunAsync(from)); + return; } - if (to != null) + var tasks = new List(); + using (var disposables = new CompositeDisposable()) { - to.IsVisible = true; - tasks.Add(_fadeInAnimation.RunAsync(to)); + if (to != null) + { + disposables.Add(to.SetValue(Visual.OpacityProperty, 0, Data.BindingPriority.Animation)); + } - } + if (from != null) + { + tasks.Add(_fadeOutAnimation.RunAsync(from, null, cancellationToken)); + } - await Task.WhenAll(tasks); + if (to != null) + { + to.IsVisible = true; + tasks.Add(_fadeInAnimation.RunAsync(to, null, cancellationToken)); + } - if (from != null) - { - from.IsVisible = false; - } + await Task.WhenAll(tasks); - if (to != null) - { - to.Opacity = 1; + if (from != null && !cancellationToken.IsCancellationRequested) + { + from.IsVisible = false; + } } } @@ -158,9 +150,9 @@ namespace Avalonia.Animation /// /// A that tracks the progress of the animation. /// - Task IPageTransition.Start(Visual from, Visual to, bool forward) + Task IPageTransition.Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken) { - return Start(from, to); + return Start(from, to, cancellationToken); } } } diff --git a/src/Avalonia.Visuals/Animation/IPageTransition.cs b/src/Avalonia.Visuals/Animation/IPageTransition.cs index 659bc12424..2d19ddbb5b 100644 --- a/src/Avalonia.Visuals/Animation/IPageTransition.cs +++ b/src/Avalonia.Visuals/Animation/IPageTransition.cs @@ -1,3 +1,4 @@ +using System.Threading; using System.Threading.Tasks; namespace Avalonia.Animation @@ -19,9 +20,12 @@ namespace Avalonia.Animation /// /// If the animation is bidirectional, controls the direction of the animation. /// + /// + /// Animation cancellation. + /// /// /// A that tracks the progress of the animation. /// - Task Start(Visual from, Visual to, bool forward); + Task Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken); } } diff --git a/src/Avalonia.Visuals/Animation/PageSlide.cs b/src/Avalonia.Visuals/Animation/PageSlide.cs index dd5d598e12..7d033ccf61 100644 --- a/src/Avalonia.Visuals/Animation/PageSlide.cs +++ b/src/Avalonia.Visuals/Animation/PageSlide.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Avalonia.Animation.Easings; using Avalonia.Media; @@ -60,23 +61,14 @@ namespace Avalonia.Animation /// public Easing SlideOutEasing { get; set; } = new LinearEasing(); - /// - /// Starts the animation. - /// - /// - /// The control that is being transitioned away from. May be null. - /// - /// - /// The control that is being transitioned to. May be null. - /// - /// - /// If true, the new page is slid in from the right, or if false from the left. - /// - /// - /// A that tracks the progress of the animation. - /// - public async Task Start(Visual from, Visual to, bool forward) + /// + public async Task Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken) { + if (cancellationToken.IsCancellationRequested) + { + return; + } + var tasks = new List(); var parent = GetVisualParent(from, to); var distance = Orientation == SlideAxis.Horizontal ? parent.Bounds.Width : parent.Bounds.Height; @@ -109,7 +101,7 @@ namespace Avalonia.Animation }, Duration = Duration }; - tasks.Add(animation.RunAsync(from)); + tasks.Add(animation.RunAsync(from, null, cancellationToken)); } if (to != null) @@ -140,12 +132,12 @@ namespace Avalonia.Animation }, Duration = Duration }; - tasks.Add(animation.RunAsync(to)); + tasks.Add(animation.RunAsync(to, null, cancellationToken)); } await Task.WhenAll(tasks); - if (from != null) + if (from != null && !cancellationToken.IsCancellationRequested) { from.IsVisible = false; } diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index f9fd125615..c917902dc3 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -1,4 +1,10 @@ Compat issues with assembly Avalonia.Visuals: +MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.CompositePageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.CrossFade.Start(Avalonia.Visual, Avalonia.Visual)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean, System.Threading.CancellationToken)' is present in the implementation but not in the contract. +MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.PageSlide.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract. CannotSealType : Type 'Avalonia.Media.TextFormatting.GenericTextParagraphProperties' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. @@ -63,9 +69,8 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalon InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' is present in the contract but not in the implementation. MembersMustExist : Member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' does not exist in the implementation but it does exist in the contract. -Total Issues: 64 InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmap(System.IO.Stream)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmap(System.String)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToHeight(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToWidth(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract. -Total Issues: 11 +Total Issues: 74 From f13ece461b43306c8f9c2680662e649c265b1e07 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 5 Jun 2021 22:29:16 -0400 Subject: [PATCH 03/32] Provide cancellation to animations where neccessary --- src/Avalonia.Controls/Expander.cs | 21 +++++++++++++------ .../Presenters/CarouselPresenter.cs | 2 +- .../TransitioningContentControl.cs | 13 +++++++++--- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index 052b42a233..b9c79e5749 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -1,7 +1,11 @@ +using System.Threading; + using Avalonia.Animation; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; +#nullable enable + namespace Avalonia.Controls { /// @@ -36,8 +40,8 @@ namespace Avalonia.Controls [PseudoClasses(":expanded", ":up", ":down", ":left", ":right")] public class Expander : HeaderedContentControl { - public static readonly StyledProperty ContentTransitionProperty = - AvaloniaProperty.Register(nameof(ContentTransition)); + public static readonly StyledProperty ContentTransitionProperty = + AvaloniaProperty.Register(nameof(ContentTransition)); public static readonly StyledProperty ExpandDirectionProperty = AvaloniaProperty.Register(nameof(ExpandDirection), ExpandDirection.Down); @@ -50,6 +54,7 @@ namespace Avalonia.Controls defaultBindingMode: Data.BindingMode.TwoWay); private bool _isExpanded; + private CancellationTokenSource? _lastTransitionCts; static Expander() { @@ -61,7 +66,7 @@ namespace Avalonia.Controls UpdatePseudoClasses(ExpandDirection); } - public IPageTransition ContentTransition + public IPageTransition? ContentTransition { get => GetValue(ContentTransitionProperty); set => SetValue(ContentTransitionProperty, value); @@ -83,19 +88,23 @@ namespace Avalonia.Controls } } - protected virtual void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e) + protected virtual async void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e) { if (Content != null && ContentTransition != null && Presenter is Visual visualContent) { bool forward = ExpandDirection == ExpandDirection.Left || ExpandDirection == ExpandDirection.Up; + + _lastTransitionCts?.Cancel(); + _lastTransitionCts = new CancellationTokenSource(); + if (IsExpanded) { - ContentTransition.Start(null, visualContent, forward); + await ContentTransition.Start(null, visualContent, forward, _lastTransitionCts.Token); } else { - ContentTransition.Start(visualContent, null, !forward); + await ContentTransition.Start(visualContent, null, forward, _lastTransitionCts.Token); } } } diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index 7888249bdd..81f43865a7 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -186,7 +186,7 @@ namespace Avalonia.Controls.Presenters if (PageTransition != null && (from != null || to != null)) { - await PageTransition.Start((Visual)from, (Visual)to, fromIndex < toIndex); + await PageTransition.Start((Visual)from, (Visual)to, fromIndex < toIndex, default); } else if (to != null) { diff --git a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs index 9685ecbe91..c4dd79f468 100644 --- a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs +++ b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; + using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Styling; @@ -22,7 +24,9 @@ namespace Avalonia.ReactiveUI /// public static readonly StyledProperty DefaultContentProperty = AvaloniaProperty.Register(nameof(DefaultContent)); - + + private CancellationTokenSource? _lastTransitionCts; + /// /// Gets or sets the animation played when content appears and disappears. /// @@ -62,11 +66,14 @@ namespace Avalonia.ReactiveUI /// New content to set. private async void UpdateContentWithTransition(object? content) { + _lastTransitionCts?.Cancel(); + _lastTransitionCts = new CancellationTokenSource(); + if (PageTransition != null) - await PageTransition.Start(this, null, true); + await PageTransition.Start(this, null, true, _lastTransitionCts.Token); base.Content = content; if (PageTransition != null) - await PageTransition.Start(null, this, true); + await PageTransition.Start(null, this, true, _lastTransitionCts.Token); } } } From 9da802d4845a8507b586926fb7ca4e65c1d84ce2 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 5 Jun 2021 22:38:19 -0400 Subject: [PATCH 04/32] Do not run animation if it was cancelled --- src/Avalonia.Animation/Animation.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index a170456854..eb48fd7b16 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -327,6 +327,11 @@ namespace Avalonia.Animation /// public Task RunAsync(Animatable control, IClock clock = null, CancellationToken cancellationToken = default) { + if (cancellationToken.IsCancellationRequested) + { + return Task.CompletedTask; + } + var run = new TaskCompletionSource(); if (this.IterationCount == IterationCount.Infinite) From 6872b2a8a46cb47eb18cdc85cdff91e7e4394db2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 15 Jun 2021 11:53:40 +0200 Subject: [PATCH 05/32] Ensure we unsubscribe from PropertyChanged. --- src/Avalonia.Animation/AnimationInstance`1.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Animation/AnimationInstance`1.cs b/src/Avalonia.Animation/AnimationInstance`1.cs index 0e1882ce75..b9ba49e510 100644 --- a/src/Avalonia.Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Animation/AnimationInstance`1.cs @@ -46,9 +46,6 @@ namespace Avalonia.Animation _onCompleteAction = OnComplete; _interpolator = Interpolator; _baseClock = baseClock; - control.PropertyChanged += ControlPropertyChanged; - - UpdateNeutralValue(); FetchProperties(); } @@ -82,6 +79,7 @@ namespace Avalonia.Animation // Animation may have been stopped before it has finished. ApplyFinalFill(); + _targetControl.PropertyChanged -= ControlPropertyChanged; _timerSub?.Dispose(); _clock.PlayState = PlayState.Stop; } @@ -90,6 +88,8 @@ namespace Avalonia.Animation { _clock = new Clock(_baseClock); _timerSub = _clock.Subscribe(Step); + _targetControl.PropertyChanged += ControlPropertyChanged; + UpdateNeutralValue(); } public void Step(TimeSpan frameTick) From 038805bb26c74f8896093fb0204c73082de34268 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 16 Jun 2021 17:23:48 +0200 Subject: [PATCH 06/32] Cache property changed delegate. --- src/Avalonia.Animation/AnimationInstance`1.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Animation/AnimationInstance`1.cs b/src/Avalonia.Animation/AnimationInstance`1.cs index b9ba49e510..cf79640150 100644 --- a/src/Avalonia.Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Animation/AnimationInstance`1.cs @@ -37,6 +37,7 @@ namespace Avalonia.Animation private IDisposable _timerSub; private readonly IClock _baseClock; private IClock _clock; + private EventHandler _propertyChangedDelegate; public AnimationInstance(Animation animation, Animatable control, Animator animator, IClock baseClock, Action OnComplete, Func Interpolator) { @@ -79,7 +80,7 @@ namespace Avalonia.Animation // Animation may have been stopped before it has finished. ApplyFinalFill(); - _targetControl.PropertyChanged -= ControlPropertyChanged; + _targetControl.PropertyChanged -= _propertyChangedDelegate; _timerSub?.Dispose(); _clock.PlayState = PlayState.Stop; } @@ -88,7 +89,8 @@ namespace Avalonia.Animation { _clock = new Clock(_baseClock); _timerSub = _clock.Subscribe(Step); - _targetControl.PropertyChanged += ControlPropertyChanged; + _propertyChangedDelegate ??= ControlPropertyChanged; + _targetControl.PropertyChanged += _propertyChangedDelegate; UpdateNeutralValue(); } From f10f54682161bdcfc87ea26d823924691bf90885 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 17 Jun 2021 12:49:51 +0100 Subject: [PATCH 07/32] scale flyout transient area. --- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 4725d17d65..ea47d15914 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -252,7 +252,9 @@ namespace Avalonia.Controls.Primitives if (Popup?.Host is PopupRoot root) { // Get the popup root bounds and convert to screen coordinates - var tmp = root.Bounds.Inflate(100); + var topLevel = root.Parent as TopLevel; + + var tmp = root.Bounds.Inflate(topLevel.PointToClient(new PixelPoint(100, 100)).X); var scPt = root.PointToScreen(tmp.TopLeft); enlargedPopupRect = new Rect(scPt.X, scPt.Y, tmp.Width, tmp.Height); } From b80ee8672954999a2c52212bff0deb5f458078eb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 17 Jun 2021 12:53:06 +0100 Subject: [PATCH 08/32] no need for toplevel. --- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index ea47d15914..b3b5b5238e 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -252,9 +252,7 @@ namespace Avalonia.Controls.Primitives if (Popup?.Host is PopupRoot root) { // Get the popup root bounds and convert to screen coordinates - var topLevel = root.Parent as TopLevel; - - var tmp = root.Bounds.Inflate(topLevel.PointToClient(new PixelPoint(100, 100)).X); + var tmp = root.Bounds.Inflate(root.PointToClient(new PixelPoint(100, 100)).X); var scPt = root.PointToScreen(tmp.TopLeft); enlargedPopupRect = new Rect(scPt.X, scPt.Y, tmp.Width, tmp.Height); } From 45299c15059732404ef07a88e0d2987af0186433 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 17 Jun 2021 13:04:28 +0100 Subject: [PATCH 09/32] use renderscaling. --- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index b3b5b5238e..73f2329513 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -4,6 +4,7 @@ using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Layout; using Avalonia.Logging; +using Avalonia.Rendering; #nullable enable @@ -252,7 +253,8 @@ namespace Avalonia.Controls.Primitives if (Popup?.Host is PopupRoot root) { // Get the popup root bounds and convert to screen coordinates - var tmp = root.Bounds.Inflate(root.PointToClient(new PixelPoint(100, 100)).X); + + var tmp = root.Bounds.Inflate(100 * (root as IRenderRoot).RenderScaling); var scPt = root.PointToScreen(tmp.TopLeft); enlargedPopupRect = new Rect(scPt.X, scPt.Y, tmp.Width, tmp.Height); } From b78d2a0e06808c32ef1573d7dda249fc4605f342 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 17 Jun 2021 14:44:43 +0100 Subject: [PATCH 10/32] use Pixel api types when working in screen coordinates. --- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 32 +++++++++------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 73f2329513..e4b68c62fd 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -52,8 +52,9 @@ namespace Avalonia.Controls.Primitives private bool _isOpen; private Control? _target; private FlyoutShowMode _showMode = FlyoutShowMode.Standard; - private Rect? enlargedPopupRect; - private IDisposable? transientDisposable; + private Rect? _enlargedPopupRect; + private PixelRect? _enlargePopupRectScreenPixelRect; + private IDisposable? _transientDisposable; protected Popup? Popup { get; private set; } @@ -164,8 +165,10 @@ namespace Avalonia.Controls.Primitives Popup.IsOpen = false; // Ensure this isn't active - transientDisposable?.Dispose(); - transientDisposable = null; + _transientDisposable?.Dispose(); + _transientDisposable = null; + _enlargedPopupRect = null; + _enlargePopupRectScreenPixelRect = null; OnClosed(); } @@ -231,7 +234,7 @@ namespace Avalonia.Controls.Primitives } else if (ShowMode == FlyoutShowMode.TransientWithDismissOnPointerMoveAway) { - transientDisposable = InputManager.Instance?.Process.Subscribe(HandleTransientDismiss); + _transientDisposable = InputManager.Instance?.Process.Subscribe(HandleTransientDismiss); } } @@ -247,21 +250,20 @@ namespace Avalonia.Controls.Primitives // For windowed popups, enlargedPopupRect is in screen coordinates, // for overlay popups, its in OverlayLayer coordinates - if (enlargedPopupRect == null) + if (_enlargedPopupRect == null && _enlargePopupRectScreenPixelRect == null) { // Only do this once when the Flyout opens & cache the result if (Popup?.Host is PopupRoot root) { // Get the popup root bounds and convert to screen coordinates - var tmp = root.Bounds.Inflate(100 * (root as IRenderRoot).RenderScaling); - var scPt = root.PointToScreen(tmp.TopLeft); - enlargedPopupRect = new Rect(scPt.X, scPt.Y, tmp.Width, tmp.Height); + var tmp = root.Bounds.Inflate(100); + _enlargePopupRectScreenPixelRect = new PixelRect(root.PointToScreen(tmp.TopLeft), root.PointToScreen(tmp.BottomRight)); } else if (Popup?.Host is OverlayPopupHost host) { // Overlay popups are in OverlayLayer coordinates, just use that - enlargedPopupRect = host.Bounds.Inflate(100); + _enlargedPopupRect = host.Bounds.Inflate(100); } return; @@ -275,24 +277,18 @@ namespace Avalonia.Controls.Primitives // window will not close this (as pointer events stop), which // does match UWP var pt = pArgs.Root.PointToScreen(pArgs.Position); - if (!enlargedPopupRect?.Contains(new Point(pt.X, pt.Y)) ?? false) + if (!_enlargePopupRectScreenPixelRect?.Contains(pt) ?? false) { HideCore(false); - enlargedPopupRect = null; - transientDisposable?.Dispose(); - transientDisposable = null; } } else if (Popup?.Host is OverlayPopupHost) { // Same as above here, but just different coordinate space // so we don't need to translate - if (!enlargedPopupRect?.Contains(pArgs.Position) ?? false) + if (!_enlargedPopupRect?.Contains(pArgs.Position) ?? false) { HideCore(false); - enlargedPopupRect = null; - transientDisposable?.Dispose(); - transientDisposable = null; } } } From b37ddadf811f97dd6ebaf181460b349d1c49e5f1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 17 Jun 2021 16:31:57 +0200 Subject: [PATCH 11/32] Implement measure pass in AdornerLayer. Previously the `AdornerLayer` was falling back to the base class implementation of `MeasureOverride`, in `Canvas`. This meant that at times, adorner controls were being measured with a larger constraint than the final size given to them. `Grid` doesn't like it when this happens which causes problems with various controls. Add a measure pass to `AdornerLayer` which passes the adorned control's bounds as the constraint (which is the value that the arrange pass will use). --- .../Primitives/AdornerLayer.cs | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 9834bf3d3b..a397608aba 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Specialized; using System.Linq; +using System.Resources; using Avalonia.Media; using Avalonia.Rendering; using Avalonia.Utilities; @@ -45,10 +46,27 @@ namespace Avalonia.Controls.Primitives ?.AdornerLayer; } - protected override Size ArrangeOverride(Size finalSize) + protected override Size MeasureOverride(Size availableSize) { - var parent = Parent; + foreach (var child in Children) + { + var info = child.GetValue(s_adornedElementInfoProperty); + + if (info != null && info.Bounds.HasValue) + { + child.Measure(info.Bounds.Value.Bounds.Size); + } + else + { + child.Measure(availableSize); + } + } + return default; + } + + protected override Size ArrangeOverride(Size finalSize) + { foreach (var child in Children) { var info = child.GetValue(s_adornedElementInfoProperty); From eb3933b636bea07a7a06e1e9994738f590d319cf Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 17 Jun 2021 15:56:39 +0100 Subject: [PATCH 12/32] fix order of operations, set Offset before setting Extent --- src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index b0b52812b9..e3783febdd 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -511,8 +511,8 @@ namespace Avalonia.Controls.Presenters else if (scrollable.IsLogicalScrollEnabled) { Viewport = scrollable.Viewport; - Extent = scrollable.Extent; Offset = scrollable.Offset; + Extent = scrollable.Extent; } } From 955fb1ffdb75e4f77f4e55c1bfdf2d883435c4e9 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 15 Jun 2021 19:01:16 +0200 Subject: [PATCH 13/32] fixes: Warnings CS0169 --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 3 --- src/Avalonia.Controls/Menu.cs | 1 - src/Avalonia.Controls/Repeater/ViewportManager.cs | 1 - src/Avalonia.Controls/Repeater/VirtualizationInfo.cs | 1 - src/Avalonia.X11/TransparencyHelper.cs | 1 - src/Avalonia.X11/X11Window.cs | 1 - src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs | 1 - .../Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreen.cs | 1 - src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs | 1 - .../WinRT/Composition/WinUICompositorConnection.cs | 1 - 10 files changed, 12 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 1b4632d368..83f13fe199 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -75,7 +75,6 @@ namespace Avalonia.Controls private const double DATAGRID_defaultMinColumnWidth = 20; private const double DATAGRID_defaultMaxColumnWidth = double.PositiveInfinity; - private List _validationErrors; private List _bindingValidationErrors; private IDisposable _validationSubscription; @@ -102,7 +101,6 @@ namespace Avalonia.Controls private bool _areHandlersSuspended; private bool _autoSizingColumns; private IndexToValueTable _collapsedSlotsTable; - private DataGridCellCoordinates _currentCellCoordinates; private Control _clickedElement; // used to store the current column during a Reset @@ -141,7 +139,6 @@ namespace Avalonia.Controls private DataGridSelectedItemsCollection _selectedItems; private bool _temporarilyResetCurrentCell; private object _uneditedValue; // Represents the original current cell value at the time it enters editing mode. - private ICellEditBinding _currentCellEditBinding; // An approximation of the sum of the heights in pixels of the scrolling rows preceding // the first displayed scrolling row. Since the scrolled off rows are discarded, the grid diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index 4da044fec1..706be376a9 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -17,7 +17,6 @@ namespace Avalonia.Controls private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel { Orientation = Orientation.Horizontal }); - private LightDismissOverlayLayer? _overlay; /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs index 6e24408aa9..da3c2b15e6 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -27,7 +27,6 @@ namespace Avalonia.Controls private IScrollAnchorProvider _scroller; private IControl _makeAnchorElement; private bool _isAnchorOutsideRealizedRange; - private Task _cacheBuildAction; private Rect _visibleWindow; private Rect _layoutExtent; // This is the expected shift by the layout. diff --git a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs index f8cfde609e..7e6b24f1b5 100644 --- a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs +++ b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs @@ -27,7 +27,6 @@ namespace Avalonia.Controls internal class VirtualizationInfo { private int _pinCounter; - private object _data; public Rect ArrangeBounds { get; set; } public bool AutoRecycleCandidate { get; set; } diff --git a/src/Avalonia.X11/TransparencyHelper.cs b/src/Avalonia.X11/TransparencyHelper.cs index 0578680136..2140b61b6f 100644 --- a/src/Avalonia.X11/TransparencyHelper.cs +++ b/src/Avalonia.X11/TransparencyHelper.cs @@ -10,7 +10,6 @@ namespace Avalonia.X11 private readonly X11Globals _globals; private WindowTransparencyLevel _currentLevel; private WindowTransparencyLevel _requestedLevel; - private bool _isCompositing; private bool _blurAtomsAreSet; public Action TransparencyLevelChanged { get; set; } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 5ac4c4c9d0..37260aa78b 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -30,7 +30,6 @@ namespace Avalonia.X11 ITopLevelImplWithTextInputMethod { private readonly AvaloniaX11Platform _platform; - private readonly IWindowImpl _popupParent; private readonly bool _popup; private readonly X11Info _x11; private XConfigureEvent? _configure; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 4bbb58e53e..ac2fd40c54 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -15,7 +15,6 @@ namespace Avalonia.LinuxFramebuffer private readonly IOutputBackend _outputBackend; private readonly IInputBackend _inputBackend; - private bool _renderQueued; public IInputRoot InputRoot { get; private set; } public FramebufferToplevelImpl(IOutputBackend outputBackend, IInputBackend inputBackend) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreen.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreen.cs index b69b151c3b..c35a3d1174 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreen.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevTouchScreen.cs @@ -7,7 +7,6 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev internal class EvDevSingleTouchScreen : EvDevDeviceHandler { private readonly IScreenInfoProvider _screenInfo; - private readonly int _width, _height; private readonly Matrix _calibration; private input_absinfo _axisX; private input_absinfo _axisY; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index dc44d2d55f..ee4125101c 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -16,7 +16,6 @@ namespace Avalonia.LinuxFramebuffer.Output public unsafe class DrmOutput : IGlOutputBackend, IGlPlatformSurface { private DrmCard _card; - private readonly EglGlPlatformSurface _eglPlatformSurface; public PixelSize PixelSize => _mode.Resolution; public double Scaling { get; set; } public IGlContext PrimaryContext => _deferredContext; diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs index 2aa82436f6..1c3c959acf 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs @@ -17,7 +17,6 @@ namespace Avalonia.Win32.WinRT.Composition class WinUICompositorConnection : IRenderTimer { private readonly EglContext _syncContext; - private IntPtr _queue; private ICompositor _compositor; private ICompositor2 _compositor2; private ICompositor5 _compositor5; From 7d863e877f52323cff846d9c0b835c30d54787f9 Mon Sep 17 00:00:00 2001 From: amwx Date: Fri, 18 Jun 2021 21:22:53 -0500 Subject: [PATCH 14/32] Let Data and Stretch be overridden --- src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml index 2eee656dc6..60420f2f44 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml @@ -112,9 +112,7 @@ + VerticalAlignment="Center" /> + + From a49ba4b0e346e9de534ae0e5e090c39b1ed1ca05 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 19 Jun 2021 19:59:15 -0400 Subject: [PATCH 15/32] Add tests --- src/Avalonia.Animation/Animation.cs | 13 +- src/Avalonia.Animation/IAnimation.cs | 2 +- .../AnimationIterationTests.cs | 195 ++++++++++++++++++ 3 files changed, 206 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index eb48fd7b16..b5f89c00ab 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -318,7 +318,9 @@ namespace Avalonia.Animation if (onComplete != null) { - Task.WhenAll(completionTasks).ContinueWith(_ => onComplete()); + Task.WhenAll(completionTasks).ContinueWith( + (_, state) => ((Action)state).Invoke(), + onComplete); } } return new CompositeDisposable(subscriptions); @@ -340,12 +342,17 @@ namespace Avalonia.Animation IDisposable subscriptions = null, cancellation = null; subscriptions = this.Apply(control, clock, Observable.Return(true), () => { - run.SetResult(null); + run.TrySetResult(null); subscriptions?.Dispose(); cancellation?.Dispose(); }); - cancellation = cancellationToken.Register(state => ((IDisposable)state).Dispose(), subscriptions); + cancellation = cancellationToken.Register(() => + { + run.TrySetResult(null); + subscriptions?.Dispose(); + cancellation?.Dispose(); + }); return run.Task; } diff --git a/src/Avalonia.Animation/IAnimation.cs b/src/Avalonia.Animation/IAnimation.cs index 5844ba5688..d037834630 100644 --- a/src/Avalonia.Animation/IAnimation.cs +++ b/src/Avalonia.Animation/IAnimation.cs @@ -17,6 +17,6 @@ namespace Avalonia.Animation /// /// Run the animation on the specified control. /// - Task RunAsync(Animatable control, IClock clock, CancellationToken cancellationToken); + Task RunAsync(Animatable control, IClock clock, CancellationToken cancellationToken = default); } } diff --git a/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs b/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs index fe718ec32b..6ddc31ec1b 100644 --- a/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs +++ b/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs @@ -176,5 +176,200 @@ namespace Avalonia.Animation.UnitTests clock.Step(TimeSpan.FromSeconds(0.100d)); Assert.Equal(border.Width, 300d); } + + [Fact] + public void Do_Not_Run_Cancelled_Animation() + { + var keyframe1 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 200d), + }, + Cue = new Cue(1d) + }; + + var keyframe2 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 100d), + }, + Cue = new Cue(0d) + }; + + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(10), + Delay = TimeSpan.FromSeconds(0), + DelayBetweenIterations = TimeSpan.FromSeconds(0), + IterationCount = new IterationCount(1), + Children = + { + keyframe2, + keyframe1 + } + }; + + var border = new Border() + { + Height = 100d, + Width = 100d + }; + var propertyChangedCount = 0; + border.PropertyChanged += (sender, e) => + { + if (e.Property == Control.WidthProperty) + { + propertyChangedCount++; + } + }; + + var clock = new TestClock(); + var cancellationTokenSource = new CancellationTokenSource(); + cancellationTokenSource.Cancel(); + var animationRun = animation.RunAsync(border, clock, cancellationTokenSource.Token); + + clock.Step(TimeSpan.FromSeconds(10)); + Assert.Equal(0, propertyChangedCount); + Assert.True(animationRun.IsCompleted); + } + + [Fact] + public void Cancellation_Should_Stop_Animation() + { + var keyframe1 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 200d), + }, + Cue = new Cue(1d) + }; + + var keyframe2 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 100d), + }, + Cue = new Cue(0d) + }; + + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(10), + Delay = TimeSpan.FromSeconds(0), + DelayBetweenIterations = TimeSpan.FromSeconds(0), + IterationCount = new IterationCount(1), + Children = + { + keyframe2, + keyframe1 + } + }; + + var border = new Border() + { + Height = 100d, + Width = 50d + }; + var propertyChangedCount = 0; + border.PropertyChanged += (sender, e) => + { + if (e.Property == Control.WidthProperty) + { + propertyChangedCount++; + } + }; + + var clock = new TestClock(); + var cancellationTokenSource = new CancellationTokenSource(); + var animationRun = animation.RunAsync(border, clock, cancellationTokenSource.Token); + + Assert.Equal(0, propertyChangedCount); + + clock.Step(TimeSpan.FromSeconds(0)); + Assert.False(animationRun.IsCompleted); + Assert.Equal(1, propertyChangedCount); + + cancellationTokenSource.Cancel(); + clock.Step(TimeSpan.FromSeconds(1)); + clock.Step(TimeSpan.FromSeconds(2)); + clock.Step(TimeSpan.FromSeconds(3)); + //Assert.Equal(2, propertyChangedCount); + + animationRun.Wait(); + + clock.Step(TimeSpan.FromSeconds(6)); + Assert.True(animationRun.IsCompleted); + Assert.Equal(2, propertyChangedCount); + } + + [Fact] + public void Cancellation_Of_Completed_Animation_Does_Not_Fail() + { + var keyframe1 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 200d), + }, + Cue = new Cue(1d) + }; + + var keyframe2 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 100d), + }, + Cue = new Cue(0d) + }; + + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(10), + Delay = TimeSpan.FromSeconds(0), + DelayBetweenIterations = TimeSpan.FromSeconds(0), + IterationCount = new IterationCount(1), + Children = + { + keyframe2, + keyframe1 + } + }; + + var border = new Border() + { + Height = 100d, + Width = 50d + }; + var propertyChangedCount = 0; + border.PropertyChanged += (sender, e) => + { + if (e.Property == Control.WidthProperty) + { + propertyChangedCount++; + } + }; + + var clock = new TestClock(); + var cancellationTokenSource = new CancellationTokenSource(); + var animationRun = animation.RunAsync(border, clock, cancellationTokenSource.Token); + + Assert.Equal(0, propertyChangedCount); + + clock.Step(TimeSpan.FromSeconds(0)); + Assert.False(animationRun.IsCompleted); + Assert.Equal(1, propertyChangedCount); + + clock.Step(TimeSpan.FromSeconds(10)); + Assert.True(animationRun.IsCompleted); + Assert.Equal(2, propertyChangedCount); + + cancellationTokenSource.Cancel(); + animationRun.Wait(); + } } } From 04d3ce168eafba7a4b45a66f2cabb8a7b2ea3e07 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 19 Jun 2021 20:01:08 -0400 Subject: [PATCH 16/32] Add failing test --- .../AnimationIterationTests.cs | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs b/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs index 6ddc31ec1b..60d4dddaf0 100644 --- a/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs +++ b/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs @@ -9,6 +9,8 @@ using Avalonia.UnitTests; using Avalonia.Data; using Xunit; using Avalonia.Animation.Easings; +using System.Threading; +using System.Reactive.Linq; namespace Avalonia.Animation.UnitTests { @@ -177,6 +179,77 @@ namespace Avalonia.Animation.UnitTests Assert.Equal(border.Width, 300d); } + [Fact] + public void Dispose_Subscription_Should_Stop_Animation() + { + var keyframe1 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 200d), + }, + Cue = new Cue(1d) + }; + + var keyframe2 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 100d), + }, + Cue = new Cue(0d) + }; + + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(10), + Delay = TimeSpan.FromSeconds(0), + DelayBetweenIterations = TimeSpan.FromSeconds(0), + IterationCount = new IterationCount(1), + Children = + { + keyframe2, + keyframe1 + } + }; + + var border = new Border() + { + Height = 100d, + Width = 50d + }; + var propertyChangedCount = 0; + var animationCompletedCount = 0; + border.PropertyChanged += (sender, e) => + { + if (e.Property == Control.WidthProperty) + { + propertyChangedCount++; + } + }; + + var clock = new TestClock(); + var disposable = animation.Apply(border, clock, Observable.Return(true), () => animationCompletedCount++); + + Assert.Equal(0, propertyChangedCount); + + clock.Step(TimeSpan.FromSeconds(0)); + Assert.Equal(0, animationCompletedCount); + Assert.Equal(1, propertyChangedCount); + + disposable.Dispose(); + + // Clock ticks should be ignored after Dispose + clock.Step(TimeSpan.FromSeconds(5)); + clock.Step(TimeSpan.FromSeconds(6)); + clock.Step(TimeSpan.FromSeconds(7)); + + // On animation disposing (cancellation) on completed is not invoked (is it expected?) + Assert.Equal(0, animationCompletedCount); + // Initial property changed before cancellation + animation value removal. + Assert.Equal(2, propertyChangedCount); + } + [Fact] public void Do_Not_Run_Cancelled_Animation() { From e44256b5dd28e9f7b1b8ad0cbb6c24eec5782be8 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 19 Jun 2021 20:29:30 -0400 Subject: [PATCH 17/32] Skip failing tests, issue created --- tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs b/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs index 60d4dddaf0..58bd7a42c3 100644 --- a/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs +++ b/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs @@ -179,7 +179,7 @@ namespace Avalonia.Animation.UnitTests Assert.Equal(border.Width, 300d); } - [Fact] + [Fact(Skip = "See #6111")] public void Dispose_Subscription_Should_Stop_Animation() { var keyframe1 = new KeyFrame() @@ -308,7 +308,7 @@ namespace Avalonia.Animation.UnitTests Assert.True(animationRun.IsCompleted); } - [Fact] + [Fact(Skip = "See #6111")] public void Cancellation_Should_Stop_Animation() { var keyframe1 = new KeyFrame() From 1739ed4138be8a1572600dd7b809b05359489fae Mon Sep 17 00:00:00 2001 From: aguahombre Date: Mon, 21 Jun 2021 19:42:00 +0100 Subject: [PATCH 18/32] Adds a pixel format parameter to Linux frame buffer platform setup. (#6101) * Add pixel format parameter to Linux frame buffer platform setup. Currently setup always changes the frame buffer pixel format to RGBA which results in the screen being cleared if the default pixel format is not RGBA (as on a Raspberry PI) . This clears any splash screen and leaves the screen blank for a period which is not a good UX. Now the frame buffer setup can select the correct pixel format or use null to leave the pixel format unchanged. * Remove unnecessary formatting changes. Add v0.10.x compatible constructor. * Keep old StartLinuxFbDev extension method for v0.10.x binary compatibility --- .../LinuxFramebufferPlatform.cs | 5 +- .../LockedFramebuffer.cs | 2 +- .../Output/FbdevOutput.cs | 88 ++++++++++++++----- 3 files changed, 72 insertions(+), 23 deletions(-) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index a6b70069c1..89f81a7649 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -132,7 +132,10 @@ public static class LinuxFramebufferPlatformExtensions { public static int StartLinuxFbDev(this T builder, string[] args, string fbdev = null, double scaling = 1) where T : AppBuilderBase, new() => - StartLinuxDirect(builder, args, new FbdevOutput(fbdev) {Scaling = scaling}); + StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: null) { Scaling = scaling }); + public static int StartLinuxFbDev(this T builder, string[] args, string fbdev, PixelFormat? format, double scaling) + where T : AppBuilderBase, new() => + StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: format) { Scaling = scaling }); public static int StartLinuxDrm(this T builder, string[] args, string card = null, double scaling = 1) where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card) {Scaling = scaling}); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs b/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs index ed59166eb8..87c7b64c26 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs @@ -41,6 +41,6 @@ namespace Avalonia.LinuxFramebuffer public PixelSize Size => new PixelSize((int)_varInfo.xres, (int) _varInfo.yres); public int RowBytes => (int) _fixedInfo.line_length; public Vector Dpi { get; } - public PixelFormat Format => _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 : PixelFormat.Bgra8888; + public PixelFormat Format => _varInfo.bits_per_pixel == 16 ? PixelFormat.Rgb565 : _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 : PixelFormat.Bgra8888; } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs index b83fe6cbe8..add744ee16 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs @@ -16,16 +16,33 @@ namespace Avalonia.LinuxFramebuffer private IntPtr _mappedAddress; public double Scaling { get; set; } - public FbdevOutput(string fileName = null) + /// + /// Create a Linux frame buffer device output + /// + /// The frame buffer device name. + /// Defaults to the value in environment variable FRAMEBUFFER or /dev/fb0 when FRAMEBUFFER is not set + public FbdevOutput(string fileName = null) : this(null, null) { - fileName = fileName ?? Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0"; + } + + /// + /// Create a Linux frame buffer device output + /// + /// The frame buffer device name. + /// Defaults to the value in environment variable FRAMEBUFFER or /dev/fb0 when FRAMEBUFFER is not set + /// The required pixel format for the frame buffer. + /// A null value will leave the frame buffer in the current pixel format. + /// Otherwise sets the frame buffer to the required format + public FbdevOutput(string fileName, PixelFormat? format) + { + fileName ??= Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0"; _fd = NativeUnsafeMethods.open(fileName, 2, 0); if (_fd <= 0) throw new Exception("Error: " + Marshal.GetLastWin32Error()); try { - Init(); + Init(format); } catch { @@ -34,25 +51,28 @@ namespace Avalonia.LinuxFramebuffer } } - void Init() + void Init(PixelFormat? format) { fixed (void* pnfo = &_varInfo) { if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_VSCREENINFO, pnfo)) throw new Exception("FBIOGET_VSCREENINFO error: " + Marshal.GetLastWin32Error()); - SetBpp(); + if (format.HasValue) + { + SetBpp(format.Value); - if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOPUT_VSCREENINFO, pnfo)) - _varInfo.transp = new fb_bitfield(); + if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOPUT_VSCREENINFO, pnfo)) + _varInfo.transp = new fb_bitfield(); - NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOPUT_VSCREENINFO, pnfo); + NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOPUT_VSCREENINFO, pnfo); - if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_VSCREENINFO, pnfo)) - throw new Exception("FBIOGET_VSCREENINFO error: " + Marshal.GetLastWin32Error()); + if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_VSCREENINFO, pnfo)) + throw new Exception("FBIOGET_VSCREENINFO error: " + Marshal.GetLastWin32Error()); - if (_varInfo.bits_per_pixel != 32) - throw new Exception("Unable to set 32-bit display mode"); + if (_varInfo.bits_per_pixel != 32) + throw new Exception("Unable to set 32-bit display mode"); + } } fixed(void*pnfo = &_fixedInfo) if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_FSCREENINFO, pnfo)) @@ -70,17 +90,43 @@ namespace Avalonia.LinuxFramebuffer } } - void SetBpp() + void SetBpp(PixelFormat format) { - _varInfo.bits_per_pixel = 32; - _varInfo.grayscale = 0; - _varInfo.red = _varInfo.blue = _varInfo.green = _varInfo.transp = new fb_bitfield + switch (format) { - length = 8 - }; - _varInfo.green.offset = 8; - _varInfo.blue.offset = 16; - _varInfo.transp.offset = 24; + case PixelFormat.Rgba8888: + _varInfo.bits_per_pixel = 32; + _varInfo.grayscale = 0; + _varInfo.red = _varInfo.blue = _varInfo.green = _varInfo.transp = new fb_bitfield + { + length = 8 + }; + _varInfo.green.offset = 8; + _varInfo.blue.offset = 16; + _varInfo.transp.offset = 24; + break; + case PixelFormat.Bgra8888: + _varInfo.bits_per_pixel = 32; + _varInfo.grayscale = 0; + _varInfo.red = _varInfo.blue = _varInfo.green = _varInfo.transp = new fb_bitfield + { + length = 8 + }; + _varInfo.green.offset = 8; + _varInfo.red.offset = 16; + _varInfo.transp.offset = 24; + break; + case PixelFormat.Rgb565: + _varInfo.bits_per_pixel = 16; + _varInfo.grayscale = 0; + _varInfo.red = _varInfo.blue = _varInfo.green = _varInfo.transp = new fb_bitfield(); + _varInfo.red.length = 5; + _varInfo.green.offset = 5; + _varInfo.green.length = 6; + _varInfo.blue.offset = 11; + _varInfo.blue.length = 5; + break; + } } public string Id { get; private set; } From 49014f43b38bcc48690b21bf8d57275406b3186e Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 24 Jun 2021 13:26:18 +0300 Subject: [PATCH 19/32] Update FbdevOutput.cs --- src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs index add744ee16..61f00b2795 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs @@ -21,7 +21,7 @@ namespace Avalonia.LinuxFramebuffer /// /// The frame buffer device name. /// Defaults to the value in environment variable FRAMEBUFFER or /dev/fb0 when FRAMEBUFFER is not set - public FbdevOutput(string fileName = null) : this(null, null) + public FbdevOutput(string fileName = null) : this(fileName, null) { } From c81c97a19a2db066533100a303527113c578b0d2 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 24 Jun 2021 16:08:24 -0400 Subject: [PATCH 20/32] Use IsNullOrEmpty instead of IsNullOrWhiteSpace for :empty pseudoclass --- src/Avalonia.Controls/TextBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 1bee15bccd..c1516613b3 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -1290,7 +1290,7 @@ namespace Avalonia.Controls private void UpdatePseudoclasses() { - PseudoClasses.Set(":empty", string.IsNullOrWhiteSpace(Text)); + PseudoClasses.Set(":empty", string.IsNullOrEmpty(Text)); } private bool IsPasswordBox => PasswordChar != default(char); From b4e11b227d325871ff80c4b8bcbf8aad5ff3773c Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 25 Jun 2021 10:09:15 +0200 Subject: [PATCH 21/32] fixes: code documentation --- src/Avalonia.Controls/Grid.cs | 1 + src/Avalonia.Controls/SplitView.cs | 2 +- src/Avalonia.Visuals/Animation/CrossFade.cs | 1 + .../Rendering/SceneGraph/BitmapBlendModeNode.cs | 4 ++-- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 2 +- src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs | 3 ++- tests/Avalonia.Controls.UnitTests/TreeViewTests.cs | 2 +- 7 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index c7d598006d..229fa674f0 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -978,6 +978,7 @@ namespace Avalonia.Controls /// width is not registered in columns. /// Passed through to MeasureCell. /// When "true" cells' desired height is not registered in rows. + /// return true when desired size has ghanged private void MeasureCellsGroup( int cellsHead, Size referenceSize, diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index 4500d52484..0e35c610b2 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView.cs @@ -133,7 +133,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(Pane)); /// - /// Defines the property. + /// Defines the property. /// public static readonly StyledProperty PaneTemplateProperty = AvaloniaProperty.Register(nameof(PaneTemplate)); diff --git a/src/Avalonia.Visuals/Animation/CrossFade.cs b/src/Avalonia.Visuals/Animation/CrossFade.cs index 9ff0d99b23..5eaa920b32 100644 --- a/src/Avalonia.Visuals/Animation/CrossFade.cs +++ b/src/Avalonia.Visuals/Animation/CrossFade.cs @@ -147,6 +147,7 @@ namespace Avalonia.Animation /// /// Unused for cross-fades. /// + /// allowed cancel transition /// /// A that tracks the progress of the animation. /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/BitmapBlendModeNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/BitmapBlendModeNode.cs index 0a5c1f8db6..45b62b843b 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/BitmapBlendModeNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/BitmapBlendModeNode.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - /// Initializes a new instance of the class that represents an + /// Initializes a new instance of the class that represents an /// pop. /// public BitmapBlendModeNode() @@ -40,7 +40,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Determines if this draw operation equals another. /// - /// The opacity of the other draw operation. + /// the how to compare /// True if the draw operations are the same, otherwise false. /// /// The properties of the other draw operation are passed in as arguments to prevent diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index d8bd0607d1..2352b8b076 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -591,7 +591,7 @@ namespace Avalonia.Skia /// Configure paint wrapper for using gradient brush. /// /// Paint wrapper. - /// Target size. + /// Target bound rect. /// Gradient brush. private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Rect targetRect, IGradientBrush gradientBrush) { diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs index aa86df7c23..32818dfdd2 100644 --- a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs @@ -16,11 +16,12 @@ namespace Avalonia.Skia /// Surfaces. /// Created render target or if it fails. ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable surfaces); - + /// /// Creates an offscreen render target surface /// /// size in pixels + /// current Skia render session ISkiaSurface TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session); } diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index cea77bb7c9..72ba3ab273 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1066,7 +1066,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Auto_Expanding_In_Style_Should_Not_Break_Range_Selection() { - /// Issue #2980. + // Issue #2980. using (Application()) { var target = new DerivedTreeView From b51957f9d87d9459db7c7ba36ae77d2ae84bac29 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 25 Jun 2021 11:58:23 +0200 Subject: [PATCH 22/32] fixes: typo --- src/Avalonia.Controls/Grid.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 229fa674f0..a14df1eb43 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -978,7 +978,7 @@ namespace Avalonia.Controls /// width is not registered in columns. /// Passed through to MeasureCell. /// When "true" cells' desired height is not registered in rows. - /// return true when desired size has ghanged + /// return true when desired size has changed private void MeasureCellsGroup( int cellsHead, Size referenceSize, From 6e48959f1084d098c0d3bc7bf1daa4908b2c23bd Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 25 Jun 2021 12:25:46 +0200 Subject: [PATCH 23/32] fixes(MenuItem): nullable warnings --- src/Avalonia.Controls/MenuItem.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 4c801c2e06..7b06d3c868 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -36,7 +36,7 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty HotKeyProperty = + public static readonly StyledProperty HotKeyProperty = HotKeyManager.HotKeyProperty.AddOwner(); /// @@ -108,7 +108,7 @@ namespace Avalonia.Controls private ICommand? _command; private bool _commandCanExecute = true; private Popup? _popup; - private KeyGesture _hotkey; + private KeyGesture? _hotkey; private bool _isEmbeddedInMenu; /// @@ -214,7 +214,7 @@ namespace Avalonia.Controls /// /// Gets or sets an associated with this control /// - public KeyGesture HotKey + public KeyGesture? HotKey { get { return GetValue(HotKeyProperty); } set { SetValue(HotKeyProperty, value); } From 97882e3c76f0b821b38e6796b3b5b2a59e43e1e1 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 25 Jun 2021 12:47:52 +0200 Subject: [PATCH 24/32] fixes(Application): some nullable warnings --- src/Avalonia.Controls/Application.cs | 31 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 54c576bb76..157bebe02b 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -13,6 +13,7 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Threading; +#nullable enable namespace Avalonia { @@ -35,27 +36,27 @@ namespace Avalonia /// /// The application-global data templates. /// - private DataTemplates _dataTemplates; + private DataTemplates? _dataTemplates; private readonly Lazy _clipboard = new Lazy(() => (IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))); private readonly Styler _styler = new Styler(); - private Styles _styles; - private IResourceDictionary _resources; + private Styles? _styles; + private IResourceDictionary? _resources; private bool _notifyingResourcesChanged; - private Action> _stylesAdded; - private Action> _stylesRemoved; + private Action>? _stylesAdded; + private Action>? _stylesRemoved; /// /// Defines the property. /// - public static readonly StyledProperty DataContextProperty = + public static readonly StyledProperty DataContextProperty = StyledElement.DataContextProperty.AddOwner(); /// - public event EventHandler ResourcesChanged; + public event EventHandler? ResourcesChanged; - public event EventHandler UrlsOpened; + public event EventHandler? UrlsOpened; /// /// Creates an instance of the class. @@ -72,7 +73,7 @@ namespace Avalonia /// The data context property specifies the default object that will /// be used for data binding. /// - public object DataContext + public object? DataContext { get { return GetValue(DataContextProperty); } set { SetValue(DataContextProperty, value); } @@ -162,7 +163,7 @@ namespace Avalonia /// /// Gets the styling parent of the application, which is null. /// - IStyleHost IStyleHost.StylingParent => null; + IStyleHost? IStyleHost.StylingParent => null; /// bool IStyleHost.IsStylesInitialized => _styles != null; @@ -194,7 +195,7 @@ namespace Avalonia public virtual void Initialize() { } /// - bool IResourceNode.TryGetResource(object key, out object value) + bool IResourceNode.TryGetResource(object key, out object? value) { value = null; return (_resources?.TryGetResource(key, out value) ?? false) || @@ -279,17 +280,17 @@ namespace Avalonia NotifyResourcesChanged(e); } - private string _name; + private string? _name; /// /// Defines Name property /// - public static readonly DirectProperty NameProperty = - AvaloniaProperty.RegisterDirect("Name", o => o.Name, (o, v) => o.Name = v); + public static readonly DirectProperty NameProperty = + AvaloniaProperty.RegisterDirect("Name", o => o.Name, (o, v) => o.Name = v); /// /// Application name to be used for various platform-specific purposes /// - public string Name + public string? Name { get => _name; set => SetAndRaise(NameProperty, ref _name, value); From 87639cab8ee433dcc27f6f7f408871f5a0cab802 Mon Sep 17 00:00:00 2001 From: FoggyFinder Date: Sat, 26 Jun 2021 18:59:08 +0300 Subject: [PATCH 25/32] adjust checking --- src/Avalonia.Controls.DataGrid/DataGridRows.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridRows.cs b/src/Avalonia.Controls.DataGrid/DataGridRows.cs index a69b8eafe1..4bfbd7d818 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRows.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRows.cs @@ -425,7 +425,7 @@ namespace Avalonia.Controls UpdateDisplayedRows(DisplayData.FirstScrollingSlot, CellsHeight); } - if (DisplayData.FirstScrollingSlot < slot && DisplayData.LastScrollingSlot > slot) + if (DisplayData.FirstScrollingSlot < slot && (DisplayData.LastScrollingSlot > slot || DisplayData.LastScrollingSlot == -1)) { // The row is already displayed in its entirety return true; From f94e4eb9e75d69f3b12b436f2d036331f4d4a0d9 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 29 Jun 2021 16:54:29 +0200 Subject: [PATCH 26/32] fixes(ComboBox): Closing window on Alt+F4 KeyDown when ComboBox is focused --- src/Avalonia.Controls/ComboBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index f8728af87f..dd2109e6cd 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -205,7 +205,7 @@ namespace Avalonia.Controls if (e.Handled) return; - if (e.Key == Key.F4 || + if ((e.Key == Key.F4 && e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) == false) || ((e.Key == Key.Down || e.Key == Key.Up) && e.KeyModifiers.HasAllFlags(KeyModifiers.Alt))) { IsDropDownOpen = !IsDropDownOpen; From 83c141172f17de098907b7b207f4300f35e8ba73 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 29 Jun 2021 16:55:09 +0200 Subject: [PATCH 27/32] feat(tests): Add test to check closing window on Alt+F4 KeyDown when ComboBox is focused --- .../ComboBoxTests.cs | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 8f9c7fdb0b..cb2fd11175 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -201,6 +201,65 @@ namespace Avalonia.Controls.UnitTests } - } + } + + [Fact] + public void Close_Window_On_Alt_F4_When_ComboBox_Is_Focus() + { + var inputManagerMock = new Moq.Mock(); + var services = TestServices.StyledWindow.With(inputManager: inputManagerMock.Object); + + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(); + + window.KeyDown += (s, e) => + { + if (e.Handled == false + && e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) == true + && e.Key == Key.F4 ) + { + e.Handled = true; + window.Close(); + } + }; + + var count = 0; + + var target = new ComboBox + { + Items = new[] { new Canvas() }, + SelectedIndex = 0, + Template = GetTemplate(), + }; + + window.Content = target; + + + window.Closing += + (sender, e) => + { + count++; + }; + + window.Show(); + + target.Focus(); + + _helper.Down(target); + _helper.Up(target); + Assert.True(target.IsDropDownOpen); + + target.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + KeyModifiers = KeyModifiers.Alt, + Key = Key.F4 + }); + + + Assert.Equal(1, count); + } + } } } From 5bea0e4791e284829b53fae2935a26de4e86a6ea Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 2 Jul 2021 14:44:04 +0200 Subject: [PATCH 28/32] Add "Star" static to GridLength --- src/Avalonia.Controls/GridLength.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Avalonia.Controls/GridLength.cs b/src/Avalonia.Controls/GridLength.cs index b8418949d9..ee6a146d61 100644 --- a/src/Avalonia.Controls/GridLength.cs +++ b/src/Avalonia.Controls/GridLength.cs @@ -77,6 +77,12 @@ namespace Avalonia.Controls /// public static GridLength Auto => new GridLength(0, GridUnitType.Auto); + /// + /// Gets an instance of that indicates that a row or column should + /// fill its content. + /// + public static GridLength Star => new GridLength(1, GridUnitType.Star); + /// /// Gets the unit of the . /// From fa3f2c89327b3c31f9dc737fe15152fe184c365c Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 2 Jul 2021 15:49:19 +0200 Subject: [PATCH 29/32] Adding more ToString() overloads to support DevTools --- src/Avalonia.Controls/RowDefinitions.cs | 8 +++-- src/Avalonia.Visuals/Media/BoxShadow.cs | 41 ++++++++++++++++++++++++ src/Avalonia.Visuals/Media/BoxShadows.cs | 20 +++++++++++- 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/RowDefinitions.cs b/src/Avalonia.Controls/RowDefinitions.cs index 02ab12b5af..a5ed6ae09d 100644 --- a/src/Avalonia.Controls/RowDefinitions.cs +++ b/src/Avalonia.Controls/RowDefinitions.cs @@ -1,5 +1,4 @@ using System.Linq; -using Avalonia.Collections; namespace Avalonia.Controls { @@ -25,6 +24,11 @@ namespace Avalonia.Controls AddRange(GridLength.ParseLengths(s).Select(x => new RowDefinition(x))); } + public override string ToString() + { + return string.Join(",", this.Select(x => x.Height)); + } + /// /// Parses a string representation of row definitions collection. /// @@ -32,4 +36,4 @@ namespace Avalonia.Controls /// The . public static RowDefinitions Parse(string s) => new RowDefinitions(s); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Media/BoxShadow.cs b/src/Avalonia.Visuals/Media/BoxShadow.cs index 69395fd3b8..7696c6c34b 100644 --- a/src/Avalonia.Visuals/Media/BoxShadow.cs +++ b/src/Avalonia.Visuals/Media/BoxShadow.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Text; using Avalonia.Animation.Animators; using Avalonia.Utilities; @@ -75,6 +76,46 @@ namespace Avalonia.Media return rv; } } + + public override string ToString() + { + var sb = new StringBuilder(); + + if (IsEmpty) + { + return "none"; + } + + if (IsInset) + { + sb.Append("inset"); + } + + if (OffsetX != 0.0) + { + sb.Append($" {OffsetX}"); + } + + if (OffsetY != 0.0) + { + sb.Append($" {OffsetY}"); + } + + if (Blur != 0.0) + { + sb.Append($" {Blur}"); + } + + if (Spread != 0.0) + { + sb.Append($" {Spread}"); + } + + sb.Append($" {Color.ToString()}"); + + return sb.ToString(); + } + public static unsafe BoxShadow Parse(string s) { if(s == null) diff --git a/src/Avalonia.Visuals/Media/BoxShadows.cs b/src/Avalonia.Visuals/Media/BoxShadows.cs index 9e4d6aacb0..6e1b0e7f7d 100644 --- a/src/Avalonia.Visuals/Media/BoxShadows.cs +++ b/src/Avalonia.Visuals/Media/BoxShadows.cs @@ -1,6 +1,6 @@ using System; -using System.Collections.Generic; using System.ComponentModel; +using System.Text; using Avalonia.Animation.Animators; namespace Avalonia.Media @@ -43,6 +43,24 @@ namespace Avalonia.Media } } + public override string ToString() + { + var sb = new StringBuilder(); + + if (Count == 0) + { + return "none"; + } + + foreach (var boxShadow in this) + { + sb.Append(boxShadow + " "); + } + + return sb.ToString(); + + } + [EditorBrowsable(EditorBrowsableState.Never)] public struct BoxShadowsEnumerator { From 40e6282375b3fc9ae56464b39eb2421df3c8a3b6 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 2 Jul 2021 15:51:47 +0200 Subject: [PATCH 30/32] Use interpolation string --- src/Avalonia.Visuals/Media/BoxShadows.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/BoxShadows.cs b/src/Avalonia.Visuals/Media/BoxShadows.cs index 6e1b0e7f7d..2b1714aded 100644 --- a/src/Avalonia.Visuals/Media/BoxShadows.cs +++ b/src/Avalonia.Visuals/Media/BoxShadows.cs @@ -54,7 +54,7 @@ namespace Avalonia.Media foreach (var boxShadow in this) { - sb.Append(boxShadow + " "); + sb.Append($"{boxShadow} "); } return sb.ToString(); From cddae052e87ce0b06839cee7b8ed4c32d63fa058 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 2 Jul 2021 16:46:10 +0200 Subject: [PATCH 31/32] Don't box params, prevent creating temporary string --- src/Avalonia.Visuals/Media/BoxShadow.cs | 10 +++++----- src/Avalonia.Visuals/Media/BoxShadows.cs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Visuals/Media/BoxShadow.cs b/src/Avalonia.Visuals/Media/BoxShadow.cs index 7696c6c34b..50f75365b0 100644 --- a/src/Avalonia.Visuals/Media/BoxShadow.cs +++ b/src/Avalonia.Visuals/Media/BoxShadow.cs @@ -93,25 +93,25 @@ namespace Avalonia.Media if (OffsetX != 0.0) { - sb.Append($" {OffsetX}"); + sb.AppendFormat(" {0}", OffsetX.ToString()); } if (OffsetY != 0.0) { - sb.Append($" {OffsetY}"); + sb.AppendFormat(" {0}", OffsetY.ToString()); } if (Blur != 0.0) { - sb.Append($" {Blur}"); + sb.AppendFormat(" {0}", Blur.ToString()); } if (Spread != 0.0) { - sb.Append($" {Spread}"); + sb.AppendFormat(" {0}", Spread.ToString()); } - sb.Append($" {Color.ToString()}"); + sb.AppendFormat(" {0}", Color.ToString()); return sb.ToString(); } diff --git a/src/Avalonia.Visuals/Media/BoxShadows.cs b/src/Avalonia.Visuals/Media/BoxShadows.cs index 2b1714aded..810ac70b99 100644 --- a/src/Avalonia.Visuals/Media/BoxShadows.cs +++ b/src/Avalonia.Visuals/Media/BoxShadows.cs @@ -54,7 +54,7 @@ namespace Avalonia.Media foreach (var boxShadow in this) { - sb.Append($"{boxShadow} "); + sb.AppendFormat("{0} ", boxShadow.ToString()); } return sb.ToString(); From 17c94d3cf3b43d71aea697f9a17c264ac80da87b Mon Sep 17 00:00:00 2001 From: amwx Date: Sun, 4 Jul 2021 21:33:52 -0500 Subject: [PATCH 32/32] Add impl from upstream --- src/Avalonia.Layout/ElementManager.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Layout/ElementManager.cs b/src/Avalonia.Layout/ElementManager.cs index cb13deb15f..3f106708e6 100644 --- a/src/Avalonia.Layout/ElementManager.cs +++ b/src/Avalonia.Layout/ElementManager.cs @@ -325,7 +325,10 @@ namespace Avalonia.Layout break; case NotifyCollectionChangedAction.Move: - throw new NotImplementedException(); + int size = args.OldItems != null ? args.OldItems.Count : 1; + OnItemsRemoved(args.OldStartingIndex, size); + OnItemsAdded(args.NewStartingIndex, size); + break; } } }