committed by
GitHub
58 changed files with 930 additions and 470 deletions
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
|
|||
namespace Avalonia.Animation |
|||
{ |
|||
public interface IGlobalClock : IClock |
|||
{ |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -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…
Reference in new issue