Browse Source

Merge branch 'master' into dev/siegfriedpammer/event-viewer

pull/1888/head
Steven Kirk 8 years ago
committed by GitHub
parent
commit
017f8a16cf
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      samples/RenderDemo/Pages/AnimationsPage.xaml
  2. 16
      samples/RenderDemo/Pages/AnimationsPage.xaml.cs
  3. 26
      samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs
  4. 3
      src/Android/Avalonia.Android/AndroidPlatform.cs
  5. 28
      src/Avalonia.Animation/Animatable.cs
  6. 15
      src/Avalonia.Animation/Animation.cs
  7. 61
      src/Avalonia.Animation/AnimationInstance`1.cs
  8. 118
      src/Avalonia.Animation/Animator`1.cs
  9. 30
      src/Avalonia.Animation/Clock.cs
  10. 72
      src/Avalonia.Animation/ClockBase.cs
  11. 65
      src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs
  12. 5
      src/Avalonia.Animation/DoubleAnimator.cs
  13. 5
      src/Avalonia.Animation/FillMode.cs
  14. 11
      src/Avalonia.Animation/IAnimation.cs
  15. 3
      src/Avalonia.Animation/IAnimationSetter.cs
  16. 7
      src/Avalonia.Animation/IAnimator.cs
  17. 11
      src/Avalonia.Animation/IClock.cs
  18. 10
      src/Avalonia.Animation/IGlobalClock.cs
  19. 2
      src/Avalonia.Animation/ITransition.cs
  20. 5
      src/Avalonia.Animation/KeyFrame.cs
  21. 3
      src/Avalonia.Animation/KeyFramePair`1.cs
  22. 5
      src/Avalonia.Animation/PlayState.cs
  23. 5
      src/Avalonia.Animation/PlaybackDirection.cs
  24. 54
      src/Avalonia.Animation/Timing.cs
  25. 27
      src/Avalonia.Animation/TransitionInstance.cs
  26. 7
      src/Avalonia.Animation/Transition`1.cs
  27. 10
      src/Avalonia.Base/Reactive/LightweightObservableBase.cs
  28. 5
      src/Avalonia.Base/Reactive/ObservableEx.cs
  29. 2
      src/Avalonia.Controls/AppBuilderBase.cs
  30. 7
      src/Avalonia.Controls/Application.cs
  31. 11
      src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs
  32. 7
      src/Avalonia.Controls/ProgressBar.cs
  33. 1
      src/Avalonia.Controls/TopLevel.cs
  34. 3
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  35. 19
      src/Avalonia.Styling/Styling/Style.cs
  36. 26
      src/Avalonia.Visuals/Animation/RenderLoopClock.cs
  37. 20
      src/Avalonia.Visuals/Animation/TransformAnimator.cs
  38. 1
      src/Avalonia.Visuals/Avalonia.Visuals.csproj
  39. 24
      src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs
  40. 93
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  41. 27
      src/Avalonia.Visuals/Rendering/IRenderLoop.cs
  42. 12
      src/Avalonia.Visuals/Rendering/IRenderLoopTask.cs
  43. 20
      src/Avalonia.Visuals/Rendering/IRenderTimer.cs
  44. 120
      src/Avalonia.Visuals/Rendering/RenderLoop.cs
  45. 3
      src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs
  46. 3
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  47. 7
      src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs
  48. 31
      src/OSX/Avalonia.MonoMac/RenderLoop.cs
  49. 28
      src/OSX/Avalonia.MonoMac/RenderTimer.cs
  50. 25
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  51. 35
      src/Windows/Avalonia.Win32/RenderLoop.cs
  52. 56
      src/Windows/Avalonia.Win32/RenderTimer.cs
  53. 4
      src/Windows/Avalonia.Win32/Win32Platform.cs
  54. 9
      src/iOS/Avalonia.iOS/DisplayLinkRenderTimer.cs
  55. 3
      src/iOS/Avalonia.iOS/iOSPlatform.cs
  56. 4
      tests/Avalonia.UnitTests/TestServices.cs
  57. 64
      tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs
  58. 119
      tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs

7
samples/RenderDemo/Pages/AnimationsPage.xaml

@ -107,9 +107,12 @@
</UserControl.Styles>
<Grid>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" ClipToBounds="False">
<StackPanel.Clock>
<Clock />
</StackPanel.Clock>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock VerticalAlignment="Center">Hover to activate Transform Keyframe Animations.</TextBlock>
<Button Content="{Binding PlayStateText}" Command="{Binding ToggleGlobalPlayState}"/>
<Button Content="{Binding PlayStateText}" Command="{Binding TogglePlayState}" Click="ToggleClock" />
</StackPanel>
<WrapPanel ClipToBounds="False">
<Border Classes="Test Rect1" Background="DarkRed"/>
@ -120,4 +123,4 @@
</WrapPanel>
</StackPanel>
</Grid>
</UserControl>
</UserControl>

16
samples/RenderDemo/Pages/AnimationsPage.xaml.cs

@ -5,6 +5,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using RenderDemo.ViewModels;
@ -23,5 +24,20 @@ namespace RenderDemo.Pages
{
AvaloniaXamlLoader.Load(this);
}
private void ToggleClock(object sender, RoutedEventArgs args)
{
var button = sender as Button;
var clock = button.Clock;
if (clock.PlayState == PlayState.Run)
{
clock.PlayState = PlayState.Pause;
}
else if (clock.PlayState == PlayState.Pause)
{
clock.PlayState = PlayState.Run;
}
}
}
}

26
samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs

@ -6,27 +6,15 @@ namespace RenderDemo.ViewModels
{
public class AnimationsPageViewModel : ReactiveObject
{
private string _playStateText = "Pause all animations";
private bool _isPlaying = true;
public AnimationsPageViewModel()
{
ToggleGlobalPlayState = ReactiveCommand.Create(() => TogglePlayState());
}
private string _playStateText = "Pause animations on this page";
void TogglePlayState()
public void TogglePlayState()
{
switch (Animation.GlobalPlayState)
{
case PlayState.Run:
PlayStateText = "Resume all animations";
Animation.GlobalPlayState = PlayState.Pause;
break;
case PlayState.Pause:
PlayStateText = "Pause all animations";
Animation.GlobalPlayState = PlayState.Run;
break;
}
PlayStateText = _isPlaying
? "Resume animations on this page" : "Pause animations on this page";
_isPlaying = !_isPlaying;
}
public string PlayStateText
@ -34,7 +22,5 @@ namespace RenderDemo.ViewModels
get { return _playStateText; }
set { this.RaiseAndSetIfChanged(ref _playStateText, value); }
}
public ReactiveCommand ToggleGlobalPlayState { get; }
}
}

3
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -52,7 +52,8 @@ namespace Avalonia.Android
.Bind<ISystemDialogImpl>().ToTransient<SystemDialogImpl>()
.Bind<IWindowingPlatform>().ToConstant(Instance)
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoader>()
.Bind<IRenderLoop>().ToConstant(new DefaultRenderLoop(60))
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<IAssetLoader>().ToConstant(new AssetLoader(app.GetType().Assembly));
SkiaPlatform.Initialize();

28
src/Avalonia.Animation/Animatable.cs

@ -14,26 +14,14 @@ namespace Avalonia.Animation
/// Base class for all animatable objects.
/// </summary>
public class Animatable : AvaloniaObject
{
/// <summary>
/// Defines the <see cref="PlayState"/> property.
/// </summary>
public static readonly DirectProperty<Animatable, PlayState> PlayStateProperty =
AvaloniaProperty.RegisterDirect<Animatable, PlayState>(
nameof(PlayState),
o => o.PlayState,
(o, v) => o.PlayState = v);
private PlayState _playState = PlayState.Run;
{
public static readonly StyledProperty<IClock> ClockProperty =
AvaloniaProperty.Register<Animatable, IClock>(nameof(Clock), inherits: true);
/// <summary>
/// Gets or sets the state of the animation for this
/// control.
/// </summary>
public PlayState PlayState
public IClock Clock
{
get { return _playState; }
set { SetAndRaise(PlayStateProperty, ref _playState, value); }
get => GetValue(ClockProperty);
set => SetValue(ClockProperty, value);
}
/// <summary>
@ -69,9 +57,9 @@ namespace Avalonia.Animation
if (match != null)
{
match.Apply(this, e.OldValue, e.NewValue);
match.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue);
}
}
}
}
}
}

15
src/Avalonia.Animation/Animation.cs

