From 03d6ca9374f932a7d411dc3a9b2003fc0cb277eb Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 22 Dec 2021 00:33:26 -0500 Subject: [PATCH 01/56] Better fix for #6830 Previous numericupdown bugfix (see https://github.com/AvaloniaUI/Avalonia/pull/6830/files) introduced another bug (see https://github.com/AvaloniaUI/Avalonia/discussions/7230#discussioncomment-1855837). This PR solves both problems. --- src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml b/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml index 1e58259358..36ab07e3e3 100644 --- a/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml @@ -38,7 +38,7 @@ BorderBrush="{TemplateBinding BorderBrush}" CornerRadius="{TemplateBinding CornerRadius}" Padding="0" - MinWidth="{TemplateBinding MinWidth}" + MinWidth="0" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" AllowSpin="{TemplateBinding AllowSpin}" @@ -50,7 +50,7 @@ BorderBrush="Transparent" Margin="-1" Padding="{TemplateBinding Padding}" - MinWidth="{TemplateBinding MinWidth}" + MinWidth="0" Foreground="{TemplateBinding Foreground}" FontSize="{TemplateBinding FontSize}" Watermark="{TemplateBinding Watermark}" From ffac3eb0278ea1281aea3aa281d910663c6f010d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 22 Dec 2021 14:24:46 +0100 Subject: [PATCH 02/56] Added nullable annotations to Avalonia.Animation. --- src/Avalonia.Animation/Animatable.cs | 16 +++---- src/Avalonia.Animation/Animation.cs | 46 +++++++++++++------ src/Avalonia.Animation/AnimationInstance`1.cs | 33 +++++++------ src/Avalonia.Animation/AnimatorKeyFrame.cs | 20 ++++---- .../Animators/Animator`1.cs | 9 ++-- .../Avalonia.Animation.csproj | 4 ++ src/Avalonia.Animation/Clock.cs | 2 +- src/Avalonia.Animation/Cue.cs | 8 ++-- .../DisposeAnimationInstanceSubject.cs | 8 ++-- src/Avalonia.Animation/Easing/Easing.cs | 4 +- .../Easing/EasingTypeConverter.cs | 4 +- src/Avalonia.Animation/IAnimation.cs | 2 +- src/Avalonia.Animation/IAnimationSetter.cs | 4 +- src/Avalonia.Animation/IAnimator.cs | 4 +- src/Avalonia.Animation/ITransition.cs | 2 +- src/Avalonia.Animation/IterationCount.cs | 2 +- .../IterationCountTypeConverter.cs | 4 +- src/Avalonia.Animation/KeyFrame.cs | 4 +- .../KeySplineTypeConverter.cs | 4 +- src/Avalonia.Animation/Transition.cs | 21 +++++++-- src/Avalonia.Animation/TransitionInstance.cs | 6 +-- .../Animation/Animators/BaseBrushAnimator.cs | 7 ++- .../Animators/GradientBrushAnimator.cs | 5 ++ .../Animators/SolidColorBrushAnimator.cs | 5 ++ .../Animation/Animators/TransformAnimator.cs | 7 ++- 25 files changed, 142 insertions(+), 89 deletions(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 4811028f85..50fc5ac73b 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -157,7 +157,7 @@ namespace Avalonia.Animation state.Instance?.Dispose(); state.Instance = transition.Apply( this, - Clock ?? AvaloniaLocator.Current.GetService(), + Clock ?? AvaloniaLocator.Current.GetRequiredService(), oldValue, newValue); return; @@ -169,7 +169,7 @@ namespace Avalonia.Animation base.OnPropertyChangedCore(change); } - private void TransitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + private void TransitionsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (!_transitionsEnabled) { @@ -179,14 +179,14 @@ namespace Avalonia.Animation switch (e.Action) { case NotifyCollectionChangedAction.Add: - AddTransitions(e.NewItems); + AddTransitions(e.NewItems!); break; case NotifyCollectionChangedAction.Remove: - RemoveTransitions(e.OldItems); + RemoveTransitions(e.OldItems!); break; case NotifyCollectionChangedAction.Replace: - RemoveTransitions(e.OldItems); - AddTransitions(e.NewItems); + RemoveTransitions(e.OldItems!); + AddTransitions(e.NewItems!); break; case NotifyCollectionChangedAction.Reset: throw new NotSupportedException("Transitions collection cannot be reset."); @@ -204,7 +204,7 @@ namespace Avalonia.Animation for (var i = 0; i < items.Count; ++i) { - var t = (ITransition)items[i]; + var t = (ITransition)items[i]!; _transitionState.Add(t, new TransitionState { @@ -222,7 +222,7 @@ namespace Avalonia.Animation for (var i = 0; i < items.Count; ++i) { - var t = (ITransition)items[i]; + var t = (ITransition)items[i]!; if (_transitionState.TryGetValue(t, out var state)) { diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index a4515db514..03b2d17e44 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -203,7 +203,7 @@ namespace Avalonia.Animation /// /// The animation setter. /// The property animator type. - public static Type GetAnimator(IAnimationSetter setter) + public static Type? GetAnimator(IAnimationSetter setter) { if (s_animators.TryGetValue(setter, out var type)) { @@ -254,7 +254,7 @@ namespace Avalonia.Animation Animators.Insert(0, (condition, typeof(TAnimator))); } - private static Type GetAnimatorType(AvaloniaProperty property) + private static Type? GetAnimatorType(AvaloniaProperty property) { foreach (var (condition, type) in Animators) { @@ -276,6 +276,11 @@ namespace Avalonia.Animation { foreach (var setter in keyframe.Setters) { + if (setter.Property is null) + { + throw new InvalidOperationException("No Setter property assigned."); + } + var handler = Animation.GetAnimator(setter) ?? GetAnimatorType(setter.Property); if (handler == null) @@ -305,7 +310,7 @@ namespace Avalonia.Animation foreach (var (handlerType, property) in handlerList) { - var newInstance = (IAnimator)Activator.CreateInstance(handlerType); + var newInstance = (IAnimator)Activator.CreateInstance(handlerType)!; newInstance.Property = property; newAnimatorInstances.Add(newInstance); } @@ -321,32 +326,43 @@ namespace Avalonia.Animation } /// - public IDisposable Apply(Animatable control, IClock clock, IObservable match, Action onComplete) + public IDisposable Apply(Animatable control, IClock? clock, IObservable match, Action? onComplete) { var (animators, subscriptions) = InterpretKeyframes(control); if (animators.Count == 1) { - subscriptions.Add(animators[0].Apply(this, control, clock, match, onComplete)); + var subscription = animators[0].Apply(this, control, clock, match, onComplete); + + if (subscription is not null) + { + subscriptions.Add(subscription); + } } else { var completionTasks = onComplete != null ? new List() : null; foreach (IAnimator animator in animators) { - Action animatorOnComplete = null; + Action? animatorOnComplete = null; if (onComplete != null) { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(); animatorOnComplete = () => tcs.SetResult(null); - completionTasks.Add(tcs.Task); + completionTasks!.Add(tcs.Task); + } + + var subscription = animator.Apply(this, control, clock, match, animatorOnComplete); + + if (subscription is not null) + { + subscriptions.Add(subscription); } - subscriptions.Add(animator.Apply(this, control, clock, match, animatorOnComplete)); } if (onComplete != null) { - Task.WhenAll(completionTasks).ContinueWith( - (_, state) => ((Action)state).Invoke(), + Task.WhenAll(completionTasks!).ContinueWith( + (_, state) => ((Action)state!).Invoke(), onComplete); } } @@ -354,25 +370,25 @@ namespace Avalonia.Animation } /// - public Task RunAsync(Animatable control, IClock clock = null) + public Task RunAsync(Animatable control, IClock? clock = null) { return RunAsync(control, clock, default); } /// - public Task RunAsync(Animatable control, IClock clock = null, CancellationToken cancellationToken = default) + public Task RunAsync(Animatable control, IClock? clock = null, CancellationToken cancellationToken = default) { if (cancellationToken.IsCancellationRequested) { return Task.CompletedTask; } - var run = new TaskCompletionSource(); + var run = new TaskCompletionSource(); if (this.IterationCount == IterationCount.Infinite) run.SetException(new InvalidOperationException("Looping animations must not use the Run method.")); - IDisposable subscriptions = null, cancellation = null; + IDisposable? subscriptions = null, cancellation = null; subscriptions = this.Apply(control, clock, Observable.Return(true), () => { run.TrySetResult(null); diff --git a/src/Avalonia.Animation/AnimationInstance`1.cs b/src/Avalonia.Animation/AnimationInstance`1.cs index cf79640150..52cd4b324f 100644 --- a/src/Avalonia.Animation/AnimationInstance`1.cs +++ b/src/Avalonia.Animation/AnimationInstance`1.cs @@ -31,15 +31,15 @@ namespace Avalonia.Animation private TimeSpan _initialDelay; private TimeSpan _iterationDelay; private TimeSpan _duration; - private Easings.Easing _easeFunc; - private Action _onCompleteAction; + private Easings.Easing? _easeFunc; + private Action? _onCompleteAction; private Func _interpolator; - private IDisposable _timerSub; + private IDisposable? _timerSub; private readonly IClock _baseClock; - private IClock _clock; - private EventHandler _propertyChangedDelegate; + private IClock? _clock; + private EventHandler? _propertyChangedDelegate; - public AnimationInstance(Animation animation, Animatable control, Animator animator, IClock baseClock, Action OnComplete, Func Interpolator) + public AnimationInstance(Animation animation, Animatable control, Animator animator, IClock baseClock, Action? OnComplete, Func Interpolator) { _animator = animator; _animation = animation; @@ -47,6 +47,9 @@ namespace Avalonia.Animation _onCompleteAction = OnComplete; _interpolator = Interpolator; _baseClock = baseClock; + _lastInterpValue = default!; + _firstKFValue = default!; + _neutralValue = default!; FetchProperties(); } @@ -82,7 +85,7 @@ namespace Avalonia.Animation _targetControl.PropertyChanged -= _propertyChangedDelegate; _timerSub?.Dispose(); - _clock.PlayState = PlayState.Stop; + _clock!.PlayState = PlayState.Stop; } protected override void Subscribed() @@ -108,6 +111,8 @@ namespace Avalonia.Animation private void ApplyFinalFill() { + if (_animator.Property is null) + throw new InvalidOperationException("Animator has no property specified."); if (_fillMode == FillMode.Forward || _fillMode == FillMode.Both) _targetControl.SetValue(_animator.Property, _lastInterpValue, BindingPriority.LocalValue); } @@ -130,12 +135,12 @@ namespace Avalonia.Animation private void DoPlayStates() { - if (_clock.PlayState == PlayState.Stop || _baseClock.PlayState == PlayState.Stop) + if (_clock!.PlayState == PlayState.Stop || _baseClock.PlayState == PlayState.Stop) DoComplete(); if (!_gotFirstKFValue) { - _firstKFValue = (T)_animator.First().Value; + _firstKFValue = (T)_animator.First().Value!; _gotFirstKFValue = true; } } @@ -169,7 +174,7 @@ namespace Avalonia.Animation // and snap the last iteration value to exact values. if ((_currentIteration + 1) > _iterationCount) { - var easedTime = _easeFunc.Ease(_playbackReversed ? 0.0 : 1.0); + var easedTime = _easeFunc!.Ease(_playbackReversed ? 0.0 : 1.0); _lastInterpValue = _interpolator(easedTime, _neutralValue); DoComplete(); } @@ -203,7 +208,7 @@ namespace Avalonia.Animation normalizedTime = 1 - normalizedTime; // Ease and interpolate - var easedTime = _easeFunc.Ease(normalizedTime); + var easedTime = _easeFunc!.Ease(normalizedTime); _lastInterpValue = _interpolator(easedTime, _neutralValue); PublishNext(_lastInterpValue); @@ -223,14 +228,14 @@ namespace Avalonia.Animation private void UpdateNeutralValue() { - var property = _animator.Property; + var property = _animator.Property ?? throw new InvalidOperationException("Animator has no property specified."); var baseValue = _targetControl.GetBaseValue(property, BindingPriority.LocalValue); _neutralValue = baseValue != AvaloniaProperty.UnsetValue ? - (T)baseValue : (T)_targetControl.GetValue(property); + (T)baseValue! : (T)_targetControl.GetValue(property)!; } - private void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + private void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { if (e.Property == _animator.Property && e.Priority > BindingPriority.Animation) { diff --git a/src/Avalonia.Animation/AnimatorKeyFrame.cs b/src/Avalonia.Animation/AnimatorKeyFrame.cs index f6a0c12be4..8af31f2948 100644 --- a/src/Avalonia.Animation/AnimatorKeyFrame.cs +++ b/src/Avalonia.Animation/AnimatorKeyFrame.cs @@ -12,22 +12,22 @@ namespace Avalonia.Animation /// public class AnimatorKeyFrame : AvaloniaObject { - public static readonly DirectProperty ValueProperty = - AvaloniaProperty.RegisterDirect(nameof(Value), k => k.Value, (k, v) => k.Value = v); + public static readonly DirectProperty ValueProperty = + AvaloniaProperty.RegisterDirect(nameof(Value), k => k.Value, (k, v) => k.Value = v); public AnimatorKeyFrame() { } - public AnimatorKeyFrame(Type animatorType, Cue cue) + public AnimatorKeyFrame(Type? animatorType, Cue cue) { AnimatorType = animatorType; Cue = cue; KeySpline = null; } - public AnimatorKeyFrame(Type animatorType, Cue cue, KeySpline keySpline) + public AnimatorKeyFrame(Type? animatorType, Cue cue, KeySpline? keySpline) { AnimatorType = animatorType; Cue = cue; @@ -35,14 +35,14 @@ namespace Avalonia.Animation } internal bool isNeutral; - public Type AnimatorType { get; } + public Type? AnimatorType { get; } public Cue Cue { get; } - public KeySpline KeySpline { get; } - public AvaloniaProperty Property { get; private set; } + public KeySpline? KeySpline { get; } + public AvaloniaProperty? Property { get; private set; } - private object _value; + private object? _value; - public object Value + public object? Value { get => _value; set => SetAndRaise(ValueProperty, ref _value, value); @@ -80,7 +80,7 @@ namespace Avalonia.Animation throw new InvalidCastException($"KeyFrame value doesnt match property type."); } - return (T)typeConv.ConvertTo(Value, typeof(T)); + return (T)typeConv.ConvertTo(Value, typeof(T))!; } } } diff --git a/src/Avalonia.Animation/Animators/Animator`1.cs b/src/Avalonia.Animation/Animators/Animator`1.cs index 23afa76bf6..248ca61c1d 100644 --- a/src/Avalonia.Animation/Animators/Animator`1.cs +++ b/src/Avalonia.Animation/Animators/Animator`1.cs @@ -24,7 +24,7 @@ namespace Avalonia.Animation.Animators /// /// Gets or sets the target property for the keyframe. /// - public AvaloniaProperty Property { get; set; } + public AvaloniaProperty? Property { get; set; } public Animator() { @@ -33,7 +33,7 @@ namespace Avalonia.Animation.Animators } /// - public virtual IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable match, Action onComplete) + public virtual IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable match, Action? onComplete) { if (!_isVerifiedAndConverted) VerifyConvertKeyFrames(); @@ -106,13 +106,16 @@ namespace Avalonia.Animation.Animators public virtual IDisposable BindAnimation(Animatable control, IObservable instance) { + if (Property is null) + throw new InvalidOperationException("Animator has no property specified."); + return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation); } /// /// Runs the KeyFrames Animation. /// - internal IDisposable Run(Animation animation, Animatable control, IClock clock, Action onComplete) + internal IDisposable Run(Animation animation, Animatable control, IClock? clock, Action? onComplete) { var instance = new AnimationInstance( animation, diff --git a/src/Avalonia.Animation/Avalonia.Animation.csproj b/src/Avalonia.Animation/Avalonia.Animation.csproj index 85938ad958..9e3758658c 100644 --- a/src/Avalonia.Animation/Avalonia.Animation.csproj +++ b/src/Avalonia.Animation/Avalonia.Animation.csproj @@ -2,9 +2,13 @@ netstandard2.0;net6.0 + + + + diff --git a/src/Avalonia.Animation/Clock.cs b/src/Avalonia.Animation/Clock.cs index 5c2b7ce0dd..5afd2ae705 100644 --- a/src/Avalonia.Animation/Clock.cs +++ b/src/Avalonia.Animation/Clock.cs @@ -4,7 +4,7 @@ namespace Avalonia.Animation { public class Clock : ClockBase { - public static IClock GlobalClock => AvaloniaLocator.Current.GetService(); + public static IClock GlobalClock => AvaloniaLocator.Current.GetRequiredService(); private readonly IDisposable _parentSubscription; diff --git a/src/Avalonia.Animation/Cue.cs b/src/Avalonia.Animation/Cue.cs index 7da7a9382b..6578148b07 100644 --- a/src/Avalonia.Animation/Cue.cs +++ b/src/Avalonia.Animation/Cue.cs @@ -30,7 +30,7 @@ namespace Avalonia.Animation /// /// Parses a string to a object. /// - public static Cue Parse(string value, CultureInfo culture) + public static Cue Parse(string value, CultureInfo? culture) { string v = value; @@ -72,14 +72,14 @@ namespace Avalonia.Animation public class CueTypeConverter : TypeConverter { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { return sourceType == typeof(string); } - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { return Cue.Parse((string)value, culture); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs b/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs index 696f43d006..7283eaeedf 100644 --- a/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs +++ b/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs @@ -8,15 +8,15 @@ namespace Avalonia.Animation /// internal class DisposeAnimationInstanceSubject : IObserver, IDisposable { - private IDisposable _lastInstance; + private IDisposable? _lastInstance; private bool _lastMatch; private Animator _animator; private Animation _animation; private Animatable _control; - private Action _onComplete; - private IClock _clock; + private Action? _onComplete; + private IClock? _clock; - public DisposeAnimationInstanceSubject(Animator animator, Animation animation, Animatable control, IClock clock, Action onComplete) + public DisposeAnimationInstanceSubject(Animator animator, Animation animation, Animatable control, IClock? clock, Action? onComplete) { this._animator = animator; this._animation = animation; diff --git a/src/Avalonia.Animation/Easing/Easing.cs b/src/Avalonia.Animation/Easing/Easing.cs index e006459652..2f4b93dab1 100644 --- a/src/Avalonia.Animation/Easing/Easing.cs +++ b/src/Avalonia.Animation/Easing/Easing.cs @@ -15,7 +15,7 @@ namespace Avalonia.Animation.Easings /// public abstract double Ease(double progress); - static Dictionary _easingTypes; + static Dictionary? _easingTypes; static readonly Type s_thisType = typeof(Easing); @@ -48,7 +48,7 @@ namespace Avalonia.Animation.Easings if (_easingTypes.ContainsKey(e)) { var type = _easingTypes[e]; - return (Easing)Activator.CreateInstance(type); + return (Easing)Activator.CreateInstance(type)!; } else { diff --git a/src/Avalonia.Animation/Easing/EasingTypeConverter.cs b/src/Avalonia.Animation/Easing/EasingTypeConverter.cs index 6613f6d393..3d67d54a6f 100644 --- a/src/Avalonia.Animation/Easing/EasingTypeConverter.cs +++ b/src/Avalonia.Animation/Easing/EasingTypeConverter.cs @@ -6,12 +6,12 @@ namespace Avalonia.Animation.Easings { public class EasingTypeConverter : TypeConverter { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { return sourceType == typeof(string); } - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { return Easing.Parse((string)value); } diff --git a/src/Avalonia.Animation/IAnimation.cs b/src/Avalonia.Animation/IAnimation.cs index d037834630..436a765d27 100644 --- a/src/Avalonia.Animation/IAnimation.cs +++ b/src/Avalonia.Animation/IAnimation.cs @@ -12,7 +12,7 @@ namespace Avalonia.Animation /// /// Apply the animation to the specified control and run it when produces true. /// - IDisposable Apply(Animatable control, IClock clock, IObservable match, Action onComplete = null); + IDisposable Apply(Animatable control, IClock? clock, IObservable match, Action? onComplete = null); /// /// Run the animation on the specified control. diff --git a/src/Avalonia.Animation/IAnimationSetter.cs b/src/Avalonia.Animation/IAnimationSetter.cs index 2d22377286..6a1d3539e2 100644 --- a/src/Avalonia.Animation/IAnimationSetter.cs +++ b/src/Avalonia.Animation/IAnimationSetter.cs @@ -2,7 +2,7 @@ namespace Avalonia.Animation { public interface IAnimationSetter { - AvaloniaProperty Property { get; set; } - object Value { get; set; } + AvaloniaProperty? Property { get; set; } + object? Value { get; set; } } } diff --git a/src/Avalonia.Animation/IAnimator.cs b/src/Avalonia.Animation/IAnimator.cs index d0fb173c54..f64ac9f913 100644 --- a/src/Avalonia.Animation/IAnimator.cs +++ b/src/Avalonia.Animation/IAnimator.cs @@ -11,11 +11,11 @@ namespace Avalonia.Animation /// /// The target property. /// - AvaloniaProperty Property {get; set;} + AvaloniaProperty? Property {get; set;} /// /// Applies the current KeyFrame group to the specified control. /// - IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable match, Action onComplete); + IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable match, Action? onComplete); } } diff --git a/src/Avalonia.Animation/ITransition.cs b/src/Avalonia.Animation/ITransition.cs index ade2ec8b9e..241ca208d1 100644 --- a/src/Avalonia.Animation/ITransition.cs +++ b/src/Avalonia.Animation/ITransition.cs @@ -10,7 +10,7 @@ namespace Avalonia.Animation /// /// Applies the transition to the specified . /// - IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue); + IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue); /// /// Gets the property to be animated. diff --git a/src/Avalonia.Animation/IterationCount.cs b/src/Avalonia.Animation/IterationCount.cs index 9463718608..3b52cdab49 100644 --- a/src/Avalonia.Animation/IterationCount.cs +++ b/src/Avalonia.Animation/IterationCount.cs @@ -97,7 +97,7 @@ namespace Avalonia.Animation /// /// The object with which to test equality. /// True if the objects are equal, otherwise false. - public override bool Equals(object o) + public override bool Equals(object? o) { if (o == null) { diff --git a/src/Avalonia.Animation/IterationCountTypeConverter.cs b/src/Avalonia.Animation/IterationCountTypeConverter.cs index 1c63f8cdf1..f64972ff5c 100644 --- a/src/Avalonia.Animation/IterationCountTypeConverter.cs +++ b/src/Avalonia.Animation/IterationCountTypeConverter.cs @@ -6,12 +6,12 @@ namespace Avalonia.Animation { public class IterationCountTypeConverter : TypeConverter { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { return sourceType == typeof(string); } - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { return IterationCount.Parse((string)value); } diff --git a/src/Avalonia.Animation/KeyFrame.cs b/src/Avalonia.Animation/KeyFrame.cs index c2cc1aa051..3ab7a70d90 100644 --- a/src/Avalonia.Animation/KeyFrame.cs +++ b/src/Avalonia.Animation/KeyFrame.cs @@ -19,7 +19,7 @@ namespace Avalonia.Animation { private TimeSpan _ktimeSpan; private Cue _kCue; - private KeySpline _kKeySpline; + private KeySpline? _kKeySpline; public KeyFrame() { @@ -79,7 +79,7 @@ namespace Avalonia.Animation /// Gets or sets the KeySpline of this . /// /// The key spline. - public KeySpline KeySpline + public KeySpline? KeySpline { get { diff --git a/src/Avalonia.Animation/KeySplineTypeConverter.cs b/src/Avalonia.Animation/KeySplineTypeConverter.cs index b026206e5f..eecad3c3ac 100644 --- a/src/Avalonia.Animation/KeySplineTypeConverter.cs +++ b/src/Avalonia.Animation/KeySplineTypeConverter.cs @@ -12,12 +12,12 @@ namespace Avalonia.Animation /// public class KeySplineTypeConverter : TypeConverter { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { return sourceType == typeof(string); } - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { return KeySpline.Parse((string)value, CultureInfo.InvariantCulture); } diff --git a/src/Avalonia.Animation/Transition.cs b/src/Avalonia.Animation/Transition.cs index 4115c95c0f..d307f348c4 100644 --- a/src/Avalonia.Animation/Transition.cs +++ b/src/Avalonia.Animation/Transition.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Animation.Easings; namespace Avalonia.Animation @@ -8,7 +9,7 @@ namespace Avalonia.Animation /// public abstract class Transition : AvaloniaObject, ITransition { - private AvaloniaProperty _prop; + private AvaloniaProperty? _prop; /// /// Gets or sets the duration of the transition. @@ -26,7 +27,8 @@ namespace Avalonia.Animation public Easing Easing { get; set; } = new LinearEasing(); /// - public AvaloniaProperty Property + [DisallowNull] + public AvaloniaProperty? Property { get { @@ -42,16 +44,25 @@ namespace Avalonia.Animation } } + AvaloniaProperty ITransition.Property + { + get => Property ?? throw new InvalidOperationException("Transition has no property specified."); + set => Property = value; + } + /// /// Apply interpolation to the property. /// public abstract IObservable DoTransition(IObservable progress, T oldValue, T newValue); /// - public virtual IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue) + public virtual IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue) { - var transition = DoTransition(new TransitionInstance(clock, Delay, Duration), (T)oldValue, (T)newValue); + if (Property is null) + throw new InvalidOperationException("Transition has no property specified."); + + var transition = DoTransition(new TransitionInstance(clock, Delay, Duration), (T)oldValue!, (T)newValue!); return control.Bind((AvaloniaProperty)Property, transition, Data.BindingPriority.Animation); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs index b522d1961e..9c9494ff87 100644 --- a/src/Avalonia.Animation/TransitionInstance.cs +++ b/src/Avalonia.Animation/TransitionInstance.cs @@ -10,11 +10,11 @@ namespace Avalonia.Animation /// internal class TransitionInstance : SingleSubscriberObservableBase, IObserver { - private IDisposable _timerSubscription; + private IDisposable? _timerSubscription; private TimeSpan _delay; private TimeSpan _duration; private readonly IClock _baseClock; - private TransitionClock _clock; + private TransitionClock? _clock; public TransitionInstance(IClock clock, TimeSpan delay, TimeSpan duration) { @@ -67,7 +67,7 @@ namespace Avalonia.Animation protected override void Unsubscribed() { _timerSubscription?.Dispose(); - _clock.PlayState = PlayState.Stop; + _clock!.PlayState = PlayState.Stop; } protected override void Subscribed() diff --git a/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs index a2c4b0313b..5f22254fb5 100644 --- a/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs @@ -38,8 +38,8 @@ namespace Avalonia.Animation.Animators } /// - public override IDisposable Apply(Animation animation, Animatable control, IClock clock, - IObservable match, Action onComplete) + public override IDisposable? Apply(Animation animation, Animatable control, IClock? clock, + IObservable match, Action? onComplete) { if (TryCreateCustomRegisteredAnimator(out var animator) || TryCreateGradientAnimator(out animator) @@ -135,9 +135,8 @@ namespace Avalonia.Animation.Animators private bool TryCreateCustomRegisteredAnimator([NotNullWhen(true)] out IAnimator? animator) { - if (_brushAnimators.Count > 0) + if (_brushAnimators.Count > 0 && this[0].Value?.GetType() is Type firstKeyType) { - var firstKeyType = this[0].Value.GetType(); foreach (var (match, animatorType) in _brushAnimators) { if (!match(firstKeyType)) diff --git a/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs index 864e12413f..0979de16d0 100644 --- a/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs @@ -58,6 +58,11 @@ namespace Avalonia.Animation.Animators public override IDisposable BindAnimation(Animatable control, IObservable instance) { + if (Property is null) + { + throw new InvalidOperationException("Animator has no property specified."); + } + return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation); } diff --git a/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs index 7c6372aae2..57f9f3c1a5 100644 --- a/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs @@ -24,6 +24,11 @@ namespace Avalonia.Animation.Animators public override IDisposable BindAnimation(Animatable control, IObservable instance) { + if (Property is null) + { + throw new InvalidOperationException("Animator has no property specified."); + } + return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation); } } diff --git a/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs index 1d7bfd3748..34ec8ac503 100644 --- a/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs @@ -14,10 +14,15 @@ namespace Avalonia.Animation.Animators DoubleAnimator? _doubleAnimator; /// - public override IDisposable? Apply(Animation animation, Animatable control, IClock clock, IObservable obsMatch, Action onComplete) + public override IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable obsMatch, Action? onComplete) { var ctrl = (Visual)control; + if (Property is null) + { + throw new InvalidOperationException("Animator has no property specified."); + } + // Check if the Target Property is Transform derived. if (typeof(Transform).IsAssignableFrom(Property.OwnerType)) { From 98d061ec30b65b63ec98d9c45e52332d65a6db21 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 22 Dec 2021 15:53:12 +0100 Subject: [PATCH 03/56] Fix failing unit tests. --- .../AnimatableTests.cs | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs index 26d8059eec..1d5296bebd 100644 --- a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs +++ b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs @@ -36,7 +36,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Transition_Is_Not_Applied_To_Initial_Style() { - using (UnitTestApplication.Start(TestServices.RealStyler)) + using (Start()) { var target = CreateTarget(); var control = new Control @@ -74,6 +74,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Transition_Is_Applied_When_Local_Value_Changes() { + using var app = Start(); var target = CreateTarget(); var control = CreateControl(target.Object); @@ -170,6 +171,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Transition_Is_Not_Applied_When_StyleTrigger_Changes_With_LocalValue_Present() { + using var app = Start(); var target = CreateTarget(); var control = CreateControl(target.Object); @@ -195,6 +197,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Transition_Is_Disposed_When_Local_Value_Changes() { + using var app = Start(); var target = CreateTarget(); var control = CreateControl(target.Object); var sub = new Mock(); @@ -211,6 +214,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void New_Transition_Is_Applied_When_Local_Value_Changes() { + using var app = Start(); var target = CreateTarget(); var control = CreateControl(target.Object); @@ -239,6 +243,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Transition_Is_Not_Applied_When_Removed_From_Visual_Tree() { + using var app = Start(); var target = CreateTarget(); var control = CreateControl(target.Object); @@ -266,6 +271,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Animation_Is_Cancelled_When_Transition_Removed() { + using var app = Start(); var target = CreateTarget(); var control = CreateControl(target.Object); var sub = new Mock(); @@ -285,7 +291,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Animation_Is_Cancelled_When_New_Style_Activates() { - using (UnitTestApplication.Start(TestServices.RealStyler)) + using (Start()) { var target = CreateTarget(); var control = CreateStyledControl(target.Object); @@ -301,7 +307,7 @@ namespace Avalonia.Animation.UnitTests target.Verify(x => x.Apply( control, - It.IsAny(), + It.IsAny(), 1.0, 0.5), Times.Once); @@ -315,7 +321,7 @@ namespace Avalonia.Animation.UnitTests [Fact] public void Transition_From_Style_Trigger_Is_Applied() { - using (UnitTestApplication.Start(TestServices.RealStyler)) + using (Start()) { var target = CreateTransition(Control.WidthProperty); var control = CreateStyledControl(transition2: target.Object); @@ -326,7 +332,7 @@ namespace Avalonia.Animation.UnitTests target.Verify(x => x.Apply( control, - It.IsAny(), + It.IsAny(), double.NaN, 100.0), Times.Once); @@ -337,7 +343,7 @@ namespace Avalonia.Animation.UnitTests public void Replacing_Transitions_During_Animation_Does_Not_Throw_KeyNotFound() { // Issue #4059 - using (UnitTestApplication.Start(TestServices.RealStyler)) + using (Start()) { Border target; var clock = new TestClock(); @@ -428,6 +434,13 @@ namespace Avalonia.Animation.UnitTests control.EndBatchUpdate(); } + private static IDisposable Start() + { + var clock = new MockGlobalClock(); + var services = TestServices.RealStyler.With(globalClock: clock); + return UnitTestApplication.Start(services); + } + private static Mock CreateTarget() { return CreateTransition(Visual.OpacityProperty); From 6d5021d1df49a4b112a076be7b6f39842e3dd6f0 Mon Sep 17 00:00:00 2001 From: "DESKTOP-AICEQIT\\niels" Date: Mon, 10 Jan 2022 16:08:33 +0100 Subject: [PATCH 04/56] Only handle a KeyBinding if it's actually been handled. --- src/Avalonia.Input/KeyBinding.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Input/KeyBinding.cs b/src/Avalonia.Input/KeyBinding.cs index 856157eafa..7cb4f443f3 100644 --- a/src/Avalonia.Input/KeyBinding.cs +++ b/src/Avalonia.Input/KeyBinding.cs @@ -35,9 +35,11 @@ namespace Avalonia.Input { if (Gesture?.Matches(args) == true) { - args.Handled = true; - if (Command?.CanExecute(CommandParameter) == true) + if (Command?.CanExecute(CommandParameter) == true) + { + args.Handled = true; Command.Execute(CommandParameter); + } } } } From c9a5090f8da9a77772ce63db1e6b5b56de460367 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 11 Jan 2022 12:28:01 +0000 Subject: [PATCH 05/56] restore debugging functionality on all platforms. --- build/SourceLink.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/SourceLink.props b/build/SourceLink.props index 1e007e01eb..0f77c2a613 100644 --- a/build/SourceLink.props +++ b/build/SourceLink.props @@ -3,7 +3,7 @@ true false true - embedded + full $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb From e389ba133be5a2d31b238b1007a38f914ac8c10e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 11 Jan 2022 12:28:20 +0000 Subject: [PATCH 06/56] make sure that Control Catalog can change the app menu title on osx. --- samples/ControlCatalog/App.xaml | 1 + 1 file changed, 1 insertion(+) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 227b31bf20..d0e1bd885e 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -3,6 +3,7 @@ xmlns:vm="using:ControlCatalog.ViewModels" x:DataType="vm:ApplicationViewModel" x:CompileBindings="True" + Name="Avalonia ControlCatalog" x:Class="ControlCatalog.App"> diff --git a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml index 5f18bac44a..7ab89d96e4 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml @@ -1,13 +1,9 @@ - diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index 30c6d39856..62ed6883c1 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -1,82 +1,38 @@ - diff --git a/src/Avalonia.Themes.Default/SimpleTheme.cs b/src/Avalonia.Themes.Default/SimpleTheme.cs index c0aba17818..1d9f2d5f9d 100644 --- a/src/Avalonia.Themes.Default/SimpleTheme.cs +++ b/src/Avalonia.Themes.Default/SimpleTheme.cs @@ -139,6 +139,10 @@ namespace Avalonia.Themes.Default new StyleInclude(baseUri) { Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml") + }, + new StyleInclude(baseUri) + { + Source = new Uri("avares://Avalonia.Themes.Default/Accents/Base.xaml") } }; _simpleLight = new Styles From eabc2be815d713712a3784edeb7f36df69b76965 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 13 Jan 2022 18:28:57 +0200 Subject: [PATCH 18/56] wip --- .../Accents/BaseDark.xaml | 28 +++++++++---------- .../Accents/BaseLight.xaml | 26 ++++++++--------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml index 7ab89d96e4..1843abebfd 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml @@ -19,20 +19,20 @@ #FFDEDEDE #FF119EDA - - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index 62ed6883c1..8f6155e481 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -19,19 +19,19 @@ #FF000000 #FF086F9E - - - - - - - - - - - - - + + + + + + + + + + + + + From d9df6cf15f8b3f4ed816904d4527c0a13b4811bd Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 13 Jan 2022 18:33:50 +0200 Subject: [PATCH 19/56] wip --- src/Avalonia.Themes.Default/Accents/BaseLight.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index 8f6155e481..6247815303 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -33,6 +33,6 @@ - + From ddb8f9c7deb71125f45f125d5bb2a8410c62aa7e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 14 Jan 2022 13:21:52 +0100 Subject: [PATCH 20/56] Update unit tests to use new default theme URLs. --- tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs | 2 +- tests/Avalonia.UnitTests/TestServices.cs | 2 +- tests/Avalonia.UnitTests/UnitTestApplication.cs | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index 6db4b9e624..f20faa2287 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -528,7 +528,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml var xaml = @" - + "; var styles = AvaloniaRuntimeXamlLoader.Parse(xaml); diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 8d27562146..1e586e3bb1 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -168,7 +168,7 @@ namespace Avalonia.UnitTests }; var baseLight = (IStyle)AvaloniaXamlLoader.Load( - new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default")); + new Uri("avares://Avalonia.Themes.Default/Accents/BaseLight.xaml")); result.Add(baseLight); return result; diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs index 2d37057779..dc10754ea2 100644 --- a/tests/Avalonia.UnitTests/UnitTestApplication.cs +++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs @@ -10,6 +10,7 @@ using System.Reactive.Disposables; using System.Reactive.Concurrency; using Avalonia.Input.Platform; using Avalonia.Animation; +using Avalonia.Shared.PlatformSupport; namespace Avalonia.UnitTests { @@ -29,6 +30,11 @@ namespace Avalonia.UnitTests RegisterServices(); } + static UnitTestApplication() + { + AssetLoader.RegisterResUriParsers(); + } + public static new UnitTestApplication Current => (UnitTestApplication)Application.Current; public TestServices Services => _services; From 54d83f65c3bf281595048591e38107fecc50ab7b Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Fri, 14 Jan 2022 15:49:42 +0200 Subject: [PATCH 21/56] fix tests --- tests/Avalonia.DesignerSupport.TestApp/App.xaml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/Avalonia.DesignerSupport.TestApp/App.xaml b/tests/Avalonia.DesignerSupport.TestApp/App.xaml index 56c0e8059a..5a33ffff80 100644 --- a/tests/Avalonia.DesignerSupport.TestApp/App.xaml +++ b/tests/Avalonia.DesignerSupport.TestApp/App.xaml @@ -1,8 +1,10 @@ - + - - + + + From 0c32ec32a8cdb16801c97b81d9c0698fcb156a58 Mon Sep 17 00:00:00 2001 From: amwx <40413319+amwx@users.noreply.github.com> Date: Sat, 15 Jan 2022 18:37:57 -0500 Subject: [PATCH 22/56] Fix vertical alignments --- src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml | 2 +- src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml | 2 +- src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml index ca538e4b0a..3a6af60983 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml @@ -11,7 +11,7 @@ - + diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml index ff72044c93..9223c0dd71 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml @@ -11,7 +11,7 @@ - + diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml index 5bf3ac11af..2c1031f3e5 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml @@ -32,6 +32,7 @@ diff --git a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml index 3320fc9a41..9aa73fc52e 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml @@ -7,7 +7,8 @@ + xmlns:sys="clr-namespace:System;assembly=netstandard" + x:CompileBindings="True"> 40 1 diff --git a/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml b/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml index debdfb2772..2d18be91cb 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml @@ -1,6 +1,7 @@ + xmlns:sys="clr-namespace:System;assembly=netstandard" + x:CompileBindings="True"> + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:DataType="WindowNotificationManager" + x:CompileBindings="True"> - \ No newline at end of file + From 36d6222c75048cf552b70ae10a7c883dc74fd650 Mon Sep 17 00:00:00 2001 From: Thehx Date: Wed, 19 Jan 2022 20:19:41 +0300 Subject: [PATCH 37/56] Fixed and exposed PreciseArcTo for ellipses with extreme width:height ratios --- .../Media/PreciseEllipticArcHelper.cs} | 45 ++++++++++---- .../Media/StreamGeometryContext.cs | 24 ++++++++ .../RenderHelpers/RenderHelpers.projitems | 1 - .../Media/StreamGeometryTests.cs | 56 ++++++++++++++++++ ...cs_In_All_Directions.deferred.expected.png | Bin 0 -> 6332 bytes ...s_In_All_Directions.immediate.expected.png | Bin 0 -> 6332 bytes ...cs_In_All_Directions.deferred.expected.png | Bin 0 -> 5828 bytes ...s_In_All_Directions.immediate.expected.png | Bin 0 -> 5828 bytes 8 files changed, 112 insertions(+), 14 deletions(-) rename src/{Shared/RenderHelpers/ArcToHelper.cs => Avalonia.Visuals/Media/PreciseEllipticArcHelper.cs} (97%) create mode 100644 tests/Avalonia.RenderTests/Media/StreamGeometryTests.cs create mode 100644 tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png create mode 100644 tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png create mode 100644 tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png create mode 100644 tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png diff --git a/src/Shared/RenderHelpers/ArcToHelper.cs b/src/Avalonia.Visuals/Media/PreciseEllipticArcHelper.cs similarity index 97% rename from src/Shared/RenderHelpers/ArcToHelper.cs rename to src/Avalonia.Visuals/Media/PreciseEllipticArcHelper.cs index 0bbf451970..5dd647e8ca 100644 --- a/src/Shared/RenderHelpers/ArcToHelper.cs +++ b/src/Avalonia.Visuals/Media/PreciseEllipticArcHelper.cs @@ -1,5 +1,6 @@ // Copyright © 2003-2004, Luc Maisonobe // 2015 - Alexey Rozanov - Adaptations for Avalonia and oval center computations +// 2022 - Alexey Rozanov - Fix for arcs sometimes drawn in inverted order. // All rights reserved. // // Redistribution and use in source and binary forms, with @@ -49,12 +50,10 @@ // Adapted from http://www.spaceroots.org/documents/ellipse/EllipticalArc.java using System; -using Avalonia.Media; -using Avalonia.Platform; -namespace Avalonia.RenderHelpers +namespace Avalonia.Media { - static class ArcToHelper + static class PreciseEllipticArcHelper { /// /// This class represents an elliptical arc on a 2D plane. @@ -292,6 +291,8 @@ namespace Avalonia.RenderHelpers /// internal double G2; + public bool DrawInOppositeDirection { get; set; } + /// /// Builds an elliptical arc composed of the full unit circle around (0,0) /// @@ -850,7 +851,7 @@ namespace Avalonia.RenderHelpers /// Builds the arc outline using given StreamGeometryContext and default (max) Bezier curve degree and acceptable error of half a pixel (0.5) /// /// A StreamGeometryContext to output the path commands to - public void BuildArc(IStreamGeometryContextImpl path) + public void BuildArc(StreamGeometryContext path) { BuildArc(path, _maxDegree, _defaultFlatness, true); } @@ -862,7 +863,7 @@ namespace Avalonia.RenderHelpers /// degree of the Bezier curve to use /// acceptable error /// if true, a new figure will be started in the specified StreamGeometryContext - public void BuildArc(IStreamGeometryContextImpl path, int degree, double threshold, bool openNewFigure) + public void BuildArc(StreamGeometryContext path, int degree, double threshold, bool openNewFigure) { if (degree < 1 || degree > _maxDegree) throw new ArgumentException($"degree should be between {1} and {_maxDegree}", nameof(degree)); @@ -888,8 +889,18 @@ namespace Avalonia.RenderHelpers } n = n << 1; } - dEta = (Eta2 - Eta1) / n; - etaB = Eta1; + if (!DrawInOppositeDirection) + { + dEta = (Eta2 - Eta1) / n; + etaB = Eta1; + } + else + { + dEta = (Eta1 - Eta2) / n; + etaB = Eta2; + } + + double cosEtaB = Math.Cos(etaB); double sinEtaB = Math.Sin(etaB); double aCosEtaB = A * cosEtaB; @@ -922,6 +933,7 @@ namespace Avalonia.RenderHelpers */ //otherwise we're supposed to be already at the (xB,yB) + double t = Math.Tan(0.5 * dEta); double alpha = Math.Sin(dEta) * (Math.Sqrt(4 + 3 * t * t) - 1) / 3; @@ -1012,7 +1024,7 @@ namespace Avalonia.RenderHelpers /// Ellipse theta (angle measured from the abscissa) /// Large Arc Indicator /// Clockwise direction flag - public static void BuildArc(IStreamGeometryContextImpl path, Point p1, Point p2, Size size, double theta, bool isLargeArc, bool clockwise) + public static void BuildArc(StreamGeometryContext path, Point p1, Point p2, Size size, double theta, bool isLargeArc, bool clockwise) { // var orthogonalizer = new RotateTransform(-theta); @@ -1058,7 +1070,7 @@ namespace Avalonia.RenderHelpers } - double multiplier = Math.Sqrt(numerator / denominator); + double multiplier = Math.Sqrt(Math.Abs(numerator / denominator)); Point mulVec = new Point(rx * p1S.Y / ry, -ry * p1S.X / rx); int sign = (clockwise != isLargeArc) ? 1 : -1; @@ -1104,9 +1116,16 @@ namespace Avalonia.RenderHelpers // path.LineTo(c, true, true); // path.LineTo(clockwise ? p1 : p2, true,true); - path.LineTo(clockwise ? p1 : p2); var arc = new EllipticalArc(c.X, c.Y, rx, ry, theta, thetaStart, thetaEnd, false); + + double ManhattanDistance(Point p1, Point p2) => Math.Abs(p1.X - p2.X) + Math.Abs(p1.Y - p2.Y); + if (ManhattanDistance(p2, new Point(arc.X2, arc.Y2)) > ManhattanDistance(p2, new Point(arc.X1, arc.Y1))) + { + arc.DrawInOppositeDirection = true; + } + arc.BuildArc(path, arc._maxDegree, arc._defaultFlatness, false); + //path.LineTo(p2); //uncomment this to draw a pie //path.LineTo(c, true, true); @@ -1136,9 +1155,9 @@ namespace Avalonia.RenderHelpers } } - public static void ArcTo(IStreamGeometryContextImpl streamGeometryContextImpl, Point currentPoint, Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) + public static void ArcTo(StreamGeometryContext streamGeometryContextImpl, Point currentPoint, Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) { - EllipticalArc.BuildArc(streamGeometryContextImpl, currentPoint, point, size, rotationAngle*Math.PI/180, + EllipticalArc.BuildArc(streamGeometryContextImpl, currentPoint, point, size, rotationAngle*(Math.PI/180), isLargeArc, sweepDirection == SweepDirection.Clockwise); } diff --git a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs index 0bfd774c79..88aba8365e 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs @@ -15,6 +15,8 @@ namespace Avalonia.Media { private readonly IStreamGeometryContextImpl _impl; + private Point _currentPoint; + /// /// Initializes a new instance of the class. /// @@ -47,6 +49,24 @@ namespace Avalonia.Media public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) { _impl.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection); + _currentPoint = point; + } + + + /// + /// Draws an arc to the specified point using polylines, quadratic or cubic Bezier curves + /// Significantly more precise when drawing elliptic arcs with extreme width:height ratios. + /// + /// The destination point. + /// The radii of an oval whose perimeter is used to draw the angle. + /// The rotation angle of the oval that specifies the curve. + /// true to draw the arc greater than 180 degrees; otherwise, false. + /// + /// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction. + /// + public void PreciseArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) + { + PreciseEllipticArcHelper.ArcTo(this, _currentPoint, point, size, rotationAngle, isLargeArc, sweepDirection); } /// @@ -57,6 +77,7 @@ namespace Avalonia.Media public void BeginFigure(Point startPoint, bool isFilled) { _impl.BeginFigure(startPoint, isFilled); + _currentPoint = startPoint; } /// @@ -68,6 +89,7 @@ namespace Avalonia.Media public void CubicBezierTo(Point point1, Point point2, Point point3) { _impl.CubicBezierTo(point1, point2, point3); + _currentPoint = point3; } /// @@ -78,6 +100,7 @@ namespace Avalonia.Media public void QuadraticBezierTo(Point control, Point endPoint) { _impl.QuadraticBezierTo(control, endPoint); + _currentPoint = endPoint; } /// @@ -87,6 +110,7 @@ namespace Avalonia.Media public void LineTo(Point point) { _impl.LineTo(point); + _currentPoint = point; } /// diff --git a/src/Shared/RenderHelpers/RenderHelpers.projitems b/src/Shared/RenderHelpers/RenderHelpers.projitems index c088097a9f..4c80ec50c4 100644 --- a/src/Shared/RenderHelpers/RenderHelpers.projitems +++ b/src/Shared/RenderHelpers/RenderHelpers.projitems @@ -9,7 +9,6 @@ Avalonia.RenderHelpers - \ No newline at end of file diff --git a/tests/Avalonia.RenderTests/Media/StreamGeometryTests.cs b/tests/Avalonia.RenderTests/Media/StreamGeometryTests.cs new file mode 100644 index 0000000000..fc9cbf6a7f --- /dev/null +++ b/tests/Avalonia.RenderTests/Media/StreamGeometryTests.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Xunit; + +#if AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else +namespace Avalonia.Direct2D1.RenderTests.Media +#endif +{ + public class StreamGeometryTests : TestBase + { + public StreamGeometryTests() + : base(@"Media\StreamGeometry") + { + } + + [Fact] + public async Task PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions() + { + var grid = new Avalonia.Controls.Primitives.UniformGrid() { Columns = 2, Rows = 4, Width = 320, Height = 400 }; + foreach (var sweepDirection in new[] { SweepDirection.Clockwise, SweepDirection.CounterClockwise }) + foreach (var isLargeArc in new[] { false, true }) + foreach (var isPrecise in new[] { false, true }) + { + Point Pt(double x, double y) => new Point(x, y); + Size Sz(double w, double h) => new Size(w, h); + var streamGeometry = new StreamGeometry(); + using (var context = streamGeometry.Open()) + { + context.BeginFigure(Pt(20, 20), true); + + if(isPrecise) + context.PreciseArcTo(Pt(40, 40), Sz(20, 20), 0, isLargeArc, sweepDirection); + else + context.ArcTo(Pt(40, 40), Sz(20, 20), 0, isLargeArc, sweepDirection); + context.LineTo(Pt(40, 20)); + context.LineTo(Pt(20, 20)); + context.EndFigure(true); + } + var pathShape = new Avalonia.Controls.Shapes.Path(); + pathShape.Data = streamGeometry; + pathShape.Stroke = new SolidColorBrush(Colors.CornflowerBlue); + pathShape.Fill = new SolidColorBrush(Colors.Gold); + pathShape.StrokeThickness = 2; + pathShape.Margin = new Thickness(20); + grid.Children.Add(pathShape); + } + await RenderToFile(grid); + } + } +} diff --git a/tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png b/tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..825b1d7ea7d2d1356f2798c162fed7572f8eb27b GIT binary patch literal 6332 zcmc(jcQ9Q4`p4G_kw~JqsL@Ff1VPq{=)ET_Hi_ONdXyC+(V|53mgrHVE*2{hErRG} zu_9P3mQ|O7yLja&AftvV?0svrj&{BP98ep?I zciqQyIj3jmT$+u_^Kz7yX4nF-OSC^Wpd%Cia?IQx3xPoPpU^PNtA3`{MvYr1wcIg_ zt@3-^_|OQL`aHIhi^dlfQ)yhGS<@9R!&o6=c!gF)g^Kc)-HOu5gP~cciDUZSUjsQA zI863Z;Fh;WhFvHDeKH$_i6_0(f92re6h>aRnjWFt^e2BTFNEw5^>{;0h+x`*0D2WZ zqv`cdaNTF}%{I~aLe+XgOfUcNv38N^Yin*J$&7n1-UmNB@-KoODjcGN)3YY!no`B? zD!BM{eNz(S?aJp943ZbnZl!KWm1rD-I`1UoiwuI_5sN<8hure`a{qyR)m~aks>Mpl z>af@dZ{Eo}ftSm_am|LIw7@94CrySLG>o#TnZyCwVUv{7(gC!n13N zSQK3krhFn_{mG##o4A&X$*ERz@w;au91DLSbQ1r5kDIJ;Fb1E${u56u=d;w4jBW6| zr_T#>=C{=kugHcp2^8C6vbIuN{&rH`0`5Bz*w_V%2h^W<))7v&QZe@k)W|pm>ejN> z(ndtQwcEj^JeTOhR2=fa*4bm=a1!{%-jT0aK}oH?0>p-^bUVw`zp*{Z8z;3TmHzH$ zY^JxbTgyak^Pe9XwH51cZ3ksDJdFQwzlHEY-7`A9gD>X_9P)wwvFt0VL>@ z&{r`hgNBk9<__d#+<>~bY^5)Z!rG*3fS;3-D1O(DJ}csT>^xk9ISkdiX>bic3Hahz z(n(iY)o{8mBNq!2kJ|2=0y>$cV%$x5!A|=x?Kig+iAFrETR);E5>Uj`=R?S&0;=RV z@i6o9c(k+1RBiKwd)#Yf=V*!AI$Au7J{uIUY5L^0FZgo)Fw=%_IB!`vG18=)MOo)I zAsS5qECJy(LRoGlGf}))-XMxr?sLKDs6Mu7qHU5EBzRS8^8uFHU-C|@Kiz(?45vz} z5DBv!yB*3xm5m>xCe8aGLt)r@--OmDhogEPcT2Ul1R$CKj1o^ScsVm4>+P++oyMR_23R>lfd;qZ5l2I985euZ^aWw*v z*@<1{39C>eyOh`<9QI$)(tmi!r`?Zehir)G4TE?QYnRXtqZQZsiIxs%%BMng=jj50 zT5FBHp~r)R)uhjpV{SftUtcG*DfVhFK_Pjgx%QPl^d|by{fyHS>CP=dSVU3Fnn~^7Z8c;6;B;j7 z?^@Z#z+LXsSAX1m{}^(nWVTnY%W?E3 zd+yx6rk~R+XyZ;`yF;;?!i!D>VN`n#V(i~N7t5UU{hX?V-<|sD^oL{m=(6$RAS1{h z#PiidJLRFd=*!u~#mVGLj)<+qv;%ZH>+hB_v&FP3QHfd;#t!E^FyevNLT2F-F6Rk2 z4Y@C4?=k&Lv$4yED1}eE>1+WgG2^%sU$*M`D;eCQ@9r3vE#Qc0+o;xyU*{{rPsPVH ze$lF2?X~A9eLJoM^D9#fS+bIz3mJrFaUkOL6PZwHNk~Q=1n@p$@o64&^qHlINWJiJiF+qL~p$mzQ()=bbhTVDC(E6@B~^N@@4o9tEYz-9>a82)Ld|tns+v3JAw9E$I0^j# zXA?lb=sL1e)jBWBTX*F>qH6SVmOKf=ZL_NjSEsM#^tar- zRl(u8XT9i<>NAx^1)*Y}K>`QOMr-{Pd0gO5muxVG1vF>>r|#|H9B-b;mwN_kYHgZ! zHQ=~wEM`V&$la@<)5`>ePp?!O*l5KQn8SPwQidO50uh_{7OF;!YcA&@KD6m$C*CX0 zc9mK73@*;`W(*+bJ*(R$##SVHA4zckSiv3WLH2i^fR{zsI)kPZh4zBNyd;!H8~t$A z<*|9=w`cfGD5tT|%|Gd@4<;XWj~FdiX*N1ON3o6LXj|2O2EU$)L~z0Ngsr5O=q6o^CXTtd_L))g~sS)`Wib= z+SgM%my%EAn$4Z%Cw&tGKQ;@l(gYkn*d?OSrwrAey^B*5hgXE4A*)sVXlVaI&vm zyfI_Ps2%u6w{a$bH{c|bf#UZgs!3FW`F7&{V%XESl%;SFam5l`56_8Q8IX%zpM14SGR zUQG=FtCSYEf(_0-!~u`!Zt7=?9G*(jDV^l@YtAClX&z}9ZlJ`tr;c?OGfnVm^o$)e z&mO@^3KoB3x!0H;Pp5RLdu50jj8C&<>^Q7RYpNjhhrB%G&h;Oqv3*n(Z(w0&V~XVh z6efa}@JH-4iO4WRK!~5tiRnIX2GY44F~(En{oE33AAucg+WU;pcQ}8htUblx=GTc> z@Q1x%QAkEUwOhV!gv$xYy`H`i5)@}DXU&7i6zr`kV3@THW>?i{?<=CzEIuH(zQ?pU zZ&eliweR^ujYc363nl}#=Ne!7DZyK`Ybx%9asQ}rGSAPui%eolsiSV{r2Mc|fESu^ zrDw9(44f}Z@ur3!6=+^$I=IyIT#qV~MZO*=K5omtUGrX7CQ%zVlxJLP68lOZf3G_k zgr4tR7W)qf@V^U>q~TG9qL7(Sryj;;gMBpnL+@Ykz%nEj;h*hBCO7o5X1U0N*9UM%1-*=)CH^0I zWxIrAIgesUY8>RI2&WsZA;`iYSP9G%pSN1Ez$Oywo8N98I^Zs|6w?#B+~mbWB@1(q zvU+yXU&i&l(r+oo|X$$S*FM;HOw=-FKr_mQ2d7|DgBMsGm+NcJ)%TV?T8* zZb}m-mS`ypvr$c%Ax>vR4R8yok361XQtp3yw*+#ytLGmv2ZUh`*_Ylaa@%>*zWz#x z)gq1-W8<&PAND|dW5D2FzTwWoV$`4POZYD2I;N|^wcWZJO@b+5II>j_(D8!oe!OSvPaR8JEp5?JH89JcMNI4lwF+REiP~@X zL(K3m54CI(PHPwZA+vv*ZwY*GR`2>oC+J5#gBepu71%`8#&mmG29D|;-rS8y;a5NBB2yJ@B5DN}3;PNXbZFR4R6c@Cp=Av%D$Z<` zDdKlWKhoFvipzq%Xn;|sbzO%1p+)BFRt$~6%6&rT3N-l_|?{suT8P#FQlWE|T285>OY+ka}d74A;sZH>9)H zJ(Gf_wbTAXWQU8=1EWoScbJ9LO%ed#MIpw}{DQJyK91?iANffkVx&e|D{bR;SL@O9 zy2!S9?hho4?3)gh=-+Oy{<$!($Y0$Px`sOaz*ale9wVXEti$n7f|kzac(Tqy5Vc%ZO#k+}Z6rv6O=MI^wX`VfO_ z4Tt`eaXdv_TWv;F(f|q6c_cm_+lYw>Ik@h01Z8iZ&9;@o&0?2QjpQy{5)<11m=Ba?2O*`L{Z8m>_ZF`g-gY?5y2)KCPE~K4k>xG*qi* zwkM~p?C1N@d6jtiM~6acJMNs#Qvgw$7Gj^iofodk6MkDKq62_ zAHczh9~nE?m@aDw565oWQ%b83(zC1Z2+P9qTM=%s+uML{)7Yg_r0Q1IP0kv12MhDpjlAN0` zZQO<2irJnSwB*d?*$y2AXo-bM7U{Svwr{hM>U!VY&=4zR`*uwa;B`Xs7|`k9Onths z>2U+8Vz1!k3Jp>q044oe>IEm%%n5<)p^3KMS20hxs)j%VUGw|fbAA{a#O)z#`nAHj0(X!ZOl03YyypniC8NM5 zI!18SUosGxUQ?ioEM!&|OWJG<`P-`s=MWs%W8qiO4v5u$d)52QkCPK;DYniu`B0c2 z4p844a)*p>TadC4nX>}VWnZ^yh(012csB&e7kkL-`f;@XZlEiYYq_Cv>&rzyBsl0w zXeH>+S4?RL#NqxJmtw!$YoYWRp@C+4zA(13+6SIl&Mw$^8t>}BCmV{gBO%cTnfeF8 zS;R=qmj>ai#l20a7ev2v1a}L1S(Yn(-BRE<`l69E2b3B&IY8~oNYAOT68-Yd;h%WR zAM0`Z@z#Xh)qF)3r_QTWzdCi{wbzyauzGAO16D|46De8q$xbalLbu4*E!tghEIKSmqnr+>2CGi?O zzqNEm_mN66vH`JjLKwxG4~i{G%Rdraqxd~sLvS#eeSX+lme8bV=X)h+?Hi@^>DQpb zd_MHvyQDw7xpL_C6|C#e*&1}!^IgYt$_?+Gk1Ye->QRM-fSJm#=Ib^nn43mq7UiXZ z+$f9M&5~$1kMoy3nnXV82gm#0a~zmoCTvzuwO8oi9Sy$M59PtaX>TQGf|U6tu&Zqj z5?_DjCP9vmX}a{a!k?3$FSvr;6)BrPFBzU$!GRV9gdaTAV2wx98SliJ zmLx!htpH2$H$Q_Wd}22>mfMvkPHaL-LZf>=GMDN zcMZ3QF0Q#=;hw1@By$8#Y+Q+N{Kq&qyG+5fJq9Ju=}LOIFpxud;PavTLiz!TNFVwq zWc9_MMuf&*PCK=(wg;TsW{%XlHKD9b9m>3$yZ-J8N^Fzlh?cuTM$v5xfl#capIzR1-E(BDh`3LuLFL4^Aco;grl4LykK)aEvCEvX)OZjZXJdLqKwu)AL z>;c0M?10G{j>bP~qGg`w@2ZBmBM@+(vx-*KGk49#_KfzJUtRGn(f+NtSeQGr_}DOc z7yLja&AftvV?0svrj&{BP98ep?I zciqQyIj3jmT$+u_^Kz7yX4nF-OSC^Wpd%Cia?IQx3xPoPpU^PNtA3`{MvYr1wcIg_ zt@3-^_|OQL`aHIhi^dlfQ)yhGS<@9R!&o6=c!gF)g^Kc)-HOu5gP~cciDUZSUjsQA zI863Z;Fh;WhFvHDeKH$_i6_0(f92re6h>aRnjWFt^e2BTFNEw5^>{;0h+x`*0D2WZ zqv`cdaNTF}%{I~aLe+XgOfUcNv38N^Yin*J$&7n1-UmNB@-KoODjcGN)3YY!no`B? zD!BM{eNz(S?aJp943ZbnZl!KWm1rD-I`1UoiwuI_5sN<8hure`a{qyR)m~aks>Mpl z>af@dZ{Eo}ftSm_am|LIw7@94CrySLG>o#TnZyCwVUv{7(gC!n13N zSQK3krhFn_{mG##o4A&X$*ERz@w;au91DLSbQ1r5kDIJ;Fb1E${u56u=d;w4jBW6| zr_T#>=C{=kugHcp2^8C6vbIuN{&rH`0`5Bz*w_V%2h^W<))7v&QZe@k)W|pm>ejN> z(ndtQwcEj^JeTOhR2=fa*4bm=a1!{%-jT0aK}oH?0>p-^bUVw`zp*{Z8z;3TmHzH$ zY^JxbTgyak^Pe9XwH51cZ3ksDJdFQwzlHEY-7`A9gD>X_9P)wwvFt0VL>@ z&{r`hgNBk9<__d#+<>~bY^5)Z!rG*3fS;3-D1O(DJ}csT>^xk9ISkdiX>bic3Hahz z(n(iY)o{8mBNq!2kJ|2=0y>$cV%$x5!A|=x?Kig+iAFrETR);E5>Uj`=R?S&0;=RV z@i6o9c(k+1RBiKwd)#Yf=V*!AI$Au7J{uIUY5L^0FZgo)Fw=%_IB!`vG18=)MOo)I zAsS5qECJy(LRoGlGf}))-XMxr?sLKDs6Mu7qHU5EBzRS8^8uFHU-C|@Kiz(?45vz} z5DBv!yB*3xm5m>xCe8aGLt)r@--OmDhogEPcT2Ul1R$CKj1o^ScsVm4>+P++oyMR_23R>lfd;qZ5l2I985euZ^aWw*v z*@<1{39C>eyOh`<9QI$)(tmi!r`?Zehir)G4TE?QYnRXtqZQZsiIxs%%BMng=jj50 zT5FBHp~r)R)uhjpV{SftUtcG*DfVhFK_Pjgx%QPl^d|by{fyHS>CP=dSVU3Fnn~^7Z8c;6;B;j7 z?^@Z#z+LXsSAX1m{}^(nWVTnY%W?E3 zd+yx6rk~R+XyZ;`yF;;?!i!D>VN`n#V(i~N7t5UU{hX?V-<|sD^oL{m=(6$RAS1{h z#PiidJLRFd=*!u~#mVGLj)<+qv;%ZH>+hB_v&FP3QHfd;#t!E^FyevNLT2F-F6Rk2 z4Y@C4?=k&Lv$4yED1}eE>1+WgG2^%sU$*M`D;eCQ@9r3vE#Qc0+o;xyU*{{rPsPVH ze$lF2?X~A9eLJoM^D9#fS+bIz3mJrFaUkOL6PZwHNk~Q=1n@p$@o64&^qHlINWJiJiF+qL~p$mzQ()=bbhTVDC(E6@B~^N@@4o9tEYz-9>a82)Ld|tns+v3JAw9E$I0^j# zXA?lb=sL1e)jBWBTX*F>qH6SVmOKf=ZL_NjSEsM#^tar- zRl(u8XT9i<>NAx^1)*Y}K>`QOMr-{Pd0gO5muxVG1vF>>r|#|H9B-b;mwN_kYHgZ! zHQ=~wEM`V&$la@<)5`>ePp?!O*l5KQn8SPwQidO50uh_{7OF;!YcA&@KD6m$C*CX0 zc9mK73@*;`W(*+bJ*(R$##SVHA4zckSiv3WLH2i^fR{zsI)kPZh4zBNyd;!H8~t$A z<*|9=w`cfGD5tT|%|Gd@4<;XWj~FdiX*N1ON3o6LXj|2O2EU$)L~z0Ngsr5O=q6o^CXTtd_L))g~sS)`Wib= z+SgM%my%EAn$4Z%Cw&tGKQ;@l(gYkn*d?OSrwrAey^B*5hgXE4A*)sVXlVaI&vm zyfI_Ps2%u6w{a$bH{c|bf#UZgs!3FW`F7&{V%XESl%;SFam5l`56_8Q8IX%zpM14SGR zUQG=FtCSYEf(_0-!~u`!Zt7=?9G*(jDV^l@YtAClX&z}9ZlJ`tr;c?OGfnVm^o$)e z&mO@^3KoB3x!0H;Pp5RLdu50jj8C&<>^Q7RYpNjhhrB%G&h;Oqv3*n(Z(w0&V~XVh z6efa}@JH-4iO4WRK!~5tiRnIX2GY44F~(En{oE33AAucg+WU;pcQ}8htUblx=GTc> z@Q1x%QAkEUwOhV!gv$xYy`H`i5)@}DXU&7i6zr`kV3@THW>?i{?<=CzEIuH(zQ?pU zZ&eliweR^ujYc363nl}#=Ne!7DZyK`Ybx%9asQ}rGSAPui%eolsiSV{r2Mc|fESu^ zrDw9(44f}Z@ur3!6=+^$I=IyIT#qV~MZO*=K5omtUGrX7CQ%zVlxJLP68lOZf3G_k zgr4tR7W)qf@V^U>q~TG9qL7(Sryj;;gMBpnL+@Ykz%nEj;h*hBCO7o5X1U0N*9UM%1-*=)CH^0I zWxIrAIgesUY8>RI2&WsZA;`iYSP9G%pSN1Ez$Oywo8N98I^Zs|6w?#B+~mbWB@1(q zvU+yXU&i&l(r+oo|X$$S*FM;HOw=-FKr_mQ2d7|DgBMsGm+NcJ)%TV?T8* zZb}m-mS`ypvr$c%Ax>vR4R8yok361XQtp3yw*+#ytLGmv2ZUh`*_Ylaa@%>*zWz#x z)gq1-W8<&PAND|dW5D2FzTwWoV$`4POZYD2I;N|^wcWZJO@b+5II>j_(D8!oe!OSvPaR8JEp5?JH89JcMNI4lwF+REiP~@X zL(K3m54CI(PHPwZA+vv*ZwY*GR`2>oC+J5#gBepu71%`8#&mmG29D|;-rS8y;a5NBB2yJ@B5DN}3;PNXbZFR4R6c@Cp=Av%D$Z<` zDdKlWKhoFvipzq%Xn;|sbzO%1p+)BFRt$~6%6&rT3N-l_|?{suT8P#FQlWE|T285>OY+ka}d74A;sZH>9)H zJ(Gf_wbTAXWQU8=1EWoScbJ9LO%ed#MIpw}{DQJyK91?iANffkVx&e|D{bR;SL@O9 zy2!S9?hho4?3)gh=-+Oy{<$!($Y0$Px`sOaz*ale9wVXEti$n7f|kzac(Tqy5Vc%ZO#k+}Z6rv6O=MI^wX`VfO_ z4Tt`eaXdv_TWv;F(f|q6c_cm_+lYw>Ik@h01Z8iZ&9;@o&0?2QjpQy{5)<11m=Ba?2O*`L{Z8m>_ZF`g-gY?5y2)KCPE~K4k>xG*qi* zwkM~p?C1N@d6jtiM~6acJMNs#Qvgw$7Gj^iofodk6MkDKq62_ zAHczh9~nE?m@aDw565oWQ%b83(zC1Z2+P9qTM=%s+uML{)7Yg_r0Q1IP0kv12MhDpjlAN0` zZQO<2irJnSwB*d?*$y2AXo-bM7U{Svwr{hM>U!VY&=4zR`*uwa;B`Xs7|`k9Onths z>2U+8Vz1!k3Jp>q044oe>IEm%%n5<)p^3KMS20hxs)j%VUGw|fbAA{a#O)z#`nAHj0(X!ZOl03YyypniC8NM5 zI!18SUosGxUQ?ioEM!&|OWJG<`P-`s=MWs%W8qiO4v5u$d)52QkCPK;DYniu`B0c2 z4p844a)*p>TadC4nX>}VWnZ^yh(012csB&e7kkL-`f;@XZlEiYYq_Cv>&rzyBsl0w zXeH>+S4?RL#NqxJmtw!$YoYWRp@C+4zA(13+6SIl&Mw$^8t>}BCmV{gBO%cTnfeF8 zS;R=qmj>ai#l20a7ev2v1a}L1S(Yn(-BRE<`l69E2b3B&IY8~oNYAOT68-Yd;h%WR zAM0`Z@z#Xh)qF)3r_QTWzdCi{wbzyauzGAO16D|46De8q$xbalLbu4*E!tghEIKSmqnr+>2CGi?O zzqNEm_mN66vH`JjLKwxG4~i{G%Rdraqxd~sLvS#eeSX+lme8bV=X)h+?Hi@^>DQpb zd_MHvyQDw7xpL_C6|C#e*&1}!^IgYt$_?+Gk1Ye->QRM-fSJm#=Ib^nn43mq7UiXZ z+$f9M&5~$1kMoy3nnXV82gm#0a~zmoCTvzuwO8oi9Sy$M59PtaX>TQGf|U6tu&Zqj z5?_DjCP9vmX}a{a!k?3$FSvr;6)BrPFBzU$!GRV9gdaTAV2wx98SliJ zmLx!htpH2$H$Q_Wd}22>mfMvkPHaL-LZf>=GMDN zcMZ3QF0Q#=;hw1@By$8#Y+Q+N{Kq&qyG+5fJq9Ju=}LOIFpxud;PavTLiz!TNFVwq zWc9_MMuf&*PCK=(wg;TsW{%XlHKD9b9m>3$yZ-J8N^Fzlh?cuTM$v5xfl#capIzR1-E(BDh`3LuLFL4^Aco;grl4LykK)aEvCEvX)OZjZXJdLqKwu)AL z>;c0M?10G{j>bP~qGg`w@2ZBmBM@+(vx-*KGk49#_KfzJUtRGn(f+NtSeQGr_}DOc zOA6eVho1kW$`KJWWJ@4MbV?jO&3vQ}2I&hM;q_HUoP_xH2UJ3|9)W=1YX5D3H! z(Ya>~0#T^}-`KNsz{qS_!&~5o*6S|BA7|t|s{Oj$e3X z8HNyW-*57!v41Q+qRq{9?}+jd2^=CkF+!?!0&qNns zerQjUlI=0Q!ua*7=$*_YEUU`td1A5T2|vqs`e<W zAnA&IuW0ReYRKtANOf~l<{_^aUV$S@7YJ*Fy(XTo$*bh!MDiIMQ+YK=kG>*di09+i?pu_|tM`3alpB&eybLyKpz8Zv?`K%%p{g|K>enSmM5z!QljiOix}DqZ4R zC&God0_=!2m?BzAX6N8j2FDpXBU6Px7q$jR?L+-K2*!rNo>v`1!Rh4eX}s>1eo+gR zk4IFL&i6&w6}0|z>N9qX5&xTlHSSEX{gr}A0+(SE&*BRZ3;M(Jptm_&g4lI6H_?(a zC@iSx9i1>l`t}-(0qqosU=J!U&Z2uxB8yYW^;Pf$(V!i;GP~buk;|xRBFJYRZpf-> z)1GU&iu`0E!~6p2STajlx{L4M5r=M(6s@$B^td zbmsR|1PWjj!;iA@;d#hNp_q&JT8~vAalB)7!*U`=@p^S4 z|ANob4*uke4unu6$TVWbsZ@p|1fVsuq{bw8On_wCg%8;Cj^A^yFgRZ3++!^rtNFRG z&RtzQm^@^r^)}D^7xRn7micMv@7EyS^Tqpx9pY8~6L5v!H++86mtXy|%zPw_={9g6 z^qwLszI#3KI>s%SM>$K$AW_w8C%npIyF07F8ym@8TCV`p_Mn!Wq^cVwdPgPjFcM z#kRqH$O`yI%W^ea0B@NOj%6fPZFY7DH%i{g$Mb$Klk1%kTT_XK&SVO(`>a{{RFsT` z*{1QuVi$_*>5^5qSB}6JEfIs3ZgAJ>X6?YLlrF8^D%F5Y)zev+;Dk|*;hS@~+7zPo zdF8SJf-(pSp#DP>C-|4~|N*qARsS*G9%&Kb<^X_dml9WR|R3`f-3=6A1(oajSqi#S=P&^u zC04yBU)Gr8p6kG*{2u32%ml635UVBGL<0d~ZgW^9C+)_4;BWyk9y~(P2x+0KkO%T7 zZFuKxVmz2LUdPA&L*UJPM%orCt3@HfRUld)4tOU`++d7Z1O5xxlrxX@))4nE`O@4)tH`24xnm*4ui$dqPGjsP))s0>39|k%jt#Bku`krSPa4CH2CU!lU zO=4uTk`jm_}CDYks$olIcCXvwznNX;ml)C%sm3##QHic z#WdYrxjMu=)N_Y_na_bscHBk!Jd{bj+rou6F*l^KdN!}Gr}#Wf=qHS-Zb8{>s)>|s zv43iF+*~KM_{Jii-?WEh_131qKcaE^+0vlQcsZgqidvbPHprN9_w1IMW1tbibS9ar zw*9*qP<2ptrF7vn`m}nnJM6i=o}!!rtx8LQg(#`_jx^anfx43Ph03?(c@XURwYtN} z(+h^z@LD(>RAdGFacOSlFo-bJAi*a=wmTx1(A2Cxa?qc@{#l@6jQzIST#o(Tk@g)#2mwAg`}phytOih$uTa zYx9NmohII|?*;HkQAnj3VLM%RN1G-sI7NH5zv0~u0oaP zFRaejdbNEK%-_EgvnJq_X^sh6lCjt|{Gkm9 zbSOy4y=SH3oH_I-Zr;UQYjk)FCVG~IJxlZ}ql?^=NZ#XLv*543vnhTaG2}B3GPQ#` zzD1y914_H`x`q*v6#^*PxTr%LMqTg_^SLd{EGqH+CYA)9OzzCY)udd&_s8OAYPvqC zKyG_4b7nB97t%IT-S%u0EIjQQRWW~KWfut)YrM7@=@Emj;Lx8?1s(aZWfvbJWJ!O| zZ-WrIXI-oZ#H}vW?N<{|f*Pe{o zd>S~qiF1D9TM218f**_1%Y~3l3&Nd%Wl}va;MRR20l|G2$AZGH85uGE z0e5lzB4v;ocSY%?U+~OfZp`Z0o_xw+@{s?wxw2>xmF`UYaMtzaxH$m$DTuOi3t z7PvAKSf2vK4tJ5JAiYM$#^UqH%H*5KN7pUuaV~& z?fLfdW}}Ml5SjicI^vh7z~#&M z4z&JA$<9;iFk^}WYLqp#TaZd3Q@mB~pS;O&Rt<|b5prLyg~(rp;69+G{7tR}31cW@ zrKzzM3=pGj`u|X7{OxTr8#%E|Td#7ZUy8vnCHjD7yweR&dIqIr(+-eLubSii+N>0w z+Ans9I@>RF2wQ~*#$o5qk{WiR;4%#B^%7l0{XPAm{jtmm+7xcb1%A?nxByHfElLjM zn$x4XPjQsHd)-nkCq#M#knS3B<$PRTCm0g3$z{5TaSU&qrDUZy9DSbxWw(-)?3RU^ z9qPy*tDG%vp*u(eMJ?`z%-&9bG;2k)EKlH26ey5GP3IZ6(p4@%fx4at$Zs(3(?e2gM z9Xda<Q%icC;>Mu(mMVAQe+VZ1h$j4Uw>w)$FiBXb7#1SM4hTGfN`lyZ|s{OEYo@7)+xFx=0~G z74u+zF@Kt=dWCpNX*a-=wSn#X&pdeR^X$y?Cpa+tkPWfNJ54;akA}H3<=l-?ADw?L zTysoFS2(iWe?4gwF0$nhvFiUyb7CR!u28~V&xo7iG6k(+pbMIsjHraKrf10$&YMjw zgiIQJrF#yf#|K0m%k^>%(qI>fd6s<<+ze>cD%f$~Jxd zY;VouIAyY#-lz?1p|k<2c2a zNq1O0SO-CseBJO6thWTtuY%~brM;Tub}s+wKankOy(Y_l`R)JOfF z0t$-Ue;;;^N7EySc(r9Cy-!?T84qKF?|9+Qe2Gn1o23Q-aGHKppGd(xtNJ`-Kh^uL&=$-wf6mj+60k37D{bMAf+c-P63w&$2$k(Nk1cs976`Xq9Iw=Equ$|6!3%$ zUPPWusQ&=BNHfk>9eHH>`+PIoxcLJst-`M zY4UCKg+sJ%n=o8*JCjC^zk9oj<|(fJV$$Roa5Mn$Ug$4>mXJrKYy4Lqydk3%qf0FB z57^8aUs2{t1MUX^yJaMNc)Ro!-))vkx!>-r22x!y!7GMM2?@0CR}@c2SF12M=T<2L z)QUN04-TaKU&>p=6f=SK_$gidey@+{A{+XDnbot6rV6q-aPKp$$r^n81%7j8E6Xa0 z_hG;)vcHfMaN2I_E~`h&;;OfSugUvL1vL?*EK5^^GRl&GF@q|gaUr%=4NE|~rClR= zM_yUqAg1mIa6tf2bUMj1%7D2vo#=ZN@)zwEQn~-BI6Z)_Uz~9PoZ);tAVpCZ&G}*O z7Avpkl@O?byz{kGZ0F%4hkj;al^J0L2=#4qzW}G8j}-lKnaY)Nj+Y&O_$mjthORJB zfcefDT*_}(;<3%Bk@kXzW!41ak`KJ1t3o@>8v@*dWR_!k+*1#x+W>v9du&@uSEkxM zu?zH1mbU6r{e822KOw|&|E-Tc;x5r#i@R~Ee?y61FrBh0Va*;-zp<;NOowP1+$+2L HOA6eVho1kW$`KJWWJ@4MbV?jO&3vQ}2I&hM;q_HUoP_xH2UJ3|9)W=1YX5D3H! z(Ya>~0#T^}-`KNsz{qS_!&~5o*6S|BA7|t|s{Oj$e3X z8HNyW-*57!v41Q+qRq{9?}+jd2^=CkF+!?!0&qNns zerQjUlI=0Q!ua*7=$*_YEUU`td1A5T2|vqs`e<W zAnA&IuW0ReYRKtANOf~l<{_^aUV$S@7YJ*Fy(XTo$*bh!MDiIMQ+YK=kG>*di09+i?pu_|tM`3alpB&eybLyKpz8Zv?`K%%p{g|K>enSmM5z!QljiOix}DqZ4R zC&God0_=!2m?BzAX6N8j2FDpXBU6Px7q$jR?L+-K2*!rNo>v`1!Rh4eX}s>1eo+gR zk4IFL&i6&w6}0|z>N9qX5&xTlHSSEX{gr}A0+(SE&*BRZ3;M(Jptm_&g4lI6H_?(a zC@iSx9i1>l`t}-(0qqosU=J!U&Z2uxB8yYW^;Pf$(V!i;GP~buk;|xRBFJYRZpf-> z)1GU&iu`0E!~6p2STajlx{L4M5r=M(6s@$B^td zbmsR|1PWjj!;iA@;d#hNp_q&JT8~vAalB)7!*U`=@p^S4 z|ANob4*uke4unu6$TVWbsZ@p|1fVsuq{bw8On_wCg%8;Cj^A^yFgRZ3++!^rtNFRG z&RtzQm^@^r^)}D^7xRn7micMv@7EyS^Tqpx9pY8~6L5v!H++86mtXy|%zPw_={9g6 z^qwLszI#3KI>s%SM>$K$AW_w8C%npIyF07F8ym@8TCV`p_Mn!Wq^cVwdPgPjFcM z#kRqH$O`yI%W^ea0B@NOj%6fPZFY7DH%i{g$Mb$Klk1%kTT_XK&SVO(`>a{{RFsT` z*{1QuVi$_*>5^5qSB}6JEfIs3ZgAJ>X6?YLlrF8^D%F5Y)zev+;Dk|*;hS@~+7zPo zdF8SJf-(pSp#DP>C-|4~|N*qARsS*G9%&Kb<^X_dml9WR|R3`f-3=6A1(oajSqi#S=P&^u zC04yBU)Gr8p6kG*{2u32%ml635UVBGL<0d~ZgW^9C+)_4;BWyk9y~(P2x+0KkO%T7 zZFuKxVmz2LUdPA&L*UJPM%orCt3@HfRUld)4tOU`++d7Z1O5xxlrxX@))4nE`O@4)tH`24xnm*4ui$dqPGjsP))s0>39|k%jt#Bku`krSPa4CH2CU!lU zO=4uTk`jm_}CDYks$olIcCXvwznNX;ml)C%sm3##QHic z#WdYrxjMu=)N_Y_na_bscHBk!Jd{bj+rou6F*l^KdN!}Gr}#Wf=qHS-Zb8{>s)>|s zv43iF+*~KM_{Jii-?WEh_131qKcaE^+0vlQcsZgqidvbPHprN9_w1IMW1tbibS9ar zw*9*qP<2ptrF7vn`m}nnJM6i=o}!!rtx8LQg(#`_jx^anfx43Ph03?(c@XURwYtN} z(+h^z@LD(>RAdGFacOSlFo-bJAi*a=wmTx1(A2Cxa?qc@{#l@6jQzIST#o(Tk@g)#2mwAg`}phytOih$uTa zYx9NmohII|?*;HkQAnj3VLM%RN1G-sI7NH5zv0~u0oaP zFRaejdbNEK%-_EgvnJq_X^sh6lCjt|{Gkm9 zbSOy4y=SH3oH_I-Zr;UQYjk)FCVG~IJxlZ}ql?^=NZ#XLv*543vnhTaG2}B3GPQ#` zzD1y914_H`x`q*v6#^*PxTr%LMqTg_^SLd{EGqH+CYA)9OzzCY)udd&_s8OAYPvqC zKyG_4b7nB97t%IT-S%u0EIjQQRWW~KWfut)YrM7@=@Emj;Lx8?1s(aZWfvbJWJ!O| zZ-WrIXI-oZ#H}vW?N<{|f*Pe{o zd>S~qiF1D9TM218f**_1%Y~3l3&Nd%Wl}va;MRR20l|G2$AZGH85uGE z0e5lzB4v;ocSY%?U+~OfZp`Z0o_xw+@{s?wxw2>xmF`UYaMtzaxH$m$DTuOi3t z7PvAKSf2vK4tJ5JAiYM$#^UqH%H*5KN7pUuaV~& z?fLfdW}}Ml5SjicI^vh7z~#&M z4z&JA$<9;iFk^}WYLqp#TaZd3Q@mB~pS;O&Rt<|b5prLyg~(rp;69+G{7tR}31cW@ zrKzzM3=pGj`u|X7{OxTr8#%E|Td#7ZUy8vnCHjD7yweR&dIqIr(+-eLubSii+N>0w z+Ans9I@>RF2wQ~*#$o5qk{WiR;4%#B^%7l0{XPAm{jtmm+7xcb1%A?nxByHfElLjM zn$x4XPjQsHd)-nkCq#M#knS3B<$PRTCm0g3$z{5TaSU&qrDUZy9DSbxWw(-)?3RU^ z9qPy*tDG%vp*u(eMJ?`z%-&9bG;2k)EKlH26ey5GP3IZ6(p4@%fx4at$Zs(3(?e2gM z9Xda<Q%icC;>Mu(mMVAQe+VZ1h$j4Uw>w)$FiBXb7#1SM4hTGfN`lyZ|s{OEYo@7)+xFx=0~G z74u+zF@Kt=dWCpNX*a-=wSn#X&pdeR^X$y?Cpa+tkPWfNJ54;akA}H3<=l-?ADw?L zTysoFS2(iWe?4gwF0$nhvFiUyb7CR!u28~V&xo7iG6k(+pbMIsjHraKrf10$&YMjw zgiIQJrF#yf#|K0m%k^>%(qI>fd6s<<+ze>cD%f$~Jxd zY;VouIAyY#-lz?1p|k<2c2a zNq1O0SO-CseBJO6thWTtuY%~brM;Tub}s+wKankOy(Y_l`R)JOfF z0t$-Ue;;;^N7EySc(r9Cy-!?T84qKF?|9+Qe2Gn1o23Q-aGHKppGd(xtNJ`-Kh^uL&=$-wf6mj+60k37D{bMAf+c-P63w&$2$k(Nk1cs976`Xq9Iw=Equ$|6!3%$ zUPPWusQ&=BNHfk>9eHH>`+PIoxcLJst-`M zY4UCKg+sJ%n=o8*JCjC^zk9oj<|(fJV$$Roa5Mn$Ug$4>mXJrKYy4Lqydk3%qf0FB z57^8aUs2{t1MUX^yJaMNc)Ro!-))vkx!>-r22x!y!7GMM2?@0CR}@c2SF12M=T<2L z)QUN04-TaKU&>p=6f=SK_$gidey@+{A{+XDnbot6rV6q-aPKp$$r^n81%7j8E6Xa0 z_hG;)vcHfMaN2I_E~`h&;;OfSugUvL1vL?*EK5^^GRl&GF@q|gaUr%=4NE|~rClR= zM_yUqAg1mIa6tf2bUMj1%7D2vo#=ZN@)zwEQn~-BI6Z)_Uz~9PoZ);tAVpCZ&G}*O z7Avpkl@O?byz{kGZ0F%4hkj;al^J0L2=#4qzW}G8j}-lKnaY)Nj+Y&O_$mjthORJB zfcefDT*_}(;<3%Bk@kXzW!41ak`KJ1t3o@>8v@*dWR_!k+*1#x+W>v9du&@uSEkxM zu?zH1mbU6r{e822KOw|&|E-Tc;x5r#i@R~Ee?y61FrBh0Va*;-zp<;NOowP1+$+2L H Date: Thu, 20 Jan 2022 10:23:35 +0000 Subject: [PATCH 38/56] 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 39/56] 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 40/56] 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 41/56] 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 42/56] 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 43/56] 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 44/56] 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 45/56] 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 46/56] 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 47/56] 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 48/56] 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 49/56] 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 50/56] 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 51/56] 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 52/56] 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 53/56] 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 54/56] 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 55/56] 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 56/56] 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);