File: System\Windows\Forms\Rendering\ModernControlColorMath.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.Windows.Forms.Rendering.Button;
 
namespace System.Windows.Forms;
 
/// <summary>
///  Provides shared color calculations for modern control renderers.
/// </summary>
internal static class ModernControlColorMath
{
    internal const float MinimumDisabledTextContrastRatio = 3f;
 
    private const float DisabledMuteAmount = 0.45f;
    private const int ContrastSearchIterations = 10;
 
    // Shared disabled-state palette for modern renderers. Modern controls do not honor user-set
    // BackColor/ForeColor while disabled, so these fixed surfaces replace them. This is the single
    // source of truth: the modern Button renderers and the modern ComboBox adapter all read from
    // here, so a disabled Button and a disabled ComboBox cannot drift apart.
    private static readonly Color s_darkModeDisabledSurface = Color.FromArgb(0x25, 0x25, 0x25);
    private static readonly Color s_lightModeDisabledSurface = Color.FromArgb(0xFA, 0xFA, 0xFA);
    private static readonly Color s_darkModeDisabledBorder = Color.FromArgb(0x55, 0x55, 0x55);
    private static readonly Color s_lightModeDisabledBorder = Color.FromArgb(0xD0, 0xD0, 0xD0);
    private static readonly Color s_darkModeDisabledForeground = Color.FromArgb(0x88, 0x88, 0x88);
    private static readonly Color s_lightModeDisabledForeground = Color.FromArgb(0xA0, 0xA0, 0xA0);
 
    /// <summary>
    ///  Gets the surface color for a disabled modern control, honoring the current color mode
    ///  and high contrast settings.
    /// </summary>
    internal static Color GetDisabledSurfaceColor()
        => SystemInformation.HighContrast
            ? SystemColors.Control
            : Application.IsDarkModeEnabled
                ? s_darkModeDisabledSurface
                : s_lightModeDisabledSurface;
 
    /// <summary>
    ///  Gets the border color for a disabled modern control, honoring the current color mode
    ///  and high contrast settings.
    /// </summary>
    internal static Color GetDisabledBorderColor()
        => SystemInformation.HighContrast
            ? SystemColors.GrayText
            : Application.IsDarkModeEnabled
                ? s_darkModeDisabledBorder
                : s_lightModeDisabledBorder;
 
    /// <summary>
    ///  Gets the contrast-adjusted foreground color for content drawn on
    ///  <see cref="GetDisabledSurfaceColor"/>.
    /// </summary>
    internal static Color GetDisabledForeColor(Color backColor)
        => GetDisabledTextColor(
            Application.IsDarkModeEnabled
                ? s_darkModeDisabledForeground
                : s_lightModeDisabledForeground,
            backColor);
 
    internal static Color GetDisabledTextColor(
        Color preferredForeColor,
        Color backColor)
        => GetDisabledTextColor(
            preferredForeColor,
            backColor,
            backColor);
 
    internal static Color GetDisabledTextColor(
        Color preferredForeColor,
        Color firstBackColor,
        Color secondBackColor)
    {
        if (SystemInformation.HighContrast)
        {
            return SystemColors.GrayText;
        }
 
        firstBackColor = ResolveOpaqueColor(firstBackColor);
        secondBackColor = ResolveOpaqueColor(secondBackColor);
 
        Color muteColor = PopupButtonColorMath.Blend(
            firstBackColor,
            secondBackColor,
            0.5f);
        Color mutedForeColor = PopupButtonColorMath.Blend(
            preferredForeColor,
            muteColor,
            DisabledMuteAmount);
 
        if (HasMinimumContrast(
            mutedForeColor,
            firstBackColor,
            secondBackColor))
        {
            return mutedForeColor;
        }
 
        Color contrastColor = PopupButtonColorMath.GetReadableForeColor(
            firstBackColor,
            secondBackColor);
        Color result = contrastColor;
        float low = 0f;
        float high = 1f;
 
        for (int i = 0; i < ContrastSearchIterations; i++)
        {
            float amount = (low + high) / 2f;
            Color candidate = PopupButtonColorMath.Blend(
                mutedForeColor,
                contrastColor,
                amount);
 
            if (HasMinimumContrast(
                candidate,
                firstBackColor,
                secondBackColor))
            {
                result = candidate;
                high = amount;
            }
            else
            {
                low = amount;
            }
        }
 
        return result;
    }
 
    private static bool HasMinimumContrast(
        Color foreColor,
        Color firstBackColor,
        Color secondBackColor)
        => PopupButtonColorMath.GetContrastRatio(
            foreColor,
            firstBackColor) >= MinimumDisabledTextContrastRatio
            && PopupButtonColorMath.GetContrastRatio(
                foreColor,
                secondBackColor) >= MinimumDisabledTextContrastRatio;
 
    private static Color ResolveOpaqueColor(Color color)
        => color.A == byte.MaxValue
            ? color
            : PopupButtonColorMath.Composite(color, SystemColors.Control);
}