@ -17,11 +17,6 @@ namespace Avalonia.Animation
/// </summary>
public class Animation : AvaloniaList<KeyFrame>, IAnimation
{
/// <summary>
/// Gets or sets the animation play state for all animations
/// </summary>
public static PlayState GlobalPlayState { get; set; } = PlayState.Run;
/// <summary>
/// Gets or sets the active time of this animation.
/// </summary>
@ -149,12 +144,12 @@ namespace Avalonia.Animation
}
/// <inheritdocs/>
public IDisposable Apply(Animatable control, IObservable<bool> match, Action onComplete)
public IDisposable Apply(Animatable control, IClock clock, IObservable<bool> match, Action onComplete)
{
var (animators, subscriptions) = InterpretKeyframes(control);
if (animators.Count == 1)
{
subscriptions.Add(animators[0].Apply(this, control, match, onComplete));
subscriptions.Add(animators[0].Apply(this, control, clock, match, onComplete));
}
else
{
@ -168,7 +163,7 @@ namespace Avalonia.Animation
animatorOnComplete = () => tcs.SetResult(null);
completionTasks.Add(tcs.Task);
}
subscriptions.Add(animator.Apply(this, control, match, animatorOnComplete));
subscriptions.Add(animator.Apply(this, control, clock, match, animatorOnComplete));
}
if (onComplete != null)
@ -180,7 +175,7 @@ namespace Avalonia.Animation
}
/// <inheritdocs/>
public Task RunAsync(Animatable control)
public Task RunAsync(Animatable control, IClock clock = null)
{
var run = new TaskCompletionSource<object>();
@ -188,7 +183,7 @@ namespace Avalonia.Animation
run.SetException(new InvalidOperationException("Looping animations must not use the Run method."));
IDisposable subscriptions = null;
subscriptions = this.Apply(control, Observable.Return(true), () =>
subscriptions = this.Apply(control, clock, Observable.Return(true), () =>
{
run.SetResult(null);
subscriptions?.Dispose();

61
src/Avalonia.Animation/AnimationInstance`1.cs

@ -8,7 +8,7 @@ using Avalonia.Reactive;
namespace Avalonia.Animation
{
/// <summary>
/// Handles interpolatoin and time-related functions
/// Handles interpolation and time-related functions
/// for keyframe animations.
/// </summary>
internal class AnimationInstance<T> : SingleSubscriberObservableBase<T>
@ -19,7 +19,6 @@ namespace Avalonia.Animation
private double _currentIteration;
private bool _isLooping;
private bool _gotFirstKFValue;
private bool _gotFirstFrameCount;
private bool _iterationDelay;
private FillMode _fillMode;
private PlaybackDirection _animationDirection;
@ -29,15 +28,14 @@ namespace Avalonia.Animation
private double _speedRatio;
private TimeSpan _delay;
private TimeSpan _duration;
private TimeSpan _firstFrameCount;
private TimeSpan _internalClock;
private TimeSpan? _previousClock;
private Easings.Easing _easeFunc;
private Action _onCompleteAction;
private Func<double, T, T> _interpolator;
private IDisposable _timerSubscription;
private readonly IClock _baseClock;
private IClock _clock;
public AnimationInstance(Animation animation, Animatable control, Animator<T> animator, Action OnComplete, Func<double, T, T> Interpolator)
public AnimationInstance(Animation animation, Animatable control, Animator<T> animator, IClock baseClock, Action OnComplete, Func<double, T, T> Interpolator)
{
if (animation.SpeedRatio <= 0)
throw new InvalidOperationException("Speed ratio cannot be negative or zero.");
@ -73,17 +71,19 @@ namespace Avalonia.Animation
_fillMode = animation.FillMode;
_onCompleteAction = OnComplete;
_interpolator = Interpolator;
_baseClock = baseClock;
}
protected override void Unsubscribed()
{
_timerSubscription?.Dispose();
_clock.PlayState = PlayState.Stop;
}
protected override void Subscribed()
{
_timerSubscription = Timing.AnimationsTimer
.Subscribe(p => this.Step(p));
_clock = new Clock(_baseClock);
_timerSubscription = _clock.Subscribe(Step);
}
public void Step(TimeSpan frameTick)
@ -116,46 +116,21 @@ namespace Avalonia.Animation
PublishNext(_lastInterpValue);
}
private void DoPlayStatesAndTime(TimeSpan systemTime)
private void DoPlayStates()
{
if (Animation.GlobalPlayState == PlayState.Stop || _targetControl.PlayState == PlayState.Stop)
if (_clock.PlayState == PlayState.Stop || _baseClock.PlayState == PlayState.Stop)
DoComplete();
if (!_previousClock.HasValue)
{
_previousClock = systemTime;
_internalClock = TimeSpan.Zero;
}
else
{
if (Animation.GlobalPlayState == PlayState.Pause || _targetControl.PlayState == PlayState.Pause)
{
_previousClock = systemTime;
return;
}
var delta = systemTime - _previousClock;
_internalClock += delta.Value;
_previousClock = systemTime;
}
if (!_gotFirstKFValue)
{
_firstKFValue = (T)_parent.First().Value;
_gotFirstKFValue = true;
}
if (!_gotFirstFrameCount)
{
_firstFrameCount = _internalClock;
_gotFirstFrameCount = true;
}
}
private void InternalStep(TimeSpan systemTime)
private void InternalStep(TimeSpan time)
{
DoPlayStatesAndTime(systemTime);
var time = _internalClock - _firstFrameCount;
DoPlayStates();
var delayEndpoint = _delay;
var iterationEndpoint = delayEndpoint + _duration;
@ -176,22 +151,18 @@ namespace Avalonia.Animation
}
//Calculate the current iteration number
_currentIteration = (int)Math.Floor((double)time.Ticks / iterationEndpoint.Ticks) + 2;
_currentIteration = (int)Math.Floor((double)((double)time.Ticks / iterationEndpoint.Ticks)) + 2;
}
else
{
_previousClock = systemTime;
return;
}
time = TimeSpan.FromTicks(time.Ticks % iterationEndpoint.Ticks);
time = TimeSpan.FromTicks((long)(time.Ticks % iterationEndpoint.Ticks));
if (!_isLooping)
{
if (_currentIteration > _repeatCount)
DoComplete();
if (time > iterationEndpoint)
if ((_currentIteration > _repeatCount) || (time > iterationEndpoint))
DoComplete();
}
@ -225,4 +196,4 @@ namespace Avalonia.Animation
}
}
}
}
}

118
src/Avalonia.Animation/Animator`1.cs

@ -1,10 +1,14 @@
using System;
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Animation.Utils;
using Avalonia.Collections;
using Avalonia.Data;
using Avalonia.Reactive;
namespace Avalonia.Animation
{
@ -17,7 +21,7 @@ namespace Avalonia.Animation
/// List of type-converted keyframes.
/// </summary>
private readonly List<AnimatorKeyFrame> _convertedKeyframes = new List<AnimatorKeyFrame>();
private bool _isVerifiedAndConverted;
/// <summary>
@ -28,21 +32,17 @@ namespace Avalonia.Animation
public Animator()
{
// Invalidate keyframes when changed.
this.CollectionChanged += delegate { _isVerifiedAndConverted = false; };
this.CollectionChanged += delegate { _isVerifiedAndConverted = false; };
}
/// <inheritdoc/>
public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> match, Action onComplete)
public virtual IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable<bool> match, Action onComplete)
{
if (!_isVerifiedAndConverted)
if (!_isVerifiedAndConverted)
VerifyConvertKeyFrames();
return match
.Where(p => p)
.Subscribe(_ =>
{
var timerObs = RunKeyFrames(animation, control, onComplete);
});
var subject = new DisposeAnimationInstanceSubject<T>(this, animation, control, clock, onComplete);
return match.Subscribe(subject);
}
/// <summary>
@ -52,58 +52,84 @@ namespace Avalonia.Animation
/// (i.e., the normalized time between the selected keyframes, relative to the
/// time parameter).
/// </summary>
/// <param name="t">The time parameter, relative to the total animation time</param>
protected (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double t)
/// <param name="animationTime">The time parameter, relative to the total animation time</param>
protected (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double animationTime)
{
AnimatorKeyFrame firstCue, lastCue ;
AnimatorKeyFrame firstKeyframe, lastKeyframe;
int kvCount = _convertedKeyframes.Count;
if (kvCount > 2)
{
if (t <= 0.0)
if (animationTime <= 0.0)
{
firstCue = _convertedKeyframes[0];
lastCue = _convertedKeyframes[1];
firstKeyframe = _convertedKeyframes[0];
lastKeyframe = _convertedKeyframes[1];
}
else if (t >= 1.0)
else if (animationTime >= 1.0)
{
firstCue = _convertedKeyframes[_convertedKeyframes.Count - 2];
lastCue = _convertedKeyframes[_convertedKeyframes.Count - 1];
firstKeyframe = _convertedKeyframes[_convertedKeyframes.Count - 2];
lastKeyframe = _convertedKeyframes[_convertedKeyframes.Count - 1];
}
else
{
(double time, int index) maxval = (0.0d, 0);
for (int i = 0; i < _convertedKeyframes.Count; i++)
{
var comp = _convertedKeyframes[i].Cue.CueValue;
if (t >= comp)
{
maxval = (comp, i);
}
}
firstCue = _convertedKeyframes[maxval.index];
lastCue = _convertedKeyframes[maxval.index + 1];
int index = FindClosestBeforeKeyFrame(animationTime);
firstKeyframe = _convertedKeyframes[index];
lastKeyframe = _convertedKeyframes[index + 1];
}
}
else
{
firstCue = _convertedKeyframes[0];
lastCue = _convertedKeyframes[1];
firstKeyframe = _convertedKeyframes[0];
lastKeyframe = _convertedKeyframes[1];
}
double t0 = firstCue.Cue.CueValue;
double t1 = lastCue.Cue.CueValue;
var intraframeTime = (t - t0) / (t1 - t0);
var firstFrameData = (firstCue.GetTypedValue<T>(), firstCue.isNeutral);
var lastFrameData = (lastCue.GetTypedValue<T>(), lastCue.isNeutral);
double t0 = firstKeyframe.Cue.CueValue;
double t1 = lastKeyframe.Cue.CueValue;
var intraframeTime = (animationTime - t0) / (t1 - t0);
var firstFrameData = (firstKeyframe.GetTypedValue<T>(), firstKeyframe.isNeutral);
var lastFrameData = (lastKeyframe.GetTypedValue<T>(), lastKeyframe.isNeutral);
return (intraframeTime, new KeyFramePair<T>(firstFrameData, lastFrameData));
}
private int FindClosestBeforeKeyFrame(double time)
{
int FindClosestBeforeKeyFrame(int startIndex, int length)
{
if (length == 0 || length == 1)
{
return startIndex;
}
int middle = startIndex + (length / 2);
if (_convertedKeyframes[middle].Cue.CueValue < time)
{
return FindClosestBeforeKeyFrame(middle, length - middle);
}
else if (_convertedKeyframes[middle].Cue.CueValue > time)
{
return FindClosestBeforeKeyFrame(startIndex, middle - startIndex);
}
else
{
return middle;
}
}
return FindClosestBeforeKeyFrame(0, _convertedKeyframes.Count);
}
/// <summary>
/// Runs the KeyFrames Animation.
/// </summary>
private IDisposable RunKeyFrames(Animation animation, Animatable control, Action onComplete)
internal IDisposable Run(Animation animation, Animatable control, IClock clock, Action onComplete)
{
var instance = new AnimationInstance<T>(animation, control, this, onComplete, DoInterpolation);
var instance = new AnimationInstance<T>(
animation,
control,
this,
clock ?? control.Clock ?? Clock.GlobalClock,
onComplete,
DoInterpolation);
return control.Bind<T>((AvaloniaProperty<T>)Property, instance, BindingPriority.Animation);
}
@ -124,14 +150,6 @@ namespace Avalonia.Animation
AddNeutralKeyFramesIfNeeded();
var copy = _convertedKeyframes.ToList().OrderBy(p => p.Cue.CueValue);
_convertedKeyframes.Clear();
foreach (AnimatorKeyFrame keyframe in copy)
{
_convertedKeyframes.Add(keyframe);
}
_isVerifiedAndConverted = true;
}
@ -161,7 +179,7 @@ namespace Avalonia.Animation
{
if (!hasStartKey)
{
_convertedKeyframes.Add(new AnimatorKeyFrame(null, new Cue(0.0d)) { Value = default(T), isNeutral = true });
_convertedKeyframes.Insert(0, new AnimatorKeyFrame(null, new Cue(0.0d)) { Value = default(T), isNeutral = true });
}
if (!hasEndKey)
@ -170,4 +188,4 @@ namespace Avalonia.Animation
}
}
}
}
}

