File: System\Windows\Forms\Controls\ToolStrips\ToolStripSystemDarkModeRenderer.cs
Web Access
Project: src\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;
 
namespace System.Windows.Forms;
 
/// <summary>
///  Provides dark mode rendering capabilities for ToolStrip controls in Windows Forms.
/// </summary>
/// <remarks>
///  <para>
///   This renderer is designed to be used with the ToolStripSystemRenderer to provide dark mode
///   styling while maintaining accessibility features. It inherits from ToolStripRenderer
///   and overrides necessary methods to provide dark-themed rendering.
///  </para>
/// </remarks>
internal class ToolStripSystemDarkModeRenderer : ToolStripRenderer
{
    /// <summary>
    ///  Initializes a new instance of the ToolStripSystemDarkModeRenderer class.
    /// </summary>
    public ToolStripSystemDarkModeRenderer()
    {
    }
 
    /// <summary>
    ///  Initializes a new instance of the ToolStripSystemDarkModeRenderer class with the specified default state.
    /// </summary>
    /// <param name="isSystemDefaultAlternative">
    ///  True if this should be seen as a variation of the default renderer
    ///  (so, no _custom_ renderer provided by the user); otherwise, false.
    /// </param>
    internal ToolStripSystemDarkModeRenderer(bool isSystemDefaultAlternative)
        : base(isSystemDefaultAlternative)
    {
    }
 
    /// <summary>
    ///  Gets dark-appropriate system colors based on the control type.
    /// </summary>
    /// <param name="color">The color to convert to a dark mode equivalent.</param>
    /// <returns>A color suitable for dark mode.</returns>
    private static Color GetDarkModeColor(Color color) =>
        color switch
        {
            Color c when c == SystemColors.Control => Color.FromArgb(45, 45, 45),
            Color c when c == SystemColors.ControlLight => Color.FromArgb(60, 60, 60),
            Color c when c == SystemColors.ControlDark => Color.FromArgb(30, 30, 30),
            Color c when c == SystemColors.ControlText => Color.FromArgb(240, 240, 240),
            Color c when c == SystemColors.ButtonFace => Color.FromArgb(45, 45, 45),
            Color c when c == SystemColors.Highlight => Color.FromArgb(0, 120, 215),
            Color c when c == SystemColors.HighlightText => Color.White,
            Color c when c == SystemColors.Window => Color.FromArgb(32, 32, 32),
            Color c when c == SystemColors.WindowText => Color.FromArgb(240, 240, 240),
            Color c when c == SystemColors.GrayText => Color.FromArgb(153, 153, 153),
            Color c when c == SystemColors.InactiveBorder => Color.FromArgb(70, 70, 70),
            Color c when c == SystemColors.ButtonHighlight => Color.FromArgb(80, 80, 80),
            Color c when c == SystemColors.ButtonShadow => Color.FromArgb(20, 20, 20),
            Color c when c == SystemColors.Menu => Color.FromArgb(45, 45, 45),
            Color c when c == SystemColors.MenuText => Color.FromArgb(240, 240, 240),
            _ when color.GetBrightness() > 0.5 => ControlPaint.Dark(color, 0.2f),
            _ => color
        };
 
    /// <summary>
    ///  Creates a dark mode compatible brush. Important:
    ///  Always do: `using var brush = GetDarkModeBrush(color)`,
    ///  since you're dealing with a cached brush => scope, really!
    ///  Creates a dark mode compatible brush. Always use 'using var brush = GetDarkModeBrush(color)'.
    /// </summary>
    /// <param name="color">The system color to convert.</param>
    /// <returns>A brush with the dark mode color.</returns>
    private static SolidBrushCache.Scope GetDarkModeBrush(Color color) =>
        GetDarkModeColor(color).GetCachedSolidBrushScope();
 
