File: System\Windows\Forms\Rendering\CheckBox\AnimatedCheckGlyphRenderer.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.Animation;
 
namespace System.Windows.Forms.Rendering.CheckBox;
 
/// <summary>
///  Animates and draws the modern normal-appearance CheckBox glyph.
/// </summary>
internal sealed class AnimatedCheckGlyphRenderer : AnimatedControlRenderer
{
    private const int AnimationDuration = 220;
 
    private float _checkAlphaCurrent;
    private float _checkAlphaStart;
    private float _checkAlphaTarget;
    private float _dashAlphaCurrent;
    private float _dashAlphaStart;
    private float _dashAlphaTarget;
    private float _fillCurrent;
    private float _fillStart;
    private float _fillTarget;
    private float _interactionCurrent;
    private float _interactionStart;
    private float _interactionTarget;
    private bool _interactionInitialized;
    private bool _stateInitialized;
 
    public AnimatedCheckGlyphRenderer(Control control) : base(control)
    {
    }
 
    internal void NotifyCheckStateChanged(CheckState newState)
    {
        (float fill, float checkAlpha, float dashAlpha) = GetStateTargets(newState);
        if (!_stateInitialized)
        {
            _stateInitialized = true;
            _fillCurrent = _fillStart = _fillTarget = fill;
            _checkAlphaCurrent = _checkAlphaStart = _checkAlphaTarget = checkAlpha;
            _dashAlphaCurrent = _dashAlphaStart = _dashAlphaTarget = dashAlpha;
            return;
        }
 
        if (_fillTarget == fill
            && _checkAlphaTarget == checkAlpha
            && _dashAlphaTarget == dashAlpha)
        {
            return;
        }
 
        _fillTarget = fill;
        _checkAlphaTarget = checkAlpha;
        _dashAlphaTarget = dashAlpha;
        RestartAnimation();
    }
 
    internal void SetInteractionState(bool hovered, bool focused)
    {
        float interactionTarget = hovered || focused ? 1f : 0f;
        if (!_interactionInitialized)
        {
            _interactionInitialized = true;
            _interactionCurrent = _interactionStart = _interactionTarget = interactionTarget;
            return;
        }
 
        if (_interactionTarget == interactionTarget)
        {
            return;
        }
 
        _interactionTarget = interactionTarget;
        RestartAnimation();
    }
 
    internal void DrawGlyph(
        Graphics graphics,
        Rectangle bounds,
        FlatStyle flatStyle,
        bool enabled,
        bool hovered,
        bool focused,
        Color? customOnColor,
        Color? customBorderColor)
    {
        SetInteractionState(hovered, focused);
 
        bool isDark = Application.IsDarkModeEnabled;
        bool highContrast = SystemInformation.HighContrast;
        Color onColor = highContrast
            ? SystemColors.Highlight
            : customOnColor ?? WindowsAccentColor;
 
        Color offBorderColor = highContrast
            ? SystemColors.WindowText
            : customBorderColor
                ?? (isDark
                    ? Color.FromArgb(0x9B, 0x9B, 0x9B)
                    : SystemColors.ControlDark);
 
        Color offBackColor = highContrast
            ? SystemColors.Window
            : isDark
                ? Color.FromArgb(0x2D, 0x2D, 0x2D)
                : Color.White;
 
        if (!enabled)
        {
            onColor = highContrast
                ? SystemColors.GrayText
                : isDark
                    ? Color.FromArgb(0x55, 0x55, 0x55)
                    : Color.FromArgb(0xC0, 0xC0, 0xC0);
            offBorderColor = highContrast
                ? SystemColors.GrayText
                : isDark
                    ? Color.FromArgb(0x45, 0x45, 0x45)
                    : Color.FromArgb(0xD0, 0xD0, 0xD0);
        }
 
        Color backColor = LerpColor(offBackColor, onColor, _fillCurrent);
        Color borderColor = LerpColor(offBorderColor, onColor, _fillCurrent);
        float interaction = enabled && !highContrast ? _interactionCurrent : 0f;
        backColor = ApplyInteractionShade(backColor, interaction);
        borderColor = ApplyInteractionShade(borderColor, interaction);
 
        GraphicsState? saved = graphics.Save();
        try
        {
            graphics.SmoothingMode = SmoothingMode.AntiAlias;
            using GraphicsPath path = CreateBoxPath(bounds, flatStyle);
 
            using (var brush = backColor.GetCachedSolidBrushScope())
            {
                graphics.FillPath(brush, path);
            }
 
            int borderThickness = Math.Max(
                1,
                Control.LogicalToDeviceUnits(flatStyle == FlatStyle.Popup ? 2 : 1));
 
            using (var pen = new Pen(borderColor, borderThickness) { Alignment = PenAlignment.Inset })
            {
                graphics.DrawPath(pen, path);
            }
 
            Color glyphColor = enabled
                ? highContrast
                    ? SystemColors.HighlightText
                    : ModernButtonColorMath.GetReadableForeColor(onColor)
                : offBackColor;
 
            if (_checkAlphaCurrent > 0)
            {
                DrawCheckmark(graphics, bounds, glyphColor, _checkAlphaCurrent);
            }
 
            if (_dashAlphaCurrent > 0)
            {
                DrawDash(graphics, bounds, glyphColor, _dashAlphaCurrent);
            }
        }
        finally
        {
            if (saved is not null)
            {
                graphics.Restore(saved);
            }
        }
    }
 