30
src/Avalonia.Animation/Clock.cs

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Text;
using Avalonia.Reactive;
namespace Avalonia.Animation
{
public class Clock : ClockBase
{
public static IClock GlobalClock => AvaloniaLocator.Current.GetService<IGlobalClock>();
private IDisposable _parentSubscription;
public Clock()
:this(GlobalClock)
{
}
public Clock(IClock parent)
{
_parentSubscription = parent.Subscribe(Pulse);
}
protected override void Stop()
{
_parentSubscription?.Dispose();
}
}
}

72
src/Avalonia.Animation/ClockBase.cs

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Text;
using Avalonia.Reactive;
namespace Avalonia.Animation
{
public class ClockBase : IClock
{
private ClockObservable _observable;
private IObservable<TimeSpan> _connectedObservable;
private TimeSpan? _previousTime;
private TimeSpan _internalTime;
protected ClockBase()
{
_observable = new ClockObservable();
_connectedObservable = _observable.Publish().RefCount();
}
protected bool HasSubscriptions => _observable.HasSubscriptions;
public PlayState PlayState { get; set; }
protected void Pulse(TimeSpan systemTime)
{
if (!_previousTime.HasValue)
{
_previousTime = systemTime;
_internalTime = TimeSpan.Zero;
}
else
{
if (PlayState == PlayState.Pause)
{
_previousTime = systemTime;
return;
}
var delta = systemTime - _previousTime;
_internalTime += delta.Value;
_previousTime = systemTime;
}
_observable.Pulse(_internalTime);
if (PlayState == PlayState.Stop)
{
Stop();
}
}
protected virtual void Stop()
{
}
public IDisposable Subscribe(IObserver<TimeSpan> observer)
{
return _connectedObservable.Subscribe(observer);
}
private class ClockObservable : LightweightObservableBase<TimeSpan>
{
public bool HasSubscriptions { get; private set; }
public void Pulse(TimeSpan time) => PublishNext(time);
protected override void Initialize() => HasSubscriptions = true;
protected override void Deinitialize() => HasSubscriptions = false;
}
}
}

65
src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs

@ -0,0 +1,65 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Animation.Utils;
using Avalonia.Collections;
using Avalonia.Data;
using Avalonia.Reactive;
namespace Avalonia.Animation
{
/// <summary>
/// Manages the lifetime of animation instances as determined by its selector state.
/// </summary>
internal class DisposeAnimationInstanceSubject<T> : IObserver<bool>, IDisposable
{
private IDisposable _lastInstance;
private bool _lastMatch;
private Animator<T> _animator;
private Animation _animation;
private Animatable _control;
private Action _onComplete;
private IClock _clock;
public DisposeAnimationInstanceSubject(Animator<T> animator, Animation animation, Animatable control, IClock clock, Action onComplete)
{
this._animator = animator;
this._animation = animation;
this._control = control;
this._onComplete = onComplete;
this._clock = clock;
}
public void Dispose()
{
_lastInstance?.Dispose();
}
public void OnCompleted()
{
}
public void OnError(Exception error)
{
_lastInstance?.Dispose();
}
void IObserver<bool>.OnNext(bool matchVal)
{
if (matchVal != _lastMatch)
{
_lastInstance?.Dispose();
if (matchVal)
{
_lastInstance = _animator.Run(_animation, _control, _clock, _onComplete);
}
_lastMatch = matchVal;
}
}
}
}

5
src/Avalonia.Animation/DoubleAnimator.cs

@ -1,4 +1,7 @@
namespace Avalonia.Animation
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Animation
{
/// <summary>
/// Animator that handles <see cref="double"/> properties.

5
src/Avalonia.Animation/FillMode.cs

@ -1,4 +1,7 @@
namespace Avalonia.Animation
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Animation
{
public enum FillMode
{

11
src/Avalonia.Animation/IAnimation.cs

@ -1,3 +1,6 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Threading.Tasks;
@ -9,13 +12,13 @@ namespace Avalonia.Animation
public interface IAnimation
{
/// <summary>
/// Apply the animation to the specified control
/// Apply the animation to the specified control and run it when <paramref name="match" /> produces <c>true</c>.
/// </summary>
IDisposable Apply(Animatable control, IObservable<bool> match, Action onComplete = null);
IDisposable Apply(Animatable control, IClock clock, IObservable<bool> match, Action onComplete = null);
/// <summary>
/// Run the animation to the specified control
/// Run the animation on the specified control.
/// </summary>
Task RunAsync(Animatable control);
Task RunAsync(Animatable control, IClock clock);
}
}

3
src/Avalonia.Animation/IAnimationSetter.cs

@ -1,3 +1,6 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Animation
{
public interface IAnimationSetter

7
src/Avalonia.Animation/IAnimator.cs

@ -1,4 +1,7 @@
using System;
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
namespace Avalonia.Animation
@ -16,6 +19,6 @@ namespace Avalonia.Animation
/// <summary>
/// Applies the current KeyFrame group to the specified control.
/// </summary>
IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch, Action onComplete);
IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable<bool> match, Action onComplete);
}
}

11
src/Avalonia.Animation/IClock.cs

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Animation
{
public interface IClock : IObservable<TimeSpan>
{
PlayState PlayState { get; set; }
}
}

10
src/Avalonia.Animation/IGlobalClock.cs

@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Animation
{
public interface IGlobalClock : IClock
{
}
}

2
src/Avalonia.Animation/ITransition.cs

@ -13,7 +13,7 @@ namespace Avalonia.Animation
/// <summary>
/// Applies the transition to the specified <see cref="Animatable"/>.
/// </summary>
IDisposable Apply(Animatable control, object oldValue, object newValue);
IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue);
/// <summary>
/// Gets the property to be animated.

