File: System\Windows\Forms\Rendering\Animation\AnimatedFocusIndicatorRenderer.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.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms.Rendering.Button;
 
namespace System.Windows.Forms.Rendering.Animation;
 
/// <summary>
///  Animates a rounded focus indicator between a control's border and focus colors.
/// </summary>
internal sealed class AnimatedFocusIndicatorRenderer : AnimatedControlRenderer
{
    private const int AnimationDurationMilliseconds = 200;
 
    private readonly Action _invalidate;
    private float _focusCurrent;
    private float _focusStart;
    private float _focusTarget;
    private bool _initialized;
 
    public AnimatedFocusIndicatorRenderer(Control control, Action invalidate)
        : base(control)
    {
        ArgumentNullException.ThrowIfNull(invalidate);
        _invalidate = invalidate;
    }
 
    internal float FocusAmount
    {
        get
        {
            EnsureInitialized(Control.Focused ? 1f : 0f);
            return _focusCurrent;
        }
    }
 
    internal Color GetCurrentColor(Color borderColor, Color focusColor)
        => PopupButtonColorMath.Blend(borderColor, focusColor, FocusAmount);
 
    internal void SetFocused(bool focused, bool animate)
    {
        float target = focused ? 1f : 0f;
        EnsureInitialized(1f - target);
 
        if (_focusTarget == target && (!IsRunning || animate))
        {
            return;
        }
 
        _focusStart = _focusCurrent;
        _focusTarget = target;
 
        if (!animate)
        {
            Synchronize(focused, invalidate: true);
            return;
        }
 
        RestartAnimation();
        _invalidate();
    }
 
    internal void Synchronize(bool focused, bool invalidate)
    {
        if (IsRunning)
        {
            StopAnimation();
        }
 
        _focusCurrent = focused ? 1f : 0f;
        _focusStart = _focusCurrent;
        _focusTarget = _focusCurrent;
        _initialized = true;
        AnimationProgress = 1f;
 
        if (invalidate)
        {
            _invalidate();
        }
    }
 
    internal void DrawRoundedFocusIndicator(
        Graphics graphics,
        Rectangle bounds,
        int cornerSize,
        int borderThickness,
        int focusBandHeight,
        Color borderColor,
        Color focusColor)
    {
        if (bounds.Width <= 0
            || bounds.Height <= 0
            || cornerSize <= 0
            || borderThickness <= 0
            || focusBandHeight <= 0
            || FocusAmount <= 0f)
        {
            return;
        }
 
        int focusBandTop = Math.Max(bounds.Top, bounds.Bottom - focusBandHeight + 1);
        Rectangle focusClip = Rectangle.FromLTRB(
            bounds.Left,
            focusBandTop,
            bounds.Right + 1,
            bounds.Bottom + 1);
 
        using GraphicsStateScope state = new(graphics);
        graphics.SetClip(focusClip, CombineMode.Intersect);
        graphics.SmoothingMode = SmoothingMode.AntiAlias;
 
        using GraphicsPath focusPath = new();
        focusPath.AddRoundedRectangle(bounds, new Size(cornerSize, cornerSize));
 
        Color color = GetCurrentColor(borderColor, focusColor);
        using var focusPen = color.GetCachedPenScope(borderThickness);
        graphics.DrawPath(focusPen, focusPath);
    }
 
    public override void AnimationProc(float animationProgress)
    {
        base.AnimationProc(animationProgress);
        float easedProgress = EaseOut(animationProgress);
        _focusCurrent = Lerp(_focusStart, _focusTarget, easedProgress);
        _invalidate();
    }
 
    public override void RenderControl(Graphics graphics)
    {
    }
 
    protected override (int animationDuration, AnimationCycle animationCycle) OnAnimationStarted()
    {
        AnimationProgress = 0f;
        return (AnimationDurationMilliseconds, AnimationCycle.Once);
    }
 
    protected override void OnAnimationStopped()
    {
    }
 
    protected override void OnAnimationEnded()
    {
        StopAnimation();
        _focusCurrent = _focusTarget;
        _focusStart = _focusCurrent;
        AnimationProgress = 1f;
        _invalidate();
    }
 
    private void EnsureInitialized(float initialValue)
    {
        if (_initialized)
        {
            return;
        }
 
        _focusCurrent = initialValue;
        _focusStart = initialValue;
        _focusTarget = initialValue;
        _initialized = true;
    }
 
    private static float EaseOut(float progress)
        => 1f - ((1f - progress) * (1f - progress));
 
    private static float Lerp(float start, float end, float amount)
        => start + ((end - start) * Math.Clamp(amount, 0f, 1f));
}