    /// <summary>
    ///  Creates a dark mode compatible pen. Always use 'using var pen = GetDarkModePen(color)'.
    /// </summary>
    /// <param name="color">The system color to convert.</param>
    /// <returns>A pen with the dark mode color.</returns>
    private static PenCache.Scope GetDarkModePen(Color color) =>
        GetDarkModeColor(color).GetCachedPenScope();
 
    /// <summary>
    ///  Returns whether the background should be painted.
    /// </summary>
    /// <param name="toolStrip">The ToolStrip to check.</param>
    /// <returns>true if the background should be painted; otherwise, false.</returns>
    private static bool ShouldPaintBackground(ToolStrip toolStrip)
        => toolStrip is null || toolStrip.BackgroundImage is null;
 
    /// <summary>
    ///  Fills the background with the specified color.
    /// </summary>
    /// <param name="g">The Graphics to draw on.</param>
    /// <param name="bounds">The bounds to fill.</param>
    /// <param name="backColor">The background color.</param>
    private static void FillBackground(Graphics g, Rectangle bounds, Color backColor)
    {
        if (bounds.Width <= 0 || bounds.Height <= 0)
        {
            return;
        }
 
        using var brush = GetDarkModeBrush(backColor);
 
        g.FillRectangle(brush, bounds);
    }
 