5
src/Avalonia.Animation/KeyFrame.cs

@ -1,4 +1,7 @@
using System;
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using Avalonia.Collections;

3
src/Avalonia.Animation/KeyFramePair`1.cs

@ -1,3 +1,6 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Animation
{
/// <summary>

5
src/Avalonia.Animation/PlayState.cs

@ -1,4 +1,7 @@
namespace Avalonia.Animation
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Animation
{
/// <summary>
/// Determines the playback state of an animation.

5
src/Avalonia.Animation/PlaybackDirection.cs

@ -1,4 +1,7 @@
namespace Avalonia.Animation
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Animation
{
/// <summary>
/// Determines the playback direction of an animation.

54
src/Avalonia.Animation/Timing.cs

@ -1,54 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Threading;
namespace Avalonia.Animation
{
/// <summary>
/// Provides global timing functions for animations.
/// </summary>
public static class Timing
{
/// <summary>
/// The number of frames per second.
/// </summary>
public const int FramesPerSecond = 60;
/// <summary>
/// The time span of each frame.
/// </summary>
internal static readonly TimeSpan FrameTick = TimeSpan.FromSeconds(1.0 / FramesPerSecond);
/// <summary>
/// Initializes static members of the <see cref="Timing"/> class.
/// </summary>
static Timing()
{
var globalTimer = Observable.Interval(FrameTick, AvaloniaScheduler.Instance);
AnimationsTimer = globalTimer
.Select(_ => GetTickCount())
.Publish()
.RefCount();
}
internal static TimeSpan GetTickCount() => TimeSpan.FromMilliseconds(Environment.TickCount);
/// <summary>
/// Gets the animation timer.
/// </summary>
/// <remarks>
/// The animation timer triggers usually at 60 times per second or as
/// defined in <see cref="FramesPerSecond"/>.
/// The parameter passed to a subsciber is the current playstate of the animation.
/// </remarks>
internal static IObservable<TimeSpan> AnimationsTimer
{
get;
}
}
}

27
src/Avalonia.Animation/TransitionInstance.cs

@ -15,21 +15,22 @@ namespace Avalonia.Animation
/// </summary>
internal class TransitionInstance : SingleSubscriberObservableBase<double>
{
private IDisposable timerSubscription;
private TimeSpan startTime;
private TimeSpan duration;
private IDisposable _timerSubscription;
private TimeSpan _duration;
private readonly IClock _baseClock;
private IClock _clock;
public TransitionInstance(TimeSpan Duration)
public TransitionInstance(IClock clock, TimeSpan Duration)
{
duration = Duration;
_duration = Duration;
_baseClock = clock;
}
private void TimerTick(TimeSpan t)
{
var interpVal = (double)(t.Ticks - startTime.Ticks) / duration.Ticks;
var interpVal = (double)t.Ticks / _duration.Ticks;
if (interpVal > 1d
|| interpVal < 0d)
if (interpVal > 1d || interpVal < 0d)
{
PublishCompleted();
return;
@ -40,15 +41,15 @@ namespace Avalonia.Animation
protected override void Unsubscribed()
{
timerSubscription?.Dispose();
_timerSubscription?.Dispose();
_clock.PlayState = PlayState.Stop;
}
protected override void Subscribed()
{
startTime = Timing.GetTickCount();
timerSubscription = Timing.AnimationsTimer
.Subscribe(t => TimerTick(t));
_clock = new Clock(_baseClock);
_timerSubscription = _clock.Subscribe(TimerTick);
PublishNext(0.0d);
}
}
}
}

7
src/Avalonia.Animation/Transition`1.cs

@ -14,7 +14,6 @@ namespace Avalonia.Animation
public abstract class Transition<T> : AvaloniaObject, ITransition
{
private AvaloniaProperty _prop;
private Easing _easing;
/// <summary>
/// Gets the duration of the animation.
@ -49,12 +48,10 @@ namespace Avalonia.Animation
public abstract IObservable<T> DoTransition(IObservable<double> progress, T oldValue, T newValue);
/// <inheritdocs/>
public virtual IDisposable Apply(Animatable control, object oldValue, object newValue)
public virtual IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue)
{
var transition = DoTransition(new TransitionInstance(Duration), (T)oldValue, (T)newValue);
var transition = DoTransition(new TransitionInstance(clock, Duration), (T)oldValue, (T)newValue);
return control.Bind<T>((AvaloniaProperty<T>)Property, transition, Data.BindingPriority.Animation);
}
}
}

10
src/Avalonia.Base/Reactive/LightweightObservableBase.cs

@ -82,18 +82,10 @@ namespace Avalonia.Reactive
if (observers.Count == 0)
{
observers.TrimExcess();
Deinitialize();
}
else
{
return;
}
} else
{
return;
}
}
Deinitialize();
}
}

5
src/Avalonia.Base/Reactive/ObservableEx.cs

@ -21,7 +21,7 @@ namespace Avalonia.Reactive
{
return new SingleValueImpl<T>(value);
}
private class SingleValueImpl<T> : IObservable<T>
{
private T _value;
@ -30,7 +30,6 @@ namespace Avalonia.Reactive
{
_value = value;
}
public IDisposable Subscribe(IObserver<T> observer)
{
observer.OnNext(_value);
@ -38,4 +37,4 @@ namespace Avalonia.Reactive
}
}
}
}
}

2
src/Avalonia.Controls/AppBuilderBase.cs

@ -272,10 +272,10 @@ namespace Avalonia.Controls
s_setupWasAlreadyCalled = true;
Instance.RegisterServices();
RuntimePlatformServicesInitializer();
WindowingSubsystemInitializer();
RenderingSubsystemInitializer();
Instance.RegisterServices();
Instance.Initialize();
AfterSetupCallback(Self);
}

7
src/Avalonia.Controls/Application.cs

@ -4,12 +4,14 @@
using System;
using System.Reactive.Concurrency;
using System.Threading;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.Threading;
@ -335,6 +337,11 @@ namespace Avalonia
.Bind<IScheduler>().ToConstant(AvaloniaScheduler.Instance)
.Bind<IDragDropDevice>().ToConstant(DragDropDevice.Instance)
.Bind<IPlatformDragSource>().ToTransient<InProcessDragSource>();
var clock = new RenderLoopClock();
AvaloniaLocator.CurrentMutable
.Bind<IGlobalClock>().ToConstant(clock)
.GetService<IRenderLoop>()?.Add(clock);
}
}
}

11
src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs

@ -9,12 +9,15 @@ using Avalonia.Threading;
namespace Avalonia.Controls.Platform
{
public class InternalPlatformThreadingInterface : IPlatformThreadingInterface, IRenderLoop
public class InternalPlatformThreadingInterface : IPlatformThreadingInterface, IRenderTimer
{
public InternalPlatformThreadingInterface()
{
TlsCurrentThreadIsLoopThread = true;
StartTimer(DispatcherPriority.Render, new TimeSpan(0, 0, 0, 0, 66), () => Tick?.Invoke(this, new EventArgs()));
StartTimer(
DispatcherPriority.Render,
new TimeSpan(0, 0, 0, 0, 66),
() => Tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount)));
}
private readonly AutoResetEvent _signaled = new AutoResetEvent(false);
@ -105,7 +108,7 @@ namespace Avalonia.Controls.Platform
public bool CurrentThreadIsLoopThread => TlsCurrentThreadIsLoopThread;
public event Action<DispatcherPriority?> Signaled;
public event EventHandler<EventArgs> Tick;
public event Action<TimeSpan> Tick;
}
}
}

7
src/Avalonia.Controls/ProgressBar.cs

@ -37,7 +37,8 @@ namespace Avalonia.Controls
PseudoClass<ProgressBar, Orientation>(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal");
PseudoClass<ProgressBar>(IsIndeterminateProperty, ":indeterminate");
ValueProperty.Changed.AddClassHandler<ProgressBar>(x => x.ValueChanged);
ValueProperty.Changed.AddClassHandler<ProgressBar>(x => x.UpdateIndicatorWhenPropChanged);
IsIndeterminateProperty.Changed.AddClassHandler<ProgressBar>(x => x.UpdateIndicatorWhenPropChanged);
}
public bool IsIndeterminate
@ -114,9 +115,9 @@ namespace Avalonia.Controls
}
}
private void ValueChanged(AvaloniaPropertyChangedEventArgs e)
private void UpdateIndicatorWhenPropChanged(AvaloniaPropertyChangedEventArgs e)
{
UpdateIndicator(Bounds.Size);
}
}
}
}

1
src/Avalonia.Controls/TopLevel.cs

@ -96,7 +96,6 @@ namespace Avalonia.Controls
_applicationLifecycle = TryGetService<IApplicationLifecycle>(dependencyResolver);
_renderInterface = TryGetService<IPlatformRenderInterface>(dependencyResolver);
var renderLoop = TryGetService<IRenderLoop>(dependencyResolver);
Renderer = impl.CreateRenderer(this);
impl.SetInputRoot(this);

