File: System\Windows\Forms\Rendering\RadioButton\AnimatedRadioGlyphRenderer.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;
using System.Windows.Forms.Rendering.Button;
 
namespace System.Windows.Forms.Rendering.RadioButton;
 
/// <summary>
///  Animates and draws the modern normal-appearance RadioButton glyph.
/// </summary>
internal sealed class AnimatedRadioGlyphRenderer : AnimatedControlRenderer
{
    private const int AnimationDuration = 200;
    private const float HoverGrowth = 1.1f;
 
    private float _dotScaleCurrent;
    private float _dotScaleStart;
    private float _dotScaleTarget;
    private float _focusCurrent;
    private float _focusStart;
    private float _focusTarget;
    private float _hoverCurrent;
    private float _hoverStart;
    private float _hoverTarget;
    private bool _interactionInitialized;
    private bool _stateInitialized;
 
    public AnimatedRadioGlyphRenderer(Control control) : base(control)
    {
    }
 
    internal void NotifyCheckedChanged(bool newChecked)
    {
        float dotScaleTarget = newChecked ? 1f : 0f;
        if (!_stateInitialized)
        {
            _stateInitialized = true;
            _dotScaleCurrent = _dotScaleStart = _dotScaleTarget = dotScaleTarget;
            return;
        }
 
        if (_dotScaleTarget == dotScaleTarget)
        {
            return;
        }
 
        _dotScaleTarget = dotScaleTarget;
        RestartAnimation();
    }
 
    internal void SetInteractionState(bool hovered, bool focused)
    {
        float hoverTarget = hovered ? 1f : 0f;
        float focusTarget = focused ? 1f : 0f;
        if (!_interactionInitialized)
        {
            _interactionInitialized = true;
            _hoverCurrent = _hoverStart = _hoverTarget = hoverTarget;
            _focusCurrent = _focusStart = _focusTarget = focusTarget;
            return;
        }
 
        if (_hoverTarget == hoverTarget && _focusTarget == focusTarget)
        {
            return;
        }
 
        _hoverTarget = hoverTarget;
        _focusTarget = focusTarget;
        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 borderColor = highContrast
            ? SystemColors.WindowText
            : customBorderColor
                ?? (isDark
                    ? Color.FromArgb(0x9B, 0x9B, 0x9B)
                    : SystemColors.ControlDark);
 
        Color backColor = 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);
            borderColor = highContrast
                ? SystemColors.GrayText
                : isDark
                    ? Color.FromArgb(0x45, 0x45, 0x45)
                    : Color.FromArgb(0xD0, 0xD0, 0xD0);
        }
 
        float focus = enabled && !highContrast ? _focusCurrent : 0f;
        onColor = ApplyInteractionShade(onColor, focus);
        borderColor = ApplyInteractionShade(borderColor, focus);
        backColor = ApplyInteractionShade(backColor, focus);
 
        float normalOuterScale = 1f / HoverGrowth;
        float outerScale = Lerp(normalOuterScale, 1f, enabled ? _hoverCurrent : 0f);
        RectangleF outerBounds = ScaleFromCenter(bounds, outerScale);
        RectangleF normalBounds = ScaleFromCenter(bounds, normalOuterScale);
 
        GraphicsState? saved = graphics.Save();
        try
        {
            graphics.SmoothingMode = SmoothingMode.AntiAlias;
 
            using (var brush = backColor.GetCachedSolidBrushScope())
            {
                graphics.FillEllipse(brush, outerBounds);
            }
 
            int borderThickness = Math.Max(
                1,
                Control.LogicalToDeviceUnits(flatStyle == FlatStyle.Popup ? 2 : 1));
 
            using (var pen = new Pen(borderColor, borderThickness))
            {
                graphics.DrawEllipse(pen, outerBounds);
            }
 
            if (_dotScaleCurrent > 0.001f)
            {
                float dotDiameter = normalBounds.Width * 0.5f * _dotScaleCurrent;
                RectangleF dotRectangle = new(
                    normalBounds.X + ((normalBounds.Width - dotDiameter) / 2f),
                    normalBounds.Y + ((normalBounds.Height - dotDiameter) / 2f),
                    dotDiameter,
                    dotDiameter);
 
                Color dotOutlineColor = highContrast
                    ? SystemColors.HighlightText
                    : PopupButtonColorMath.GetReadableForeColor(onColor, backColor);
                int outlineThickness = Math.Max(1, Control.LogicalToDeviceUnits(1));
                using var outlineBrush = dotOutlineColor.GetCachedSolidBrushScope();
                graphics.FillEllipse(outlineBrush, dotRectangle);
 
                RectangleF accentRectangle = RectangleF.Inflate(
                    dotRectangle,
                    -outlineThickness,
                    -outlineThickness);
                if (accentRectangle.Width > 0 && accentRectangle.Height > 0)
                {
                    using var dotBrush = onColor.GetCachedSolidBrushScope();
                    graphics.FillEllipse(dotBrush, accentRectangle);
                }
            }
        }
        finally
        {
            if (saved is not null)
            {
                graphics.Restore(saved);
            }
        }
    }
 
    public override void AnimationProc(float animationProgress)
    {
        base.AnimationProc(animationProgress);
        float easedProgress = EaseOut(animationProgress);
        _dotScaleCurrent = Lerp(_dotScaleStart, _dotScaleTarget, easedProgress);
        _focusCurrent = Lerp(_focusStart, _focusTarget, easedProgress);
        _hoverCurrent = Lerp(_hoverStart, _hoverTarget, easedProgress);
        Invalidate();
    }
 
    public override void RenderControl(Graphics graphics)
    {
    }
 
    protected override (int animationDuration, AnimationCycle animationCycle) OnAnimationStarted()
    {
        _dotScaleStart = _dotScaleCurrent;
        _focusStart = _focusCurrent;
        _hoverStart = _hoverCurrent;
        AnimationProgress = 0;
        return (AnimationDuration, AnimationCycle.Once);
    }
 
    protected override void OnAnimationStopped()
    {
    }
 
    protected override void OnAnimationEnded()
    {
        StopAnimation();
        _dotScaleCurrent = _dotScaleTarget;
        _focusCurrent = _focusTarget;
        _hoverCurrent = _hoverTarget;
        AnimationProgress = 1;
        Invalidate();
    }
 
    private static float EaseOut(float progress)
        => 1 - ((1 - progress) * (1 - progress));
 
    private static float Lerp(float from, float to, float progress)
        => from + ((to - from) * Math.Clamp(progress, 0f, 1f));
 
    private static RectangleF ScaleFromCenter(Rectangle bounds, float scale)
    {
        float width = bounds.Width * scale;
        float height = bounds.Height * scale;
 
        return new(
            bounds.X + ((bounds.Width - width) / 2f),
            bounds.Y + ((bounds.Height - height) / 2f),
            width,
            height);
    }
}