// 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); private protected override bool PaintParentBackground => true; /// <summary> /// Draws button background with system styling (larger rounded corners). /// </summary> public override Rectangle DrawButtonBackground( Graphics graphics, Rectangle bounds, PushButtonState state, bool isDefault, bool focused, Color backColor) { GraphicsState? saved = graphics.Save(); try { graphics.SmoothingMode = SmoothingMode.AntiAlias; RectangleF pathBounds = GetPathBounds(bounds); using GraphicsPath fillPath = CreateRoundedPath(pathBounds, FocusIndicatorCornerRadius); using var brush = backColor.GetCachedSolidBrushScope(); graphics.FillPath(brush, fillPath); } finally { if (saved is not null) { graphics.Restore(saved); } } // Return content bounds (area inside the button for text/image) return bounds; } /// <summary> /// Draws a focus indicator using a white thicker border. /// </summary> public override void DrawFocusIndicator(Graphics graphics, Rectangle contentBounds, bool isDefault) { GraphicsState? saved = graphics.Save(); try { graphics.SmoothingMode = SmoothingMode.AntiAlias; RectangleF outerBounds = GetPathBounds(contentBounds); float outerRadius = FocusIndicatorCornerRadius + (2 * SystemStylePadding); Color focusColor = ResolveBorderColor(Color.White); using var focusBrush = focusColor.GetCachedSolidBrushScope(); using GraphicsPath focusPath = CreateRingPath( outerBounds, outerRadius, FocusedButtonBorderThickness); graphics.FillPath(focusBrush, focusPath); } finally { if (saved is not null) { graphics.Restore(saved); } } } /// <summary> /// Gets the text color appropriate for the button state and type. /// </summary> public override Color GetTextColor(PushButtonState state, bool isDefault, Color backColor) => state == PushButtonState.Disabled ? DefaultColors.DisabledTextColor : isDefault ? DefaultColors.StandardBackColor : DefaultColors.AcceptButtonTextColor; /// <summary> /// Gets the background color appropriate for the button state and type. /// </summary> public override 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 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 = CreateRoundedPath( GetPathBounds(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; } private static RectangleF GetPathBounds(Rectangle bounds) => new( bounds.X, bounds.Y, Math.Max(1, bounds.Width - 1), Math.Max(1, bounds.Height - 1)); private static RectangleF Inset(RectangleF bounds, float inset) => new( bounds.X + inset, bounds.Y + inset, Math.Max(1, bounds.Width - (2 * inset)), Math.Max(1, bounds.Height - (2 * inset))); private static GraphicsPath CreateRoundedPath(RectangleF bounds, float radius) { GraphicsPath path = new(); float clampedRadius = Math.Clamp(radius, 1, Math.Min(bounds.Width, bounds.Height)); path.AddRoundedRectangle(bounds, new SizeF(clampedRadius, clampedRadius)); return path; } private static GraphicsPath CreateRingPath( RectangleF outerBounds, float outerRadius, float thickness) { GraphicsPath path = CreateRoundedPath(outerBounds, outerRadius); RectangleF innerBounds = Inset(outerBounds, thickness); float innerRadius = Math.Max(1, outerRadius - (2 * thickness)); using GraphicsPath innerPath = CreateRoundedPath(innerBounds, innerRadius); path.FillMode = FillMode.Alternate; path.AddPath(innerPath, connect: false); return path; } }