3
src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs

@ -53,7 +53,8 @@ namespace Avalonia.DesignerSupport.Remote
.Bind<IKeyboardDevice>().ToConstant(Keyboard)
.Bind<IPlatformSettings>().ToConstant(instance)
.Bind<IPlatformThreadingInterface>().ToConstant(threading)
.Bind<IRenderLoop>().ToConstant(threading)
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<IRenderTimer>().ToConstant(threading)
.Bind<ISystemDialogImpl>().ToSingleton<SystemDialogsStub>()
.Bind<IWindowingPlatform>().ToConstant(instance)
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>();

19
src/Avalonia.Styling/Styling/Style.cs

@ -111,17 +111,20 @@ namespace Avalonia.Styling
{
var subs = GetSubscriptions(control);
foreach (var animation in Animations)
if (control is Animatable animatable)
{
IObservable<bool> obsMatch = match.ObservableResult;
if (match.ImmediateResult == true)
foreach (var animation in Animations)
{
obsMatch = Observable.Return(true);
}
IObservable<bool> obsMatch = match.ObservableResult;
var sub = animation.Apply((Animatable)control, obsMatch);
subs.Add(sub);
if (match.ImmediateResult == true)
{
obsMatch = Observable.Return(true);
}
var sub = animation.Apply(animatable, null, obsMatch);
subs.Add(sub);
}
}
foreach (var setter in Setters)

26
src/Avalonia.Visuals/Animation/RenderLoopClock.cs

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Rendering;
namespace Avalonia.Animation
{
public class RenderLoopClock : ClockBase, IRenderLoopTask, IGlobalClock
{
protected override void Stop()
{
AvaloniaLocator.Current.GetService<IRenderLoop>().Remove(this);
}
bool IRenderLoopTask.NeedsUpdate => HasSubscriptions;
void IRenderLoopTask.Render()
{
}
void IRenderLoopTask.Update(TimeSpan time)
{
Pulse(time);
}
}
}

20
src/Avalonia.Visuals/Animation/TransformAnimator.cs

@ -9,10 +9,10 @@ namespace Avalonia.Animation
/// </summary>
public class TransformAnimator : Animator<double>
{
DoubleAnimator childKeyFrames;
DoubleAnimator childAnimator;
/// <inheritdoc/>
public override IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch, Action onComplete)
public override IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable<bool> obsMatch, Action onComplete)
{
var ctrl = (Visual)control;
@ -36,15 +36,15 @@ namespace Avalonia.Animation
var renderTransformType = ctrl.RenderTransform.GetType();
if (childKeyFrames == null)
if (childAnimator == null)
{
InitializeChildKeyFrames();
InitializeChildAnimator();
}
// It's a transform object so let's target that.
if (renderTransformType == Property.OwnerType)
{
return childKeyFrames.Apply(animation, ctrl.RenderTransform, obsMatch, onComplete);
return childAnimator.Apply(animation, ctrl.RenderTransform, clock ?? control.Clock, obsMatch, onComplete);
}
// It's a TransformGroup and try finding the target there.
else if (renderTransformType == typeof(TransformGroup))
@ -53,7 +53,7 @@ namespace Avalonia.Animation
{
if (transform.GetType() == Property.OwnerType)
{
return childKeyFrames.Apply(animation, transform, obsMatch, onComplete);
return childAnimator.Apply(animation, transform, clock ?? control.Clock, obsMatch, onComplete);
}
}
}
@ -73,16 +73,16 @@ namespace Avalonia.Animation
return null;
}
void InitializeChildKeyFrames()
void InitializeChildAnimator()
{
childKeyFrames = new DoubleAnimator();
childAnimator = new DoubleAnimator();
foreach (AnimatorKeyFrame keyframe in this)
{
childKeyFrames.Add(keyframe);
childAnimator.Add(keyframe);
}
childKeyFrames.Property = Property;
childAnimator.Property = Property;
}
/// <inheritdocs/>

1
src/Avalonia.Visuals/Avalonia.Visuals.csproj

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>Avalonia</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />

24
src/Avalonia.Visuals/Rendering/DefaultRenderLoop.cs → src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs

@ -2,31 +2,33 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Avalonia.Platform;
namespace Avalonia.Rendering
{
/// <summary>
/// Defines a default render loop that uses a standard timer.
/// Defines a default render timer that uses a standard timer.
/// </summary>
/// <remarks>
/// This class may be overridden by platform implementations to use a specialized timer
/// implementation.
/// </remarks>
public class DefaultRenderLoop : IRenderLoop
public class DefaultRenderTimer : IRenderTimer
{
private IRuntimePlatform _runtime;
private int _subscriberCount;
private EventHandler<EventArgs> _tick;
private Action<TimeSpan> _tick;
private IDisposable _subscription;
/// <summary>
/// Initializes a new instance of the <see cref="DefaultRenderLoop"/> class.
/// Initializes a new instance of the <see cref="DefaultRenderTimer"/> class.
/// </summary>
/// <param name="framesPerSecond">
/// The number of frames per second at which the loop should run.
/// </param>
public DefaultRenderLoop(int framesPerSecond)
public DefaultRenderTimer(int framesPerSecond)
{
FramesPerSecond = framesPerSecond;
}
@ -37,7 +39,7 @@ namespace Avalonia.Rendering
public int FramesPerSecond { get; }
/// <inheritdoc/>
public event EventHandler<EventArgs> Tick
public event Action<TimeSpan> Tick
{
add
{
@ -76,14 +78,16 @@ namespace Avalonia.Rendering
/// This can be overridden by platform implementations to use a specialized timer
/// implementation.
/// </remarks>
protected virtual IDisposable StartCore(Action tick)
protected virtual IDisposable StartCore(Action<TimeSpan> tick)
{
if (_runtime == null)
{
_runtime = AvaloniaLocator.Current.GetService<IRuntimePlatform>();
}
return _runtime.StartSystemTimer(TimeSpan.FromSeconds(1.0 / FramesPerSecond), tick);
return _runtime.StartSystemTimer(
TimeSpan.FromSeconds(1.0 / FramesPerSecond),
() => tick(TimeSpan.FromMilliseconds(Environment.TickCount)));
}
/// <summary>
@ -95,9 +99,9 @@ namespace Avalonia.Rendering
_subscription = null;
}
private void InternalTick()
private void InternalTick(TimeSpan tickCount)
{
_tick(this, EventArgs.Empty);
_tick(tickCount);
}
}
}

93
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@ -13,6 +13,7 @@ using Avalonia.Rendering.SceneGraph;
using Avalonia.Threading;
using Avalonia.Utilities;
using Avalonia.VisualTree;
using System.Threading.Tasks;
namespace Avalonia.Rendering
{
@ -20,7 +21,7 @@ namespace Avalonia.Rendering
/// A renderer which renders the state of the visual tree to an intermediate scene graph
/// representation which is then rendered on a rendering thread.
/// </summary>
public class DeferredRenderer : RendererBase, IRenderer, IVisualBrushRenderer
public class DeferredRenderer : RendererBase, IRenderer, IRenderLoopTask, IVisualBrushRenderer
{
private readonly IDispatcher _dispatcher;
private readonly IRenderLoop _renderLoop;
@ -31,7 +32,6 @@ namespace Avalonia.Rendering
private volatile IRef<Scene> _scene;
private DirtyVisuals _dirty;
private IRef<IRenderTargetBitmapImpl> _overlay;
private bool _updateQueued;
private object _rendering = new object();
private int _lastSceneId = -1;
private DisplayDirtyRects _dirtyRectsDisplay = new DisplayDirtyRects();
@ -149,7 +149,7 @@ namespace Avalonia.Rendering
{
if (!_running && _renderLoop != null)
{
_renderLoop.Tick += OnRenderLoopTick;
_renderLoop.Add(this);
_running = true;
}
}
@ -159,11 +159,23 @@ namespace Avalonia.Rendering
{
if (_running && _renderLoop != null)
{
_renderLoop.Tick -= OnRenderLoopTick;
_renderLoop.Remove(this);
_running = false;
}
}
bool IRenderLoopTask.NeedsUpdate => _dirty == null || _dirty.Count > 0;
void IRenderLoopTask.Update(TimeSpan time) => UpdateScene();
void IRenderLoopTask.Render()
{
using (var scene = _scene?.Clone())
{
Render(scene?.Item);
}
}
/// <inheritdoc/>
Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
{
@ -381,67 +393,34 @@ namespace Avalonia.Rendering
private void UpdateScene()
{
Dispatcher.UIThread.VerifyAccess();
try
if (_root.IsVisible)
{
if (_root.IsVisible)
{
var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root));
var scene = sceneRef.Item;
if (_dirty == null)
{
_dirty = new DirtyVisuals();
_sceneBuilder.UpdateAll(scene);
}
else if (_dirty.Count > 0)
{
foreach (var visual in _dirty)
{
_sceneBuilder.Update(scene, visual);
}
}
var oldScene = Interlocked.Exchange(ref _scene, sceneRef);
oldScene?.Dispose();
var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root));
var scene = sceneRef.Item;
_dirty.Clear();
(_root as IRenderRoot)?.Invalidate(new Rect(scene.Size));
}
else
if (_dirty == null)
{
var oldScene = Interlocked.Exchange(ref _scene, null);
oldScene?.Dispose();
_dirty = new DirtyVisuals();
_sceneBuilder.UpdateAll(scene);
}
}
finally
{
_updateQueued = false;
}
}
private void OnRenderLoopTick(object sender, EventArgs e)
{
if (Monitor.TryEnter(_rendering))
{
try
else if (_dirty.Count > 0)
{
if (!_updateQueued && (_dirty == null || _dirty.Count > 0))
foreach (var visual in _dirty)
{
_updateQueued = true;
_dispatcher.Post(UpdateScene, DispatcherPriority.Render);
}
using (var scene = _scene?.Clone())
{
Render(scene?.Item);
_sceneBuilder.Update(scene, visual);
}
}
catch { }
finally
{
Monitor.Exit(_rendering);
}
var oldScene = Interlocked.Exchange(ref _scene, sceneRef);
oldScene?.Dispose();
_dirty.Clear();
(_root as IRenderRoot)?.Invalidate(new Rect(scene.Size));
}
else
{
var oldScene = Interlocked.Exchange(ref _scene, null);
oldScene?.Dispose();
}
}

