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;
 
    // WinUI stroke alphas use black in light mode or white in dark mode; values follow
    // Common_themeresources_any.xaml except Strong, raised to meet WCAG 1.4.11 (#14906).
    private const int StrokeDefaultAlphaLight = 0x0F;    // ControlStrokeColorDefault
    private const int StrokeDefaultAlphaDark = 0x03;     // Dark rest side; WinUI is 0x12 (#14919).
    private const int StrokeSecondaryAlphaLight = 0x29;  // ControlStrokeColorSecondary
    private const int StrokeSecondaryAlphaDark = 0x18;
    private const int StrokeStrongAlphaLight = 0xB6;     // Resting bottom; ~3.1:1 WCAG 1.4.11 floor, from 0xD1 (#14906, #14997).
    private const int StrokeStrongAlphaDark = 0x8B;
 
    // Hover is one step stronger than Secondary (#14906).
    private const int StrokeHoverAlphaLight = 0x40;
    private const int StrokeHoverAlphaDark = 0x28;
 
    // ReadOnly surface tint from Leaf's #14906 table.
    private const int SurfaceReadOnlyAlphaLight = 0x0A;
    private const int SurfaceReadOnlyAlphaDark = 0x0A;
 
    // 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);
    private static readonly Color s_darkModeDisabledBorderStrong = Color.FromArgb(0x6A, 0x6A, 0x6A);
    private static readonly Color s_lightModeDisabledBorderStrong = Color.FromArgb(0xB0, 0xB0, 0xB0);
 
    /// <summary>
    ///  Gets the stable border color for modern editable text controls when enabled.
    /// </summary>
    internal static Color TextControlBorderColor
        => Application.IsDarkModeEnabled
            ? SystemColors.WindowText
            : SystemColors.WindowFrame;
 
    /// <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>Returns the strong disabled bottom-edge color.</summary>
    internal static Color GetDisabledStrongBorderColor()
        => SystemInformation.HighContrast
            ? SystemColors.GrayText
            : Application.IsDarkModeEnabled
                ? s_darkModeDisabledBorderStrong
                : s_lightModeDisabledBorderStrong;
 
    /// <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;
    }
 
    /// <summary>Returns the lightest field stroke over <paramref name="background"/>.</summary>
    internal static Color GetFieldStrokeDefault(Color background, bool darkMode)
        => CompositeStrokeOverlay(background, darkMode ? StrokeDefaultAlphaDark : StrokeDefaultAlphaLight, darkMode);
 
    /// <summary>Returns the secondary field stroke.</summary>
    internal static Color GetFieldStrokeSecondary(Color background, bool darkMode)
        => CompositeStrokeOverlay(background, darkMode ? StrokeSecondaryAlphaDark : StrokeSecondaryAlphaLight, darkMode);
 
    /// <summary>Returns the hover stroke, stronger than secondary.</summary>
    internal static Color GetFieldStrokeHover(Color background, bool darkMode)
        => CompositeStrokeOverlay(background, darkMode ? StrokeHoverAlphaDark : StrokeHoverAlphaLight, darkMode);
 
    /// <summary>Returns the ReadOnly surface tint.</summary>
    internal static Color GetFieldReadOnlySurface(Color background, bool darkMode)
        => CompositeStrokeOverlay(background, darkMode ? SurfaceReadOnlyAlphaDark : SurfaceReadOnlyAlphaLight, darkMode);
 
    /// <summary>Returns the strong resting bottom-edge stroke.</summary>
    internal static Color GetFieldStrokeStrong(Color background, bool darkMode)
        => CompositeStrokeOverlay(background, darkMode ? StrokeStrongAlphaDark : StrokeStrongAlphaLight, darkMode);
 
    // Composites a 0-255 black/white overlay onto an opaque background in linear light.
    private static Color CompositeStrokeOverlay(Color background, int overlayAlpha, bool darkMode)
    {
        background = ResolveOpaqueColor(background);
        float alpha = Math.Clamp(overlayAlpha / 255f, 0f, 1f);
        float pole = darkMode ? 1f : 0f;
 
        return Color.FromArgb(
            byte.MaxValue,
            CompositeChannel(background.R),
            CompositeChannel(background.G),
            CompositeChannel(background.B));
 
        byte CompositeChannel(byte channel)
        {
            float mixed = (pole * alpha) + (SrgbToLinear(channel) * (1f - alpha));
            return LinearToSrgb(mixed);
        }
    }
 
    // SrgbToLinear and its inverse, LinearToSrgb, implement the standard sRGB transfer
    // function from IEC 61966-2-1, converting gamma-encoded channel values to and from
    // linear light. It uses a small linear segment near black and an approximately 2.4
    // gamma power segment above it; 0.04045, 12.92, 0.055, 1.055, 2.4, and 0.0031308
    // are standard sRGB constants, not tuned values. Physically correct alpha compositing
    // is done in linear light, so overlay colors are decoded, blended, then re-encoded.
    private static float SrgbToLinear(byte channel)
    {
        float value = channel / 255f;
        return value <= 0.04045f
            ? value / 12.92f
            : MathF.Pow((value + 0.055f) / 1.055f, 2.4f);
    }
 
    private static byte LinearToSrgb(float linear)
    {
        linear = Math.Clamp(linear, 0f, 1f);
        float value = linear <= 0.0031308f
            ? linear * 12.92f
            : (1.055f * MathF.Pow(linear, 1f / 2.4f)) - 0.055f;
        return (byte)MathF.Round(value * 255f);
    }
 
    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);
}