committed by
GitHub
48 changed files with 816 additions and 444 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,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>
|
/// <summary>
|
||||
/// Defines the interface implemented by an application render loop.
|
/// The application render loop.
|
||||
/// </summary>
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// The render loop is responsible for advancing the animation timer and updating the scene
|
||||
|
/// graph for visible windows.
|
||||
|
/// </remarks>
|
||||
public interface IRenderLoop |
public interface IRenderLoop |
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// Raised when the render loop ticks to signal a new frame should be drawn.
|
/// Adds an update task.
|
||||
/// </summary>
|
/// </summary>
|
||||
|
/// <param name="i">The update task.</param>
|
||||
/// <remarks>
|
/// <remarks>
|
||||
/// This event can be raised on any thread; it is the responsibility of the subscriber to
|
/// Registered update tasks will be polled on each tick of the render loop after the
|
||||
/// switch execution to the right thread.
|
/// animation timer has been pulsed.
|
||||
/// </remarks>
|
/// </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