27
src/Avalonia.Visuals/Rendering/IRenderLoop.cs

@ -1,19 +1,28 @@
using System;
namespace Avalonia.Rendering
namespace Avalonia.Rendering
{
/// <summary>
/// Defines the interface implemented by an application render loop.
/// The application render loop.
/// </summary>
/// <remarks>
/// The render loop is responsible for advancing the animation timer and updating the scene
/// graph for visible windows.
/// </remarks>
public interface IRenderLoop
{
/// <summary>
/// Raised when the render loop ticks to signal a new frame should be drawn.
/// Adds an update task.
/// </summary>
/// <param name="i">The update task.</param>
/// <remarks>
/// This event can be raised on any thread; it is the responsibility of the subscriber to
/// switch execution to the right thread.
/// Registered update tasks will be polled on each tick of the render loop after the
/// animation timer has been pulsed.
/// </remarks>
event EventHandler<EventArgs> Tick;
void Add(IRenderLoopTask i);
/// <summary>
/// Removes an update task.
/// </summary>
/// <param name="i">The update task.</param>
void Remove(IRenderLoopTask i);
}
}
}

12
src/Avalonia.Visuals/Rendering/IRenderLoopTask.cs

@ -0,0 +1,12 @@
using System;
using System.Threading.Tasks;
namespace Avalonia.Rendering
{
public interface IRenderLoopTask
{
bool NeedsUpdate { get; }
void Update(TimeSpan time);
void Render();
}
}

20
src/Avalonia.Visuals/Rendering/IRenderTimer.cs

@ -0,0 +1,20 @@
using System;
using System.Threading.Tasks;
namespace Avalonia.Rendering
{
/// <summary>
/// Defines the interface implemented by an application render timer.
/// </summary>
public interface IRenderTimer
{
/// <summary>
/// Raised when the render timer ticks to signal a new frame should be drawn.
/// </summary>
/// <remarks>
/// This event can be raised on any thread; it is the responsibility of the subscriber to
/// switch execution to the right thread.
/// </remarks>
event Action<TimeSpan> Tick;
}
}

120
src/Avalonia.Visuals/Rendering/RenderLoop.cs

@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Avalonia.Logging;
using Avalonia.Threading;
namespace Avalonia.Rendering
{
/// <summary>
/// The application render loop.
/// </summary>
/// <remarks>
/// The render loop is responsible for advancing the animation timer and updating the scene
/// graph for visible windows.
/// </remarks>
public class RenderLoop : IRenderLoop
{
private readonly IDispatcher _dispatcher;
private List<IRenderLoopTask> _items = new List<IRenderLoopTask>();
private IRenderTimer _timer;
private int inTick;
/// <summary>
/// Initializes a new instance of the <see cref="RenderLoop"/> class.
/// </summary>
public RenderLoop()
{
_dispatcher = Dispatcher.UIThread;
}
/// <summary>
/// Initializes a new instance of the <see cref="RenderLoop"/> class.
/// </summary>
/// <param name="timer">The render timer.</param>
/// <param name="dispatcher">The UI thread dispatcher.</param>
public RenderLoop(IRenderTimer timer, IDispatcher dispatcher)
{
_timer = timer;
_dispatcher = dispatcher;
}
/// <summary>
/// Gets the render timer.
/// </summary>
protected IRenderTimer Timer
{
get
{
if (_timer == null)
{
_timer = AvaloniaLocator.Current.GetService<IRenderTimer>();
}
return _timer;
}
}
/// <inheritdoc/>
public void Add(IRenderLoopTask i)
{
Contract.Requires<ArgumentNullException>(i != null);
Dispatcher.UIThread.VerifyAccess();
_items.Add(i);
if (_items.Count == 1)
{
Timer.Tick += TimerTick;
}
}
/// <inheritdoc/>
public void Remove(IRenderLoopTask i)
{
Contract.Requires<ArgumentNullException>(i != null);
Dispatcher.UIThread.VerifyAccess();
_items.Remove(i);
if (_items.Count == 0)
{
Timer.Tick -= TimerTick;
}
}
private async void TimerTick(TimeSpan time)
{
if (Interlocked.CompareExchange(ref inTick, 1, 0) == 0)
{
try
{
if (_items.Any(item => item.NeedsUpdate))
{
await _dispatcher.InvokeAsync(() =>
{
foreach (var i in _items)
{
i.Update(time);
}
}, DispatcherPriority.Render).ConfigureAwait(false);
}
foreach (var i in _items)
{
i.Render();
}
}
catch (Exception ex)
{
Logger.Error(LogArea.Visual, this, "Exception in render loop: {Error}", ex);
}
finally
{
Interlocked.Exchange(ref inTick, 0);
}
}
}
}
}

3
src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs

@ -52,7 +52,8 @@ namespace Avalonia.Gtk3
.Bind<IPlatformSettings>().ToConstant(Instance)
.Bind<IPlatformThreadingInterface>().ToConstant(Instance)
.Bind<ISystemDialogImpl>().ToSingleton<SystemDialog>()
.Bind<IRenderLoop>().ToConstant(new DefaultRenderLoop(60))
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
.Bind<IPlatformIconLoader>().ToConstant(new PlatformIconLoader());
}

3
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@ -35,7 +35,8 @@ namespace Avalonia.LinuxFramebuffer
.Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
.Bind<IPlatformSettings>().ToSingleton<PlatformSettings>()
.Bind<IPlatformThreadingInterface>().ToConstant(Threading)
.Bind<IRenderLoop>().ToConstant(Threading);
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<IRenderTimer>().ToConstant(Threading);
}
internal static TopLevel Initialize<T>(T builder, string fbdev = null) where T : AppBuilderBase<T>, new()

7
src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs

@ -21,6 +21,7 @@ namespace Avalonia.MonoMac
private static bool s_monoMacInitialized;
private static bool s_showInDock = true;
private static IRenderLoop s_renderLoop;
private static IRenderTimer s_renderTimer;
void DoInitialize()
{
@ -35,6 +36,7 @@ namespace Avalonia.MonoMac
.Bind<ISystemDialogImpl>().ToSingleton<SystemDialogsImpl>()
.Bind<IClipboard>().ToSingleton<ClipboardImpl>()
.Bind<IRenderLoop>().ToConstant(s_renderLoop)
.Bind<IRenderTimer>().ToConstant(s_renderTimer)
.Bind<IPlatformThreadingInterface>().ToConstant(PlatformThreadingInterface.Instance)
/*.Bind<IPlatformDragSource>().ToTransient<DragSource>()*/;
}
@ -83,7 +85,8 @@ namespace Avalonia.MonoMac
ThreadHelper.InitializeCocoaThreadingLocks();
App = NSApplication.SharedApplication;
UpdateActivationPolicy();
s_renderLoop = new RenderLoop(); //TODO: use CVDisplayLink
s_renderLoop = new RenderLoop();
s_renderTimer = new RenderTimer(60); //TODO: use CVDisplayLink
s_monoMacInitialized = true;
}
@ -133,4 +136,4 @@ namespace Avalonia
return builder.UseWindowingSubsystem(MonoMac.MonoMacPlatform.Initialize, "MonoMac");
}
}
}
}

31
src/OSX/Avalonia.MonoMac/RenderLoop.cs

