|
// 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.VisualStyles;
using static System.Windows.Forms.DarkModeButtonColors;
namespace System.Windows.Forms;
/// <summary>
/// Provides methods for rendering a button with System FlatStyle in dark mode.
/// </summary>
internal class SystemButtonDarkModeRenderer : ButtonDarkModeRendererBase
{
// UI constants
private const int CornerRadius = 8;
private const int FocusIndicatorCornerRadius = 6;
private const int DefaultButtonBorderThickness = 0;
private const int NonDefaultButtonBorderThickness = 0;
private const int FocusedButtonBorderThickness = 3;
private const int DarkBorderGapThickness = 2;
private const int SystemStylePadding = FocusedButtonBorderThickness + DarkBorderGapThickness;
private const int DefaultBackgroundColorOffset = 20;
private protected override Padding PaddingCore { get; } = new Padding(SystemStylePadding);
/// <summary>
/// Draws button background with system styling (larger rounded corners).
/// </summary>
public override Rectangle DrawButtonBackground(Graphics graphics, Rectangle bounds, PushButtonState state, bool isDefault)
{
// Shrink for DarkBorderGap and FocusBorderThickness
Rectangle fillBounds = Rectangle.Inflate(bounds, -SystemStylePadding, -SystemStylePadding);
using GraphicsPath fillPath = CreateRoundedRectanglePath(fillBounds, CornerRadius - DarkBorderGapThickness);
// Get appropriate background color based on state
Color backColor = GetBackgroundColor(state, isDefault);
// Fill the background using cached brush
using var brush = backColor.GetCachedSolidBrushScope();
graphics.FillPath(brush, fillPath);
// Return content bounds (area inside the button for text/image)
return fillBounds;
}
/// <summary>
/// Draws a focus indicator using a white thicker border.
/// </summary>
public override void DrawFocusIndicator(Graphics graphics, Rectangle contentBounds, bool isDefault)
{
// We need the bottom and the right border one pixel inside the button
Rectangle focusRect = new(
x: contentBounds.X,
y: contentBounds.Y,
width: contentBounds.Width - 1,
height: contentBounds.Height - 1);
// Create path for the focus outline
using GraphicsPath focusPath = CreateRoundedRectanglePath(focusRect, FocusIndicatorCornerRadius);
// System style uses a solid white border instead of dotted lines
using var focusPen = Color.White.GetCachedPenScope(FocusedButtonBorderThickness);
graphics.DrawPath(focusPen, focusPath);
}
/// <summary>
/// Gets the text color appropriate for the button state and type.
/// </summary>
public override Color GetTextColor(PushButtonState state, bool isDefault) =>
state == PushButtonState.Disabled
? DefaultColors.DisabledTextColor
: isDefault
? DefaultColors.StandardBackColor
: DefaultColors.AcceptButtonTextColor;
/// <summary>
/// Gets the background color appropriate for the button state and type.
/// </summary>
private static Color GetBackgroundColor(PushButtonState state, bool isDefault) =>
// For default button in System style, use a darker version of the background color
isDefault
? state switch
{
PushButtonState.Normal => Color.FromArgb(
DefaultColors.StandardBackColor.R - DefaultBackgroundColorOffset,
DefaultColors.StandardBackColor.G - DefaultBackgroundColorOffset,
DefaultColors.StandardBackColor.B - DefaultBackgroundColorOffset),
PushButtonState.Hot => Color.FromArgb(
DefaultColors.HoverBackColor.R - DefaultBackgroundColorOffset,
DefaultColors.HoverBackColor.G - DefaultBackgroundColorOffset,
DefaultColors.HoverBackColor.B - DefaultBackgroundColorOffset),
PushButtonState.Pressed => Color.FromArgb(
DefaultColors.PressedBackColor.R - DefaultBackgroundColorOffset,
DefaultColors.PressedBackColor.G - DefaultBackgroundColorOffset,
DefaultColors.PressedBackColor.B - DefaultBackgroundColorOffset),
PushButtonState.Disabled => DefaultColors.DisabledBackColor,
_ => DefaultColors.StandardBackColor
}
: state switch
{
PushButtonState.Normal => DefaultColors.StandardBackColor,
PushButtonState.Hot => DefaultColors.HoverBackColor,
PushButtonState.Pressed => DefaultColors.PressedBackColor,
PushButtonState.Disabled => DefaultColors.DisabledBackColor,
_ => DefaultColors.StandardBackColor
};
/// <summary>
/// Draws the button border based on the current state, using anti-aliasing and an additional inner border.
/// </summary>
public static void DrawButtonBorder(
Graphics graphics,
Rectangle bounds,
PushButtonState state,
bool isDefault,
bool isFocused)
{
// Skip border drawing for disabled state
if (state == PushButtonState.Disabled)
{
return;
}
// Don't draw regular border if focus border is already drawn
if (isFocused)
{
return;
}
// Outer border path
Rectangle borderRect = Rectangle.Inflate(bounds, -SystemStylePadding, -SystemStylePadding);
using GraphicsPath borderPath = CreateRoundedRectanglePath(borderRect, CornerRadius);
// We need to implement a subtle 3d effect around the already
// painted filling. We do this by drawing a border with a 1px pen,
// which is - with brighter colors - top and right a bit darker than
// the fill color, and bottom and left yet another bit darker.
//
// For darker fill colors, we use a slightly lighter color for the top and right sides,
// and a yet bit lighter color for the bottom and left sides.
// We never change the color for the borders, we just adjust the brightness.
// Get base color for the border based on button state and type
Color backColor = GetBackgroundColor(state, isDefault);
// For normal colors, make borders darker
// For dark colors, make borders lighter
bool isDarkColor = backColor.GetBrightness() < 0.5;
// Top-left border (slightly lighter/darker)
Color topLeftColor = isDarkColor
? ControlPaint.Light(backColor, 0.2f)
: ControlPaint.Dark(backColor, 0.1f);
// Bottom-right border (more pronounced light/dark)
Color bottomRightColor = isDarkColor
? ControlPaint.Light(backColor, 0.4f)
: ControlPaint.Dark(backColor, 0.2f);
// Determine border thickness
int borderThickness = isDefault ? DefaultButtonBorderThickness : NonDefaultButtonBorderThickness;
// Save graphics state to restore anti-aliasing settings later
GraphicsState? graphicState = null;
try
{
graphicState = graphics.Save();
graphics.SmoothingMode = SmoothingMode.AntiAlias;
// Draw top-left border segment
using var topLeftPen = topLeftColor.GetCachedPenScope(borderThickness);
graphics.DrawPath(topLeftPen, GetTopLeftSegmentPath(borderRect, CornerRadius));
// Draw bottom-right border segment
using var bottomRightPen = bottomRightColor.GetCachedPenScope(borderThickness);
graphics.DrawPath(bottomRightPen, GetBottomRightSegmentPath(borderRect, CornerRadius));
}
finally
{
// Restore graphics state
if (graphicState is not null)
{
graphics.Restore(graphicState);
}
}
}
/// <summary>
/// Creates a path for the top and left segments of a rounded rectangle.
/// </summary>
private static GraphicsPath GetTopLeftSegmentPath(Rectangle bounds, int radius)
{
GraphicsPath path = new();
int diameter = radius * 2;
// Top left corner arc
Rectangle arcRect = new(bounds.Location, new Size(diameter, diameter));
path.AddArc(arcRect, 180, 90);
// Top line
path.AddLine(bounds.Left + radius, bounds.Top, bounds.Right - radius, bounds.Top);
// Top right corner arc (just the top portion)
arcRect.X = bounds.Right - diameter;
path.AddArc(arcRect, 270, 45);
// Path back to middle of right side
path.AddLine(
bounds.Right - (int)(radius * Math.Sin(Math.PI / 4)),
bounds.Top + (int)(radius * (1 - Math.Cos(Math.PI / 4))),
bounds.Right,
bounds.Top + bounds.Height / 2);
// Path to middle of bottom
path.AddLine(bounds.Right, bounds.Top + bounds.Height / 2, bounds.Left + bounds.Width / 2, bounds.Bottom);
// Path to bottom left corner
path.AddLine(bounds.Left + bounds.Width / 2, bounds.Bottom, bounds.Left, bounds.Bottom - bounds.Height / 2);
// Path back to start
path.AddLine(bounds.Left, bounds.Bottom - bounds.Height / 2, bounds.Left, bounds.Top + radius);
return path;
}
/// <summary>
/// Creates a path for the bottom and right segments of a rounded rectangle.
/// </summary>
private static GraphicsPath GetBottomRightSegmentPath(Rectangle bounds, int radius)
{
GraphicsPath path = new();
int diameter = radius * 2;
// Start from middle of top edge
path.AddLine(bounds.Left + bounds.Width / 2, bounds.Top, bounds.Right, bounds.Top + bounds.Height / 2);
// Right line
path.AddLine(bounds.Right, bounds.Top + bounds.Height / 2, bounds.Right, bounds.Bottom - radius);
// Bottom right corner arc
Rectangle arcRect = new(bounds.Right - diameter, bounds.Bottom - diameter, diameter, diameter);
path.AddArc(arcRect, 0, 90);
// Bottom line
path.AddLine(bounds.Right - radius, bounds.Bottom, bounds.Left + radius, bounds.Bottom);
// Bottom left corner arc
arcRect.X = bounds.Left;
path.AddArc(arcRect, 90, 45);
// Path back to middle of left side
path.AddLine(
bounds.Left + (int)(radius * (1 - Math.Cos(Math.PI / 4))),
bounds.Bottom - (int)(radius * Math.Sin(Math.PI / 4)),
bounds.Left,
bounds.Top + bounds.Height / 2);
// Close the path back to start
path.AddLine(bounds.Left, bounds.Top + bounds.Height / 2, bounds.Left + bounds.Width / 2, bounds.Top);
return path;
}
/// <summary>
/// Creates a GraphicsPath for a rounded rectangle.
/// </summary>
private static GraphicsPath CreateRoundedRectanglePath(Rectangle bounds, int radius)
{
GraphicsPath path = new();
path.AddRoundedRectangle(bounds, new Size(radius, radius));
return path;
}
}
|