    /// <summary>
    ///  Raises the RenderToolStripBackground event.
    /// </summary>
    /// <param name="e">A ToolStripRenderEventArgs that contains the event data.</param>
    protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e)
    {
        ArgumentNullException.ThrowIfNull(e);
 
        ToolStrip toolStrip = e.ToolStrip;
        Rectangle bounds = e.AffectedBounds;
 
        if (!ShouldPaintBackground(toolStrip))
        {
            return;
        }
 
        Graphics g = e.Graphics;
 
        if (toolStrip is StatusStrip)
        {
            RenderStatusStripBackground(e);
 
            return;
        }
 
        if (toolStrip.IsDropDown)
        {
            // Dark mode dropdown background
            FillBackground(g, bounds, GetDarkModeColor(SystemColors.Menu));
        }
        else if (toolStrip is MenuStrip)
        {
            // Dark mode menu background
            FillBackground(g, bounds, GetDarkModeColor(SystemColors.Menu));
        }
        else
        {
            // Standard ToolStrip background
            FillBackground(g, bounds, GetDarkModeColor(e.BackColor));
        }
    }
 
    /// <summary>
    ///  Renders the StatusStrip background in dark mode.
    /// </summary>
    /// <param name="e">A ToolStripRenderEventArgs that contains the event data.</param>
    internal static void RenderStatusStripBackground(ToolStripRenderEventArgs e)
    {
        Rectangle bounds = e.AffectedBounds;
 
        // Dark mode StatusStrip background
        FillBackground(e.Graphics, bounds, GetDarkModeColor(SystemColors.Control));
    }
 
    /// <summary>
    ///  Raises the RenderToolStripBorder event.
    /// </summary>
    /// <param name="e">A ToolStripRenderEventArgs that contains the event data.</param>
    protected override void OnRenderToolStripBorder(ToolStripRenderEventArgs e)
    {
        ArgumentNullException.ThrowIfNull(e);
 
        ToolStrip toolStrip = e.ToolStrip;
 
        if (toolStrip is StatusStrip)
        {
            RenderStatusStripBorder(e);
            return;
        }
 
        Graphics g = e.Graphics;
        Rectangle bounds = e.ToolStrip.ClientRectangle;
 
        using var borderPen = GetDarkModePen(SystemColors.ControlDark);
 
        if (toolStrip is ToolStripDropDown toolStripDropDown)
        {
            if (toolStripDropDown.DropShadowEnabled)
            {
                bounds.Width -= 1;
                bounds.Height -= 1;
 
                g.DrawRectangle(borderPen, bounds);
            }
            else
            {
                g.DrawRectangle(borderPen, bounds);
            }
 
            return;
        }
 
        g.DrawLine(borderPen, 0, bounds.Bottom - 1, bounds.Width, bounds.Bottom - 1);
    }
 
    /// <summary>
    ///  Renders the StatusStrip border in dark mode.
    /// </summary>
    /// <param name="e">A ToolStripRenderEventArgs that contains the event data.</param>
    private static void RenderStatusStripBorder(ToolStripRenderEventArgs e)
    {
        Graphics g = e.Graphics;
        Rectangle bounds = e.ToolStrip.ClientRectangle;
 
        using var borderPen = GetDarkModePen(SystemColors.ControlDark);
 
        g.DrawLine(borderPen, 0, 0, bounds.Width, 0);
    }
 
    /// <summary>
    ///  Raises the RenderItemBackground event.
    /// </summary>
    /// <param name="e">A ToolStripItemRenderEventArgs that contains the event data.</param>
    protected override void OnRenderItemBackground(ToolStripItemRenderEventArgs e)
    {
        ArgumentNullException.ThrowIfNull(e);
 
        Rectangle bounds = new(Point.Empty, e.Item.Size);
 
        if (e.Item.IsOnDropDown)
        {
            bounds.X += 2;
            bounds.Width -= 3;
        }
 
        if (e.Item.Selected || e.Item.Pressed)
        {
            // Dark mode selection highlight
            using var highlightBrush = GetDarkModeBrush(SystemColors.Highlight);
            e.Graphics.FillRectangle(highlightBrush, bounds);
 
            return;
        }
 
        // Render background image if available
        if (e.Item.BackgroundImage is not null)
        {
            ControlPaint.DrawBackgroundImage(
                g: e.Graphics,
                backgroundImage: e.Item.BackgroundImage,
                backColor: GetDarkModeColor(e.Item.BackColor),
                backgroundImageLayout: e.Item.BackgroundImageLayout,
                bounds: e.Item.ContentRectangle,
                clipRect: bounds);
 
            return;
        }
 
        if (e.Item.BackColor != Color.Transparent && e.Item.BackColor != Color.Empty)
        {
            // Custom background color (apply dark mode transformation)
            FillBackground(e.Graphics, bounds, e.Item.BackColor);
        }
    }
 
    /// <summary>
    ///  Raises the RenderButtonBackground event.
    /// </summary>
    /// <param name="e">A ToolStripItemRenderEventArgs that contains the event data.</param>
    protected override void OnRenderButtonBackground(ToolStripItemRenderEventArgs e)
    {
        ArgumentNullException.ThrowIfNull(e);
 
        Rectangle bounds = new(Point.Empty, e.Item.Size);
        bool isPressed;
        bool isSelected;
 
        if (e.Item is ToolStripButton button)
        {
            isPressed = button.Pressed;
            isSelected = button.Selected || button.Checked;
        }
        else
        {
            isPressed = e.Item.Pressed;
            isSelected = e.Item.Selected;
        }
 
        if (isPressed || isSelected)
        {
            using var fillColor = isPressed
                ? GetDarkModeBrush(SystemColors.ControlDark)
                : GetDarkModeBrush(SystemColors.Highlight);
 
            e.Graphics.FillRectangle(fillColor, bounds);
        }
    }
 
    /// <summary>
    ///  Raises the RenderDropDownButtonBackground event.
    /// </summary>
    /// <param name="e">A ToolStripItemRenderEventArgs that contains the event data.</param>
    protected override void OnRenderDropDownButtonBackground(ToolStripItemRenderEventArgs e)
    {
        OnRenderButtonBackground(e);
    }
 
    /// <summary>
    ///  Raises the RenderSplitButtonBackground event.
    /// </summary>
    /// <param name="e">A ToolStripItemRenderEventArgs that contains the event data.</param>
    protected override void OnRenderSplitButtonBackground(ToolStripItemRenderEventArgs e)
    {
        ArgumentNullException.ThrowIfNull(e);
 
        if (e.Item is not ToolStripSplitButton splitButton)
        {
            return;
        }
 
        Rectangle bounds = new(Point.Empty, e.Item.Size);
 
        // Render the background based on state
        if (splitButton.Selected || splitButton.Pressed)
        {
            using var fillColor = splitButton.Pressed
                ? GetDarkModeBrush(SystemColors.ControlDark)
                : GetDarkModeBrush(SystemColors.Highlight);
 
            e.Graphics.FillRectangle(fillColor, bounds);
        }
 
        // Draw the split line
        Rectangle dropDownRect = splitButton.DropDownButtonBounds;
        using var linePen = GetDarkModePen(SystemColors.ControlDark);
 
        e.Graphics.DrawLine(
            pen: linePen,
            x1: dropDownRect.Left - 1,
            y1: dropDownRect.Top + 2,
            x2: dropDownRect.Left - 1,
            y2: dropDownRect.Bottom - 2);
 
        DrawArrow(new ToolStripArrowRenderEventArgs(
            g: e.Graphics,
            toolStripItem: e.Item,
            arrowRectangle: dropDownRect,
            arrowColor: SystemColors.ControlText,
            arrowDirection: ArrowDirection.Down));
    }
 
    /// <summary>
    ///  Raises the RenderSeparator event.
    /// </summary>
    /// <param name="e">A ToolStripSeparatorRenderEventArgs that contains the event data.</param>
    protected override void OnRenderSeparator(ToolStripSeparatorRenderEventArgs e)
    {
        ArgumentNullException.ThrowIfNull(e);
 
        Rectangle bounds = e.Item.ContentRectangle;
        bool isVertical = e.Vertical;
        Graphics g = e.Graphics;
 
        if (bounds.Width <= 0 || bounds.Height <= 0)
        {
            return;
        }
 
        bool rightToLeft = e.Item.RightToLeft == RightToLeft.Yes;
 
        if (isVertical)
        {
            int startX = bounds.Width / 2;
 
            if (rightToLeft)
            {
                using var leftPen = GetDarkModeColor(SystemColors.ControlDark).GetCachedPenScope();
                g.DrawLine(leftPen, startX, bounds.Top, startX, bounds.Bottom);
 
                startX++;
 
                using var rightPen = GetDarkModeColor(SystemColors.ButtonShadow).GetCachedPenScope();
                g.DrawLine(rightPen, startX, bounds.Top, startX, bounds.Bottom);
            }
            else
            {
                using var leftPen = GetDarkModeColor(SystemColors.ButtonShadow).GetCachedPenScope();
                g.DrawLine(leftPen, startX, bounds.Top, startX, bounds.Bottom);
 
                startX++;
 
                using var rightPen = GetDarkModeColor(SystemColors.ControlDark).GetCachedPenScope();
                g.DrawLine(rightPen, startX, bounds.Top, startX, bounds.Bottom);
            }
        }
        else
        {
            // Horizontal separator
            if (bounds.Width >= 4)
            {
                bounds.Inflate(-2, 0);
            }
 
            int startY = bounds.Height / 2;
 
            using var foreColorPen = GetDarkModeColor(SystemColors.ControlDark).GetCachedPenScope();
            g.DrawLine(foreColorPen, bounds.Left, startY, bounds.Right, startY);
 
            startY++;
 
            using var darkModePen = GetDarkModeColor(SystemColors.ButtonShadow).GetCachedPenScope();
            g.DrawLine(darkModePen, bounds.Left, startY, bounds.Right, startY);
        }
    }
 
    /// <summary>
    ///  Raises the RenderOverflowButtonBackground event.
    /// </summary>
    /// <param name="e">A ToolStripItemRenderEventArgs that contains the event data.</param>
    protected override void OnRenderOverflowButtonBackground(ToolStripItemRenderEventArgs e)
    {
        ArgumentNullException.ThrowIfNull(e);
 
        if (e.Item is not ToolStripOverflowButton item)
        {
            return;
        }
 
        Rectangle bounds = new(Point.Empty, e.Item.Size);
 
        // Render the background based on state
        if (item.Selected || item.Pressed)
        {
            using var fillBrush = item.Pressed
                ? GetDarkModeColor(SystemColors.ControlDark).GetCachedSolidBrushScope()
                : GetDarkModeColor(SystemColors.Highlight).GetCachedSolidBrushScope();
 
            e.Graphics.FillRectangle(fillBrush, bounds);
        }
 
        Rectangle arrowRect = item.ContentRectangle;
 
        Point middle = new(
            x: arrowRect.Left + arrowRect.Width / 2,
            y: arrowRect.Top + arrowRect.Height / 2);
 
        // Default to down arrow for overflow buttons
        ArrowDirection arrowDirection = ArrowDirection.Down;
 
        // Determine actual direction based on dropdown direction
        ToolStripDropDownDirection direction = item.DropDownDirection;
 
        if (direction is ToolStripDropDownDirection.AboveLeft or ToolStripDropDownDirection.AboveRight)
        {
            arrowDirection = ArrowDirection.Up;
        }
        else if (direction == ToolStripDropDownDirection.Left)
        {
            arrowDirection = ArrowDirection.Left;
        }
        else if (direction == ToolStripDropDownDirection.Right)
        {
            arrowDirection = ArrowDirection.Right;
        }
 
        // else default to ArrowDirection.Down
 
        // Set arrow color based on state
        using var arrowBrush = GetDarkModeColor(
            item.Pressed || item.Selected
                ? SystemColors.HighlightText
                : SystemColors.ControlText)
            .GetCachedSolidBrushScope();
 
        // Define arrow polygon based on direction
        Point[] arrow = arrowDirection switch
        {
            ArrowDirection.Up =>
                [
                    new Point(middle.X - 2, middle.Y + 1),
                    new Point(middle.X + 3, middle.Y + 1),
                    new Point(middle.X, middle.Y - 2)
                ],
            ArrowDirection.Left =>
                [
                    new Point(middle.X + 2, middle.Y - 4),
                    new Point(middle.X + 2, middle.Y + 4),
                    new Point(middle.X - 2, middle.Y)
                ],
            ArrowDirection.Right =>
                [
                    new Point(middle.X - 2, middle.Y - 4),
                    new Point(middle.X - 2, middle.Y + 4),
                    new Point(middle.X + 2, middle.Y)
                ],
            _ =>
                [
                    new Point(middle.X - 2, middle.Y - 1),
                    new Point(middle.X + 3, middle.Y - 1),
                    new Point(middle.X, middle.Y + 2)
                ],
        };
 
        // Draw the arrow
        e.Graphics.FillPolygon(arrowBrush, arrow);
    }
 
    /// <summary>
    ///  Raises the RenderGrip event.
    /// </summary>
    /// <param name="e">A ToolStripGripRenderEventArgs that contains the event data.</param>
    protected override void OnRenderGrip(ToolStripGripRenderEventArgs e)
    {
        ArgumentNullException.ThrowIfNull(e);
 
        if (e.GripBounds.Width <= 0 || e.GripBounds.Height <= 0)
        {
            return;
        }
 
        using var darkColorBrush = GetDarkModeColor(SystemColors.ControlDark).GetCachedSolidBrushScope();
        using var lightColorBrush = GetDarkModeColor(SystemColors.ControlLight).GetCachedSolidBrushScope();
 
        ToolStrip toolStrip = e.ToolStrip;
        Graphics g = e.Graphics;
        Rectangle bounds = e.GripBounds;
 
        // Draw grip dots
        if (toolStrip.Orientation == Orientation.Horizontal)
        {
            // Draw horizontal grip
            int y = bounds.Top + 2;
 
            while (y < bounds.Bottom - 3)
            {
                g.FillRectangle(darkColorBrush, bounds.Left + 2, y, 1, 1);
                g.FillRectangle(lightColorBrush, bounds.Left + 3, y + 1, 1, 1);
 
                y += 3;
            }
        }
        else
        {
            // Draw vertical grip
            int x = bounds.Left + 2;
 
            while (x < bounds.Right - 3)
            {
                g.FillRectangle(darkColorBrush, x, bounds.Top + 2, 1, 1);
                g.FillRectangle(lightColorBrush, x + 1, bounds.Top + 3, 1, 1);
 
                x += 3;
            }
        }
    }
 
    /// <summary>
    ///  Raises the RenderArrow event in the derived class with dark mode support.
    /// </summary>
    /// <param name="e">A ToolStripArrowRenderEventArgs that contains the event data.</param>
    protected override void OnRenderArrow(ToolStripArrowRenderEventArgs e)
    {
        ArgumentNullException.ThrowIfNull(e);
        Debug.Assert(e.Item is not null, "The ToolStripItem should not be null on rendering the Arrow.");
 
        Color arrowColor = (e.Item.Selected || e.Item.Pressed)
            ? GetDarkModeColor(SystemColors.HighlightText)
            : GetDarkModeColor(e.ArrowColor);
 
        RenderArrowCore(e, arrowColor);
    }
 
    /// <summary>
    ///  Raises the RenderImageMargin event.
    /// </summary>
    /// <param name="e">A ToolStripRenderEventArgs that contains the event data.</param>
    protected override void OnRenderImageMargin(ToolStripRenderEventArgs e)
    {
        ArgumentNullException.ThrowIfNull(e);
 
        // Fill the image margin with a slightly different color than the background
        using var marginColorBrush = GetDarkModeBrush(SystemColors.ControlLight);
 
        e.Graphics.FillRectangle(marginColorBrush, e.AffectedBounds);
    }
 
    /// <summary>
    ///  Raises the RenderItemText event.
    /// </summary>
    /// <param name="e">A ToolStripItemTextRenderEventArgs that contains the event data.</param>
    protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e)
    {
        ArgumentNullException.ThrowIfNull(e);
 
        Color textColor = (e.Item.Selected || e.Item.Pressed)
            ? GetDarkModeColor(SystemColors.HighlightText)
            : !e.Item.Enabled
                ? GetDarkModeColor(SystemColors.GrayText)
                : GetDarkModeColor(e.TextColor);
 
        TextRenderer.DrawText(
            dc: e.Graphics,
            text: e.Text,
            font: e.TextFont,
            bounds: e.TextRectangle,
            foreColor: textColor,
            flags: e.TextFormat);
    }
 
    /// <summary>
    ///  Raises the RenderItemImage event.
    /// </summary>
    /// <param name="e">A ToolStripItemImageRenderEventArgs that contains the event data.</param>
    protected override void OnRenderItemImage(ToolStripItemImageRenderEventArgs e)
    {
        ArgumentNullException.ThrowIfNull(e);
 
        if (e.Image is null)
        {
            return;
        }
 
        // DarkMode adjustments for the image are done by
        // the base class implementation already.
        Image image = e.Item.Enabled
            ? e.Image
            : CreateDisabledImage(e.Image);
 
        e.Graphics.DrawImage(image, e.ImageRectangle);
    }
 
    /// <summary>
    ///  Raises the RenderLabelBackground event.
    /// </summary>
    /// <param name="e">A ToolStripItemRenderEventArgs that contains the event data.</param>
    protected override void OnRenderLabelBackground(ToolStripItemRenderEventArgs e)
    {
        // Use default item background rendering
        OnRenderItemBackground(e);
    }
 
    /// <summary>
    ///  Raises the RenderToolStripStatusLabelBackground event.
    /// </summary>
    /// <param name="e">A ToolStripItemRenderEventArgs that contains the event data.</param>
    protected override void OnRenderStatusStripSizingGrip(ToolStripRenderEventArgs e)
    {
        ArgumentNullException.ThrowIfNull(e);
 
        using var highLightBrush = GetDarkModeBrush(SystemColors.GrayText);
        using var shadowBrush = GetDarkModeBrush(SystemColors.ButtonShadow);
 
        OnRenderStatusStripSizingGrip(
            e: e,
            highLightBrush: highLightBrush,
            shadowBrush: shadowBrush);
    }
}