@ -1,31 +0,0 @@
using System;
using Avalonia.Platform;
using Avalonia.Rendering;
using MonoMac.Foundation;
namespace Avalonia.MonoMac
{
//TODO: Switch to using CVDisplayLink
public class RenderLoop : IRenderLoop
{
private readonly object _lock = new object();
private readonly IDisposable _timer;
public RenderLoop()
{
_timer = AvaloniaLocator.Current.GetService<IRuntimePlatform>().StartSystemTimer(new TimeSpan(0, 0, 0, 0, 1000 / 60),
() =>
{
lock (_lock)
{
using (new NSAutoreleasePool())
{
Tick?.Invoke(this, EventArgs.Empty);
}
}
});
}
public event EventHandler<EventArgs> Tick;
}
}

28
src/OSX/Avalonia.MonoMac/RenderTimer.cs

@ -0,0 +1,28 @@
using System;
using Avalonia.Platform;
using Avalonia.Rendering;
using MonoMac.Foundation;
namespace Avalonia.MonoMac
{
//TODO: Switch to using CVDisplayLink
public class RenderTimer : DefaultRenderTimer
{
public RenderTimer(int framesPerSecond) : base(framesPerSecond)
{
}
protected override IDisposable StartCore(Action<TimeSpan> tick)
{
return AvaloniaLocator.Current.GetService<IRuntimePlatform>().StartSystemTimer(
TimeSpan.FromSeconds(1.0 / FramesPerSecond),
() =>
{
using (new NSAutoreleasePool())
{
tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount));
}
});
}
}
}

25
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -26,6 +26,9 @@ namespace Avalonia.Win32.Interop
public delegate void TimeCallback(uint uTimerID, uint uMsg, UIntPtr dwUser, UIntPtr dw1, UIntPtr dw2);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate void WaitOrTimerCallback(IntPtr lpParameter, bool timerOrWaitFired);
public delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
public enum Cursor
@ -848,11 +851,25 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, ShowWindowCommand nCmdShow);
[DllImport("Winmm.dll")]
public static extern uint timeKillEvent(uint uTimerID);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr CreateTimerQueue();
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool DeleteTimerQueueEx(IntPtr TimerQueue, IntPtr CompletionEvent);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CreateTimerQueueTimer(
out IntPtr phNewTimer,
IntPtr TimerQueue,
WaitOrTimerCallback Callback,
IntPtr Parameter,
uint DueTime,
uint Period,
uint Flags);
[DllImport("Winmm.dll")]
public static extern uint timeSetEvent(uint uDelay, uint uResolution, TimeCallback lpTimeProc, UIntPtr dwUser, uint fuEvent);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool DeleteTimerQueueTimer(IntPtr TimerQueue, IntPtr Timer, IntPtr CompletionEvent);
[DllImport("user32.dll")]
public static extern int ToUnicode(

35
src/Windows/Avalonia.Win32/RenderLoop.cs

@ -1,35 +0,0 @@
using System;
using System.Reactive.Disposables;
using Avalonia.Rendering;
using Avalonia.Win32.Interop;
namespace Avalonia.Win32
{
internal class RenderLoop : DefaultRenderLoop
{
private UnmanagedMethods.TimeCallback timerDelegate;
public RenderLoop(int framesPerSecond)
: base(framesPerSecond)
{
}
protected override IDisposable StartCore(Action tick)
{
timerDelegate = (id, uMsg, user, dw1, dw2) => tick();
var handle = UnmanagedMethods.timeSetEvent(
(uint)(1000 / FramesPerSecond),
0,
timerDelegate,
UIntPtr.Zero,
1);
return Disposable.Create(() =>
{
timerDelegate = null;
UnmanagedMethods.timeKillEvent(handle);
});
}
}
}

56
src/Windows/Avalonia.Win32/RenderTimer.cs

@ -0,0 +1,56 @@
using System;
using System.Reactive.Disposables;
using System.Threading;
using Avalonia.Rendering;
using Avalonia.Win32.Interop;
namespace Avalonia.Win32
{
internal class RenderTimer : DefaultRenderTimer
{
private UnmanagedMethods.WaitOrTimerCallback timerDelegate;
private static IntPtr _timerQueue;
private static void EnsureTimerQueueCreated()
{
if (Volatile.Read(ref _timerQueue) == null)
{
var queue = UnmanagedMethods.CreateTimerQueue();
if (Interlocked.CompareExchange(ref _timerQueue, queue, IntPtr.Zero) != IntPtr.Zero)
{
UnmanagedMethods.DeleteTimerQueueEx(queue, IntPtr.Zero);
}
}
}
public RenderTimer(int framesPerSecond)
: base(framesPerSecond)
{
}
protected override IDisposable StartCore(Action<TimeSpan> tick)
{
EnsureTimerQueueCreated();
var msPerFrame = 1000 / FramesPerSecond;
timerDelegate = (_, __) => tick(TimeSpan.FromMilliseconds(Environment.TickCount));
UnmanagedMethods.CreateTimerQueueTimer(
out var timer,
_timerQueue,
timerDelegate,
IntPtr.Zero,
(uint)msPerFrame,
(uint)msPerFrame,
0
);
return Disposable.Create(() =>
{
timerDelegate = null;
UnmanagedMethods.DeleteTimerQueueTimer(_timerQueue, timer, IntPtr.Zero);
});
}
}
}

4
src/Windows/Avalonia.Win32/Win32Platform.cs

@ -9,6 +9,7 @@ using System.IO;
using System.Reactive.Disposables;
using System.Runtime.InteropServices;
using System.Threading;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Input;
@ -82,7 +83,8 @@ namespace Avalonia.Win32
.Bind<IKeyboardDevice>().ToConstant(WindowsKeyboardDevice.Instance)
.Bind<IPlatformSettings>().ToConstant(s_instance)
.Bind<IPlatformThreadingInterface>().ToConstant(s_instance)
.Bind<IRenderLoop>().ToConstant(new RenderLoop(60))
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<IRenderTimer>().ToConstant(new RenderTimer(60))
.Bind<ISystemDialogImpl>().ToSingleton<SystemDialogImpl>()
.Bind<IWindowingPlatform>().ToConstant(s_instance)
.Bind<IPlatformIconLoader>().ToConstant(s_instance);

9
src/iOS/Avalonia.iOS/DisplayLinkRenderLoop.cs → src/iOS/Avalonia.iOS/DisplayLinkRenderTimer.cs

@ -5,11 +5,12 @@ using Foundation;
namespace Avalonia.iOS
{
class DisplayLinkRenderLoop : IRenderLoop
class DisplayLinkRenderTimer : IRenderTimer
{
public event EventHandler<EventArgs> Tick;
public event Action<TimeSpan> Tick;
private CADisplayLink _link;
public DisplayLinkRenderLoop()
public DisplayLinkRenderTimer()
{
_link = CADisplayLink.Create(OnFrame);
@ -20,7 +21,7 @@ namespace Avalonia.iOS
{
try
{
Tick?.Invoke(this, new EventArgs());
Tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount));
}
catch (Exception)
{

3
src/iOS/Avalonia.iOS/iOSPlatform.cs

@ -41,7 +41,8 @@ namespace Avalonia.iOS
.Bind<IPlatformThreadingInterface>().ToConstant(PlatformThreadingInterface.Instance)
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoader>()
.Bind<IWindowingPlatform>().ToSingleton<WindowingPlatformImpl>()
.Bind<IRenderLoop>().ToSingleton<DisplayLinkRenderLoop>();
.Bind<IRenderTimer>().ToSingleton<DisplayLinkRenderTimer>()
.Bind<IRenderLoop>().ToSingleton<RenderLoop>();
}
}
}

4
tests/Avalonia.UnitTests/TestServices.cs

@ -64,7 +64,7 @@ namespace Avalonia.UnitTests
Func<IMouseDevice> mouseDevice = null,
IRuntimePlatform platform = null,
IPlatformRenderInterface renderInterface = null,
IRenderLoop renderLoop = null,
IRenderTimer renderLoop = null,
IScheduler scheduler = null,
IStandardCursorFactory standardCursorFactory = null,
IStyler styler = null,
@ -115,7 +115,7 @@ namespace Avalonia.UnitTests
Func<IMouseDevice> mouseDevice = null,
IRuntimePlatform platform = null,
IPlatformRenderInterface renderInterface = null,
IRenderLoop renderLoop = null,
IRenderTimer renderLoop = null,
IScheduler scheduler = null,
IStandardCursorFactory standardCursorFactory = null,
IStyler styler = null,

64
tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs

@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Subjects;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Media;
@ -22,27 +22,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering
{
public class DeferredRendererTests
{
[Fact]
public void First_Frame_Calls_UpdateScene_On_Dispatcher()
{
var root = new TestRoot();
var dispatcher = new Mock<IDispatcher>();
dispatcher.Setup(x => x.Post(It.IsAny<Action>(), DispatcherPriority.Render))
.Callback<Action, DispatcherPriority>((a, p) => a());
CreateTargetAndRunFrame(root, dispatcher: dispatcher.Object);
dispatcher.Verify(x =>
x.Post(
It.Is<Action>(a => a.Method.Name == "UpdateScene"),
DispatcherPriority.Render));
}
[Fact]
public void First_Frame_Calls_SceneBuilder_UpdateAll()
{
var loop = new Mock<IRenderLoop>();
var root = new TestRoot();
var sceneBuilder = MockSceneBuilder(root);
@ -54,6 +36,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
[Fact]
public void Frame_Does_Not_Call_SceneBuilder_If_No_Dirty_Controls()
{
var dispatcher = new ImmediateDispatcher();
var loop = new Mock<IRenderLoop>();
var root = new TestRoot();
var sceneBuilder = MockSceneBuilder(root);
@ -63,8 +46,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering
sceneBuilder: sceneBuilder.Object);
target.Start();
IgnoreFirstFrame(loop, sceneBuilder);
RunFrame(loop);
IgnoreFirstFrame(target, sceneBuilder);
RunFrame(target);
sceneBuilder.Verify(x => x.UpdateAll(It.IsAny<Scene>()), Times.Never);
sceneBuilder.Verify(x => x.Update(It.IsAny<Scene>(), It.IsAny<Visual>()), Times.Never);
@ -73,8 +56,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering
[Fact]
public void Should_Update_Dirty_Controls_In_Order()
{
var loop = new Mock<IRenderLoop>();
var dispatcher = new ImmediateDispatcher();
var loop = new Mock<IRenderLoop>();
Border border;
Decorator decorator;
@ -98,7 +81,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
dispatcher: dispatcher);
target.Start();
IgnoreFirstFrame(loop, sceneBuilder);
IgnoreFirstFrame(target, sceneBuilder);
target.AddDirty(border);
target.AddDirty(canvas);
target.AddDirty(root);
@ -108,7 +91,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering
sceneBuilder.Setup(x => x.Update(It.IsAny<Scene>(), It.IsAny<IVisual>()))
.Callback<Scene, IVisual>((_, v) => result.Add(v));
RunFrame(loop);
RunFrame(target);
Assert.Equal(new List<IVisual> { root, decorator, border, canvas }, result);
}
@ -198,7 +181,6 @@ namespace Avalonia.Visuals.UnitTests.Rendering
[Fact]
public void Should_Create_Layer_For_Root()
{
var loop = new Mock<IRenderLoop>();
var root = new TestRoot();
var rootLayer = new Mock<IRenderTargetBitmapImpl>();
@ -239,19 +221,19 @@ namespace Avalonia.Visuals.UnitTests.Rendering
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
var loop = new Mock<IRenderLoop>();
var target = CreateTargetAndRunFrame(root, loop: loop);
var timer = new Mock<IRenderTimer>();
var target = CreateTargetAndRunFrame(root, timer);
Assert.Equal(new[] { root }, target.Layers.Select(x => x.LayerRoot));
var animation = new BehaviorSubject<double>(0.5);
border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
RunFrame(loop);
RunFrame(target);
Assert.Equal(new IVisual[] { root, border }, target.Layers.Select(x => x.LayerRoot));
animation.OnCompleted();
RunFrame(loop);
RunFrame(target);
Assert.Equal(new[] { root }, target.Layers.Select(x => x.LayerRoot));
}
@ -280,8 +262,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
var loop = new Mock<IRenderLoop>();
var target = CreateTargetAndRunFrame(root, loop: loop);
var timer = new Mock<IRenderTimer>();
var target = CreateTargetAndRunFrame(root, timer);
Assert.Single(target.Layers);
}
@ -345,19 +327,20 @@ namespace Avalonia.Visuals.UnitTests.Rendering
private DeferredRenderer CreateTargetAndRunFrame(
TestRoot root,
Mock<IRenderLoop> loop = null,
Mock<IRenderTimer> timer = null,
ISceneBuilder sceneBuilder = null,
IDispatcher dispatcher = null)
{
loop = loop ?? new Mock<IRenderLoop>();
timer = timer ?? new Mock<IRenderTimer>();
dispatcher = dispatcher ?? new ImmediateDispatcher();
var target = new DeferredRenderer(
root,
loop.Object,
new RenderLoop(timer.Object, dispatcher),
sceneBuilder: sceneBuilder,
dispatcher: dispatcher ?? new ImmediateDispatcher());
dispatcher: dispatcher);
root.Renderer = target;
target.Start();
RunFrame(loop);
RunFrame(target);
return target;
}
@ -366,15 +349,16 @@ namespace Avalonia.Visuals.UnitTests.Rendering
return Mock.Get(renderer.Layers[layerRoot].Bitmap.Item.CreateDrawingContext(null));
}
private void IgnoreFirstFrame(Mock<IRenderLoop> loop, Mock<ISceneBuilder> sceneBuilder)
private void IgnoreFirstFrame(IRenderLoopTask task, Mock<ISceneBuilder> sceneBuilder)
{
RunFrame(loop);
RunFrame(task);
sceneBuilder.ResetCalls();
}
private void RunFrame(Mock<IRenderLoop> loop)
private void RunFrame(IRenderLoopTask task)
{
loop.Raise(x => x.Tick += null, EventArgs.Empty);
task.Update(TimeSpan.Zero);
task.Render();
}
private IRenderTargetBitmapImpl CreateLayer()