    public override void AnimationProc(float animationProgress)
    {
        base.AnimationProc(animationProgress);
        float easedProgress = EaseOut(animationProgress);
        _fillCurrent = Lerp(_fillStart, _fillTarget, easedProgress);
        _checkAlphaCurrent = Lerp(_checkAlphaStart, _checkAlphaTarget, easedProgress);
        _dashAlphaCurrent = Lerp(_dashAlphaStart, _dashAlphaTarget, easedProgress);
        _interactionCurrent = Lerp(_interactionStart, _interactionTarget, easedProgress);
        Invalidate();
    }
 
    public override void RenderControl(Graphics graphics)
    {
    }
 
    protected override (int animationDuration, AnimationCycle animationCycle) OnAnimationStarted()
    {
        _fillStart = _fillCurrent;
        _checkAlphaStart = _checkAlphaCurrent;
        _dashAlphaStart = _dashAlphaCurrent;
        _interactionStart = _interactionCurrent;
        AnimationProgress = 0;
        return (AnimationDuration, AnimationCycle.Once);
    }
 
    protected override void OnAnimationStopped()
    {
    }
 
    protected override void OnAnimationEnded()
    {
        StopAnimation();
        _fillCurrent = _fillTarget;
        _checkAlphaCurrent = _checkAlphaTarget;
        _dashAlphaCurrent = _dashAlphaTarget;
        _interactionCurrent = _interactionTarget;
        AnimationProgress = 1;
        Invalidate();
    }
 
    private static (float Fill, float CheckAlpha, float DashAlpha) GetStateTargets(CheckState state)
        => state switch
        {
            CheckState.Checked => (1f, 1f, 0f),
            CheckState.Indeterminate => (1f, 0f, 1f),
            _ => (0f, 0f, 0f)
        };
 
    private static float Lerp(float from, float to, float progress)
        => from + ((to - from) * Math.Clamp(progress, 0f, 1f));
 
    private static Color LerpColor(Color from, Color to, float progress)
    {
        progress = Math.Clamp(progress, 0f, 1f);
 
        return Color.FromArgb(
            LerpChannel(from.A, to.A, progress),
            LerpChannel(from.R, to.R, progress),
            LerpChannel(from.G, to.G, progress),
            LerpChannel(from.B, to.B, progress));
 
        static int LerpChannel(int from, int to, float progress)
            => from + (int)((to - from) * progress);
    }
 
    private static float EaseOut(float progress)
        => 1 - ((1 - progress) * (1 - progress));
 
    private static GraphicsPath CreateBoxPath(Rectangle bounds, FlatStyle flatStyle)
    {
        GraphicsPath path = new();
        if (flatStyle == FlatStyle.Flat)
        {
            path.AddRectangle(bounds);
            return path;
        }
 
        double radiusFactor = flatStyle == FlatStyle.Popup ? 0.3 : 0.2;
        int radius = Math.Max(1, (int)(Math.Min(bounds.Width, bounds.Height) * radiusFactor));
        path.AddRoundedRectangle(bounds, new Size(radius, radius));
        return path;
    }
 
    private static void DrawCheckmark(Graphics graphics, Rectangle bounds, Color color, float alpha)
    {
        using var pen = new Pen(
            Color.FromArgb((int)(alpha * 255), color),
            Math.Max(1.5f, bounds.Width * 0.12f))
        {
            StartCap = LineCap.Round,
            EndCap = LineCap.Round,
            LineJoin = LineJoin.Round
        };
 
        PointF first = new(bounds.Left + (bounds.Width * 0.20f), bounds.Top + (bounds.Height * 0.52f));
        PointF second = new(bounds.Left + (bounds.Width * 0.42f), bounds.Top + (bounds.Height * 0.74f));
        PointF third = new(bounds.Left + (bounds.Width * 0.82f), bounds.Top + (bounds.Height * 0.28f));
        graphics.DrawLines(pen, [first, second, third]);
    }
 
    private static void DrawDash(Graphics graphics, Rectangle bounds, Color color, float alpha)
    {
        using var pen = new Pen(
            Color.FromArgb((int)(alpha * 255), color),
            Math.Max(1.5f, bounds.Height * 0.14f))
        {
            StartCap = LineCap.Round,
            EndCap = LineCap.Round
        };
 
        float y = bounds.Top + (bounds.Height * 0.5f);
        graphics.DrawLine(
            pen,
            bounds.Left + (bounds.Width * 0.2f),
            y,
            bounds.Right - (bounds.Width * 0.2f),
            y);
    }
}