< Summary

Information
Class: SwitchBlade.Services.BadgeAnimationService
Assembly: SwitchBlade
File(s): D:\a\switchblade\switchblade\Services\BadgeAnimationService.cs
Tag: 203_23722840422
Line coverage
100%
Covered lines: 80
Uncovered lines: 0
Coverable lines: 80
Total lines: 168
Line coverage: 100%
Branch coverage
100%
Covered branches: 36
Total branches: 36
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_AnimationDurationMs()100%11100%
get_StaggerDelayMs()100%11100%
get_StartingOffsetX()100%11100%
get_DebounceMs()100%11100%
.ctor(...)100%44100%
ResetAnimationState(...)100%44100%
TriggerStaggeredAnimationAsync()100%2828100%

File(s)

D:\a\switchblade\switchblade\Services\BadgeAnimationService.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Threading;
 4using System.Threading.Tasks;
 5using SwitchBlade.Contracts;
 6using SwitchBlade.Core;
 7
 8namespace SwitchBlade.Services
 9{
 10    /// <summary>
 11    /// Coordinates staggered badge animations for Alt+Number shortcuts.
 12    /// Delegates the actual animation execution to an IBadgeAnimator strategy.
 13    /// Uses debouncing to prevent animation fighting during rapid input.
 14    /// </summary>
 15    public class BadgeAnimationService
 16    {
 17        private readonly IBadgeAnimator _animator;
 18        private readonly IDelayProvider _delayProvider;
 19        private CancellationTokenSource? _animationCts;
 20
 21        /// <summary>
 22        /// Duration of each badge's animation in milliseconds.
 23        /// </summary>
 4724        public int AnimationDurationMs { get; set; } = 150;
 25
 26        /// <summary>
 27        /// Delay between each badge's animation start in milliseconds.
 28        /// </summary>
 6429        public int StaggerDelayMs { get; set; } = 75;
 30
 31        /// <summary>
 32        /// Starting X offset for slide-in animation (negative = from left).
 33        /// </summary>
 3434        public double StartingOffsetX { get; set; } = -20;
 35
 36        /// <summary>
 37        /// Debounce interval: how long to wait after the last trigger before
 38        /// actually starting animations. Prevents wasteful animation starts
 39        /// during rapid typing. Matched to one stagger step for natural feel.
 40        /// </summary>
 4241        public int DebounceMs { get; set; } = 75;
 42
 1943        public BadgeAnimationService(IBadgeAnimator animator, IDelayProvider? delayProvider = null)
 1944        {
 1945            _animator = animator ?? throw new ArgumentNullException(nameof(animator));
 1846            _delayProvider = delayProvider ?? new SystemDelayProvider();
 1847        }
 48
 49        /// <summary>
 50        /// Resets the animation state for the provided items.
 51        /// Use this when you want to force re-animation (e.g. on new search or window open).
 52        /// </summary>
 53        public static void ResetAnimationState(IEnumerable<WindowItem>? items)
 254        {
 355            if (items == null) return;
 56
 57            // We just reset the flag. We do NOT reset the visual Opacity/TranslateX here.
 58            // Pushing visual state to hidden happens just-in-time in TriggerStaggeredAnimationAsync.
 559            foreach (var item in items)
 160            {
 161                item.HasBeenAnimated = false;
 162            }
 163            SwitchBlade.Core.Logger.Log($"[BadgeAnimation] ResetAnimationState: Reset HasBeenAnimated flag for items");
 264        }
 65
 66        /// <summary>
 67        /// Triggers staggered animations for the given window items.
 68        /// Only items with shortcuts (index 0-9) and not previously animated will animate.
 69        /// Uses debouncing: if called again within DebounceMs, the previous call is cancelled.
 70        /// This ensures animations only play once typing settles, preventing jitter.
 71        /// </summary>
 72        /// <param name="items">The items to animate.</param>
 73        /// <param name="skipDebounce">When true, skips the debounce delay (e.g., for hotkey/initial load).</param>
 74        public async Task TriggerStaggeredAnimationAsync(IEnumerable<WindowItem>? items, bool skipDebounce = false)
 1875        {
 1876            SwitchBlade.Core.Logger.Log($"[BadgeAnimation] TriggerStaggeredAnimationAsync: Starting");
 1977            if (items == null) return;
 78
 79            // Cancel any pending animation cycle from a previous call
 1780            _animationCts?.Cancel();
 1781            _animationCts?.Dispose();
 1782            _animationCts = new CancellationTokenSource();
 1783            var ct = _animationCts.Token;
 84
 85            // Immediately hide all badges that need animating (so they don't show stale state)
 9786            foreach (var item in items)
 2387            {
 2388                if (item.IsShortcutVisible && !item.HasBeenAnimated)
 1889                {
 1890                    item.ResetBadgeAnimation();
 1891                }
 2392            }
 93
 94            // Debounce: wait for typing to settle before starting the animation cycle.
 95            // If another call arrives during this window, this one is cancelled.
 96            // Skip debounce for hotkey/initial load so the animation feels responsive.
 1797            if (!skipDebounce)
 698            {
 99                try
 6100                {
 6101                    await _delayProvider.Delay(DebounceMs, ct);
 5102                }
 1103                catch (OperationCanceledException)
 1104                {
 1105                    return;
 106                }
 5107            }
 108
 17109            if (ct.IsCancellationRequested) return;
 110
 15111            int maxShortcutIndex = -1;
 15112            int animatedCount = 0;
 15113            int skippedCount = 0;
 114
 86115            foreach (var item in items)
 21116            {
 22117                if (ct.IsCancellationRequested) return;
 118
 20119                if (!item.IsShortcutVisible)
 1120                {
 1121                    continue;
 122                }
 123
 19124                bool shouldAnimate = !item.HasBeenAnimated;
 125
 19126                if (shouldAnimate)
 15127                {
 15128                    int delay = item.ShortcutIndex * StaggerDelayMs;
 129
 130                    // Delegate execution to the strategy
 15131                    _animator.Animate(item, delay, AnimationDurationMs, StartingOffsetX);
 132
 133                    // Mark as animated immediately so we don't re-animate on next pass
 15134                    item.HasBeenAnimated = true;
 15135                    animatedCount++;
 136
 15137                    if (item.ShortcutIndex > maxShortcutIndex)
 15138                    {
 15139                        maxShortcutIndex = item.ShortcutIndex;
 15140                    }
 15141                }
 142                else
 4143                {
 144                    // Already animated - ensure it's visible
 4145                    item.BadgeOpacity = 1.0;
 4146                    item.BadgeTranslateX = 0;
 4147                    skippedCount++;
 4148                }
 19149            }
 150
 14151            SwitchBlade.Core.Logger.Log($"[BadgeAnimation] TriggerStaggeredAnimationAsync: Animated={animatedCount}, Ski
 152
 153            // Wait for all animations to complete (approximate based on max duration)
 14154            if (maxShortcutIndex >= 0)
 11155            {
 11156                int maxDelay = (maxShortcutIndex + 1) * StaggerDelayMs + AnimationDurationMs;
 157                try
 11158                {
 11159                    await _delayProvider.Delay(maxDelay, ct);
 10160                }
 1161                catch (OperationCanceledException)
 1162                {
 163                    // Animation cycle was superseded — that's fine
 1164                }
 11165            }
 18166        }
 167    }
 168}