119
tests/Avalonia.Visuals.UnitTests/Rendering/RenderLoopTests.cs

@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Rendering;
using Avalonia.Threading;
using Moq;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Rendering
{
public class RenderLoopTests
{
[Fact]
public void RenderLoop_Update_Runs_On_Dispatcher()
{
var dispatcher = new Mock<IDispatcher>();
bool inDispatcher = false;
dispatcher.Setup(
d => d.InvokeAsync(It.IsAny<Action>(), DispatcherPriority.Render))
.Callback((Action a, DispatcherPriority _) =>
{
inDispatcher = true;
a();
inDispatcher = false;
})
.Returns(Task.CompletedTask);
var timer = new Mock<IRenderTimer>();
var loop = new RenderLoop(timer.Object, dispatcher.Object);
var renderTask = new Mock<IRenderLoopTask>();
renderTask.Setup(t => t.NeedsUpdate).Returns(true);
renderTask.Setup(t => t.Update(It.IsAny<TimeSpan>()))
.Callback((TimeSpan _) => Assert.True(inDispatcher));
loop.Add(renderTask.Object);
timer.Raise(t => t.Tick += null, TimeSpan.Zero);
renderTask.Verify(t => t.Update(It.IsAny<TimeSpan>()), Times.Once());
}
[Fact]
public void RenderLoop_Does_Not_Update_When_No_Tasks_Need_Update()
{
var dispatcher = new Mock<IDispatcher>();
dispatcher.Setup(
d => d.InvokeAsync(It.IsAny<Action>(), DispatcherPriority.Render))
.Callback((Action a, DispatcherPriority _) => a())
.Returns(Task.CompletedTask);
var timer = new Mock<IRenderTimer>();
var loop = new RenderLoop(timer.Object, dispatcher.Object);
var renderTask = new Mock<IRenderLoopTask>();
renderTask.Setup(t => t.NeedsUpdate).Returns(false);
loop.Add(renderTask.Object);
timer.Raise(t => t.Tick += null, TimeSpan.Zero);
renderTask.Verify(t => t.Update(It.IsAny<TimeSpan>()), Times.Never());
}
[Fact]
public void RenderLoop_Render_Runs_Off_Dispatcher()
{
var dispatcher = new Mock<IDispatcher>();
bool inDispatcher = false;
dispatcher.Setup(
d => d.InvokeAsync(It.IsAny<Action>(), DispatcherPriority.Render))
.Callback((Action a, DispatcherPriority _) =>
{
inDispatcher = true;
a();
inDispatcher = false;
})
.Returns(Task.CompletedTask);
var timer = new Mock<IRenderTimer>();
var loop = new RenderLoop(timer.Object, dispatcher.Object);
var renderTask = new Mock<IRenderLoopTask>();
renderTask.Setup(t => t.NeedsUpdate).Returns(true);
renderTask.Setup(t => t.Render())
.Callback(() => Assert.False(inDispatcher));
loop.Add(renderTask.Object);
timer.Raise(t => t.Tick += null, TimeSpan.Zero);
renderTask.Verify(t => t.Update(It.IsAny<TimeSpan>()), Times.Once());
}
[Fact]
public void RenderLoop_Passes_Tick_Count_To_Update()
{
var dispatcher = new Mock<IDispatcher>();
dispatcher.Setup(
d => d.InvokeAsync(It.IsAny<Action>(), DispatcherPriority.Render))
.Callback((Action a, DispatcherPriority _) => a())
.Returns(Task.CompletedTask);
var timer = new Mock<IRenderTimer>();
var loop = new RenderLoop(timer.Object, dispatcher.Object);
var renderTask = new Mock<IRenderLoopTask>();
renderTask.Setup(t => t.NeedsUpdate).Returns(true);
loop.Add(renderTask.Object);
var time = new TimeSpan(123456789L);
timer.Raise(t => t.Tick += null, time);
renderTask.Verify(t => t.Update(time), Times.Once());
}
}
}
Loading…
Cancel
Save