| File: System\Windows\Forms\Rendering\Animation\AnimationManager.cs | Web Access |
| Project: src\winforms\src\System.Windows.Forms\System.Windows.Forms.csproj (System.Windows.Forms) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; using System.Diagnostics.Tracing; using System.Windows.Forms.Animation; namespace System.Windows.Forms.Rendering.Animation; /// <summary> /// Per-UI-thread dispatcher that drives <see cref="AnimatedControlRenderer"/> instances from a single /// <c>HighPrecisionTimer</c> registration. /// </summary> /// <remarks> /// <para> /// A manager is created on demand for each UI thread. The timer captures that thread's /// <see cref="SynchronizationContext"/>, so per-frame work can invalidate controls directly without routing /// animations through another message loop. /// </para> /// </remarks> internal partial class AnimationManager { private readonly HighPrecisionTimer.TimerRegistration _timerRegistration; private readonly EventHandler _threadExitHandler; private readonly int _threadId; private readonly ConcurrentDictionary<AnimatedControlRenderer, AnimationRendererItem> _renderer = []; private bool _hasTickTimestamp; private int _disposed; private TimeSpan _lastTickTimestamp; [ThreadStatic] private static AnimationManager? s_instance; private static AnimationManager Instance => s_instance ??= new AnimationManager(); private AnimationManager() { _threadId = Environment.CurrentManagedThreadId; _threadExitHandler = OnThreadExit; _timerRegistration = HighPrecisionTimer.Register(OnFrameTickAsync); Application.ThreadExit += _threadExitHandler; } /// <summary> /// Disposes the animation renderers and releases the timer registration. /// </summary> private void DisposeRenderer() { if (Interlocked.Exchange(ref _disposed, 1) != 0) { return; } _timerRegistration.Dispose(); Application.ThreadExit -= _threadExitHandler; foreach (AnimatedControlRenderer renderer in _renderer.Keys) { _ = _renderer.TryRemove(renderer, out _); renderer.Dispose(); } if (ReferenceEquals(s_instance, this)) { s_instance = null; } } private void OnThreadExit(object? sender, EventArgs e) { if (Environment.CurrentManagedThreadId == _threadId) { DisposeRenderer(); } } /// <summary> /// Registers an animation renderer. /// </summary> /// <param name="animationRenderer">The animation renderer to register.</param> /// <param name="animationDuration">The duration of the animation.</param> /// <param name="animationCycle">The animation cycle.</param> public static void RegisterOrUpdateAnimationRenderer( AnimatedControlRenderer animationRenderer, int animationDuration, AnimationCycle animationCycle) { AnimationManager manager = Instance; // If the renderer is already registered, update the animation parameters. if (manager._renderer.TryGetValue(animationRenderer, out AnimationRendererItem? renderItem)) { manager.UpdateAnimationParameters(renderItem, animationDuration, animationCycle); return; } renderItem = new AnimationRendererItem(animationRenderer, animationDuration, animationCycle) { TargetTimestamp = manager.GetTargetTimestamp(animationDuration) }; _ = manager._renderer.TryAdd(animationRenderer, renderItem); } /// <summary> /// Unregisters an animation renderer. /// </summary> /// <param name="animationRenderer">The animation renderer to unregister.</param> internal static void UnregisterAnimationRenderer(AnimatedControlRenderer animationRenderer) { if (s_instance is AnimationManager manager) { _ = manager._renderer.TryRemove(animationRenderer, out _); } } internal static void Suspend(AnimatedControlRenderer animatedControlRenderer) { if (s_instance is AnimationManager manager && manager._renderer.TryRemove(animatedControlRenderer, out AnimationRendererItem? renderItem)) { renderItem.Renderer.StopAnimationInternal(); return; } animatedControlRenderer.StopAnimationInternal(); } /// <summary> /// Handles a single frame tick delivered by <c>HighPrecisionTimer</c> (on the UI thread). /// </summary> private ValueTask OnFrameTickAsync(HighPrecisionTimerTick tick, CancellationToken cancellationToken) { if (Volatile.Read(ref _disposed) != 0) { return ValueTask.CompletedTask; } _lastTickTimestamp = tick.Timestamp; _hasTickTimestamp = true; foreach (AnimationRendererItem item in _renderer.Values) { try { ProcessRenderer(item, tick.Timestamp); } catch (Exception ex) when (!ex.IsCriticalException()) { if (AnimationManagerEventSource.s_log.IsEnabled()) { AnimationManagerEventSource.s_log.RendererFault( item.Renderer.GetType().FullName ?? item.Renderer.GetType().Name, ex.GetType().FullName ?? ex.GetType().Name); } QuarantineRenderer(item); } } return ValueTask.CompletedTask; } private static void ProcessRenderer(AnimationRendererItem item, TimeSpan timestamp) { if (!item.Renderer.IsRunning) { return; } item.TargetTimestamp ??= timestamp + TimeSpan.FromMilliseconds(item.AnimationDuration); TimeSpan targetTimestamp = item.TargetTimestamp.Value; TimeSpan remaining = targetTimestamp - timestamp; item.FrameCount += item.FrameOffset; if (timestamp >= targetTimestamp) { switch (item.AnimationCycle) { case AnimationCycle.Once: item.Renderer.EndAnimation(); break; case AnimationCycle.Loop: item.FrameCount = 0; item.TargetTimestamp = timestamp + TimeSpan.FromMilliseconds(item.AnimationDuration); item.Renderer.RestartAnimation(); break; case AnimationCycle.Bounce: item.FrameOffset = -item.FrameOffset; item.TargetTimestamp = timestamp + TimeSpan.FromMilliseconds(item.AnimationDuration); item.Renderer.RestartAnimation(); break; } return; } float progress = 1 - (float)(remaining.TotalMilliseconds / item.AnimationDuration); // We are already on the UI thread (HighPrecisionTimer marshalled us here), so invoke directly. item.Renderer.AnimationProc(progress); } private void QuarantineRenderer(AnimationRendererItem item) { _ = _renderer.TryRemove(item.Renderer, out _); item.Renderer.StopAnimationInternal(); } private TimeSpan? GetTargetTimestamp(int animationDuration) => _hasTickTimestamp ? _lastTickTimestamp + TimeSpan.FromMilliseconds(animationDuration) : null; private void UpdateAnimationParameters( AnimationRendererItem item, int animationDuration, AnimationCycle animationCycle) { item.AnimationDuration = animationDuration; item.AnimationCycle = animationCycle; item.TargetTimestamp = GetTargetTimestamp(animationDuration); } /// <summary> /// Gets the manager associated with the calling UI thread for focused tests. /// </summary> internal static AnimationManager GetCurrentForTesting() => Instance; /// <summary> /// Disposes the manager associated with the calling UI thread for focused tests. /// </summary> internal static void DisposeCurrentForTesting() => s_instance?.DisposeRenderer(); /// <summary> /// Processes a timer tick synchronously for focused tests. /// </summary> internal ValueTask ProcessTickForTesting(HighPrecisionTimerTick tick) => OnFrameTickAsync(tick, CancellationToken.None); /// <summary> /// Gets the number of renderers currently managed by this UI-thread instance for focused tests. /// </summary> internal int RendererCountForTesting => _renderer.Count; /// <summary> /// Emits diagnostics when an individual renderer is quarantined. /// </summary> [EventSource(Name = "System.Windows.Forms.AnimationManager")] private sealed class AnimationManagerEventSource : EventSource { public static readonly AnimationManagerEventSource s_log = new(); [Event(1, Level = EventLevel.Error)] public void RendererFault(string rendererType, string exceptionType) => WriteEvent(1, rendererType, exceptionType); } }