Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ private void PaintCore(PaintEventArgs e)
PaintBackgroundImage(e);

Color? customOnColor = Control.ShouldSerializeBackColor()
? Control.BackColor
: null;
&& Control.BackColor.A == byte.MaxValue
? Control.BackColor
: null;

Color? customBorderColor = Control.FlatAppearance.BorderColor.IsEmpty
? null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,9 @@ private void PaintCore(PaintEventArgs e)
PaintBackgroundImage(e);

Color? customOnColor = Control.ShouldSerializeBackColor()
? Control.BackColor
: null;
&& Control.BackColor.A == byte.MaxValue
? Control.BackColor
: null;

Color? customBorderColor = Control.FlatAppearance.BorderColor.IsEmpty
? null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public override void RenderControl(Graphics graphics)
metrics,
textSize);

graphics.Clear(Control.BackColor);
PaintControlBackground(graphics);

if (contentBounds.Width <= 0 || contentBounds.Height <= 0)
{
Expand Down Expand Up @@ -218,7 +218,7 @@ private void RenderSwitch(Graphics graphics, Rectangle rect, ToggleSwitchMetrics
Color circleColor = Control.Enabled
? highContrast
? highContrastForeground
: PopupButtonColorMath.GetReadableForeColor(offColor, onColor)
: PopupButtonColorMath.GetReadableForeColor(backgroundColor)
: SystemColors.GrayText;
circleColor = ApplyInteractionShade(circleColor, focus);
borderColor = ApplyInteractionShade(borderColor, focus);
Expand Down Expand Up @@ -329,6 +329,25 @@ private void EnsurePositionInitialized()
_positionInitialized = true;
}

private void PaintControlBackground(Graphics graphics)
{
if (Control.BackgroundImage is null && !Control.BackColor.HasTransparency())
{
graphics.Clear(Control.BackColor);
return;
}

Rectangle clipRectangle = Rectangle.Ceiling(graphics.ClipBounds);
clipRectangle.Intersect(Control.ClientRectangle);
if (clipRectangle.Width <= 0 || clipRectangle.Height <= 0)
{
return;
}

using PaintEventArgs paintEventArgs = new(graphics, clipRectangle);
Control.PaintBackground(paintEventArgs, clipRectangle);
}

private static float EaseOut(float value)
=> 1 - ((1 - value) * (1 - value));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,15 @@ internal void DrawGlyph(
borderColor = ApplyInteractionShade(borderColor, focus);
backColor = ApplyInteractionShade(backColor, focus);

// Blend the outer circle toward the accent based on animated checked progress
// (_dotScaleCurrent), so check and uncheck transitions stay smooth and symmetric.
float checkedProgress = Math.Clamp(
_dotScaleCurrent,
0f,
1f);
Color activeBackColor = LerpColor(backColor, onColor, checkedProgress);
Color activeBorderColor = LerpColor(borderColor, onColor, checkedProgress);

float normalOuterScale = 1f / HoverGrowth;
float outerScale = Lerp(normalOuterScale, 1f, enabled ? _hoverCurrent : 0f);
RectangleF outerBounds = ScaleFromCenter(bounds, outerScale);
Expand All @@ -133,7 +142,7 @@ internal void DrawGlyph(
{
graphics.SmoothingMode = SmoothingMode.AntiAlias;

using (var brush = backColor.GetCachedSolidBrushScope())
using (var brush = activeBackColor.GetCachedSolidBrushScope())
{
graphics.FillEllipse(brush, outerBounds);
}
Expand All @@ -142,7 +151,7 @@ internal void DrawGlyph(
1,
Control.LogicalToDeviceUnits(flatStyle == FlatStyle.Popup ? 2 : 1));

using (var pen = new Pen(borderColor, borderThickness))
using (var pen = new Pen(activeBorderColor, borderThickness))
{
graphics.DrawEllipse(pen, outerBounds);
}
Expand All @@ -156,22 +165,11 @@ internal void DrawGlyph(
dotDiameter,
dotDiameter);

Color dotOutlineColor = highContrast
Color dotColor = highContrast
? SystemColors.HighlightText
: PopupButtonColorMath.GetReadableForeColor(onColor, backColor);
int outlineThickness = Math.Max(1, Control.LogicalToDeviceUnits(1));
using var outlineBrush = dotOutlineColor.GetCachedSolidBrushScope();
graphics.FillEllipse(outlineBrush, dotRectangle);

RectangleF accentRectangle = RectangleF.Inflate(
dotRectangle,
-outlineThickness,
-outlineThickness);
if (accentRectangle.Width > 0 && accentRectangle.Height > 0)
{
using var dotBrush = onColor.GetCachedSolidBrushScope();
graphics.FillEllipse(dotBrush, accentRectangle);
}
: PopupButtonColorMath.GetReadableForeColor(onColor);
using var dotBrush = dotColor.GetCachedSolidBrushScope();
graphics.FillEllipse(dotBrush, dotRectangle);
}
}
finally
Expand Down Expand Up @@ -226,6 +224,20 @@ private static float EaseOut(float progress)
private static float Lerp(float from, float to, float progress)
=> from + ((to - from) * Math.Clamp(progress, 0f, 1f));

private static Color LerpColor(Color from, Color to, float progress)
{
progress = Math.Clamp(progress, 0f, 1f);

return Color.FromArgb(
LerpChannel(from.A, to.A, progress),
LerpChannel(from.R, to.R, progress),
LerpChannel(from.G, to.G, progress),
LerpChannel(from.B, to.B, progress));

static int LerpChannel(int from, int to, float progress)
=> from + (int)((to - from) * progress);
}

private static RectangleF ScaleFromCenter(Rectangle bounds, float scale)
{
float width = bounds.Width * scale;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,43 @@ public void CheckBox_ModernGlyph_DefaultCheckedColorUsesWindowsAccent()
}

[WinFormsFact]
public void CheckBox_ModernGlyph_BrightAccentUsesReadableCheckmark()
public void CheckBox_ModernGlyph_TransparentBackColor_CheckedUsesWindowsAccent()
{
if (SystemInformation.HighContrast)
{
return;
}

using Panel parent = new() { BackColor = Color.White };
using CheckBox box = new()
{
BackColor = Color.Transparent,
CheckState = CheckState.Checked,
Size = new Size(40, 24),
VisualStylesMode = VisualStylesMode.Net11
};
parent.Controls.Add(box);

using Bitmap bitmap = new(box.Width, box.Height);
using Graphics graphics = Graphics.FromImage(bitmap);
PaintEventArgs e = new(graphics, box.ClientRectangle);

box.CreateStandardAdapter().PaintUp(e, box.CheckState);

Assert.True(CountPixels(bitmap, Application.SystemVisualSettings.AccentColor) > 0);
}

[WinFormsTheory]
[InlineData(0xFF, 0xB9, 0x00, 0, 0, 0)]
[InlineData(0x00, 0x66, 0xCC, 255, 255, 255)]
[InlineData(0x4D, 0x4D, 0x4D, 255, 255, 255)]
public void CheckBox_ModernGlyph_AccentUsesReadableCheckmark(
int accentR,
int accentG,
int accentB,
int expectedR,
int expectedG,
int expectedB)
{
if (SystemInformation.HighContrast)
{
Expand All @@ -637,7 +673,7 @@ public void CheckBox_ModernGlyph_BrightAccentUsesReadableCheckmark()
renderer.NotifyCheckStateChanged(CheckState.Checked);
using Bitmap bitmap = new(24, 24);
using Graphics graphics = Graphics.FromImage(bitmap);
Color brightAccent = Color.FromArgb(0xFF, 0xB9, 0x00);
Color accent = Color.FromArgb(accentR, accentG, accentB);

renderer.DrawGlyph(
graphics,
Expand All @@ -646,10 +682,10 @@ public void CheckBox_ModernGlyph_BrightAccentUsesReadableCheckmark()
enabled: true,
hovered: false,
focused: false,
customOnColor: brightAccent,
customOnColor: accent,
customBorderColor: null);

Assert.True(CountPixels(bitmap, Color.Black) > 0);
Assert.True(CountPixels(bitmap, Color.FromArgb(expectedR, expectedG, expectedB)) > 0);
}

[WinFormsFact]
Expand Down Expand Up @@ -831,6 +867,35 @@ public void CheckBox_ToggleSwitch_DefaultOnColorUsesWindowsAccent()
Assert.True(CountPixels(bitmap, Application.SystemVisualSettings.AccentColor) > 0);
}

[WinFormsFact]
public void CheckBox_ToggleSwitch_DarkAccentUsesReadableThumbColor()
{
if (SystemInformation.HighContrast)
{
return;
}

using SystemVisualSettingsTestScope settingsScope =
new(clientAreaAnimationEnabled: true, accentColor: Color.FromArgb(0x4D, 0x4D, 0x4D));
using CheckBox box = new()
{
Appearance = Appearance.ToggleSwitch,
BackColor = Color.Black,
Checked = true,
Size = new Size(60, 24),
VisualStylesMode = VisualStylesMode.Net11
};
Rendering.CheckBox.AnimatedToggleSwitchRenderer renderer =
box.TestAccessor.Dynamic.ToggleSwitchRenderer;
renderer.SynchronizeState();
using Bitmap bitmap = new(box.Width, box.Height);
using Graphics graphics = Graphics.FromImage(bitmap);

renderer.RenderControl(graphics);

Assert.True(CountPixels(bitmap, Color.White) > 0);
}

[WinFormsFact]
public void CheckBox_ToggleSwitch_HoverAndFocusAnimateWithoutChangingPreferredSize()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,4 +226,30 @@ public void CheckBox_VisualStylesMode_DefaultIsAmbient()

Assert.Equal(VisualStylesMode.Inherit, checkBox.VisualStylesMode);
}

[WinFormsTheory]
[InlineData(true)]
[InlineData(false)]
public void CheckBox_ToggleSwitch_TransparentBackColor_DoesNotThrow(bool useBackgroundImage)
{
using SystemVisualSettingsTestScope settingsScope = new(
clientAreaAnimationEnabled: false,
highContrastEnabled: false);
using Bitmap backgroundImage = new(10, 10);
using Panel parent = new() { BackColor = Color.LightBlue, Size = new Size(200, 100) };
using CheckBox checkBox = new()
{
Parent = parent,
Appearance = Appearance.ToggleSwitch,
VisualStylesMode = VisualStylesMode.Net11,
BackColor = Color.Transparent,
BackgroundImage = useBackgroundImage ? backgroundImage : null,
Size = new Size(140, 36),
Text = "Matrix"
};
parent.CreateControl();
using Bitmap bitmap = new(checkBox.Width, checkBox.Height);

checkBox.DrawToBitmap(bitmap, new Rectangle(Point.Empty, checkBox.Size));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,47 @@ public void RadioButton_ModernGlyph_DefaultCheckedColorUsesWindowsAccent()
}

[WinFormsFact]
public void RadioButton_ModernGlyph_BrightAccentUsesContrastingDotOutline()
public void RadioButton_ModernGlyph_TransparentBackColor_CheckedUsesWindowsAccent()
{
if (SystemInformation.HighContrast)
{
return;
}

using Panel parent = new() { BackColor = Color.White };
using RadioButton control = new()
{
BackColor = Color.Transparent,
Checked = true,
Size = new Size(40, 24),
VisualStylesMode = VisualStylesMode.Net11
};
parent.Controls.Add(control);

using Bitmap bitmap = new(control.Width, control.Height);
using Graphics graphics = Graphics.FromImage(bitmap);
PaintEventArgs e = new(graphics, control.ClientRectangle);

control.CreateStandardAdapter().PaintUp(e, CheckState.Checked);

Assert.True(
CountPixels(
bitmap,
Application.SystemVisualSettings.AccentColor,
channelTolerance: 24) > 0);
}

[WinFormsTheory]
[InlineData(0xFF, 0xB9, 0x00, 0, 0, 0)]
[InlineData(0x00, 0x66, 0xCC, 255, 255, 255)]
[InlineData(0x4D, 0x4D, 0x4D, 255, 255, 255)]
public void RadioButton_ModernGlyph_AccentUsesReadableDot(
int accentR,
int accentG,
int accentB,
int expectedR,
int expectedG,
int expectedB)
{
if (SystemInformation.HighContrast)
{
Expand All @@ -277,7 +317,7 @@ public void RadioButton_ModernGlyph_BrightAccentUsesContrastingDotOutline()
renderer.NotifyCheckedChanged(newChecked: true);
using Bitmap bitmap = new(24, 24);
using Graphics graphics = Graphics.FromImage(bitmap);
Color brightAccent = Color.FromArgb(0xFF, 0xB9, 0x00);
Color accent = Color.FromArgb(accentR, accentG, accentB);

renderer.DrawGlyph(
graphics,
Expand All @@ -286,10 +326,10 @@ public void RadioButton_ModernGlyph_BrightAccentUsesContrastingDotOutline()
enabled: true,
hovered: false,
focused: false,
customOnColor: brightAccent,
customOnColor: accent,
customBorderColor: null);

Assert.True(CountPixels(bitmap, Color.Black) > 0);
Assert.True(CountPixels(bitmap, Color.FromArgb(expectedR, expectedG, expectedB)) > 0);
}

[WinFormsFact]
Expand Down Expand Up @@ -331,15 +371,20 @@ public void RadioButton_ModernGlyph_EndAnimation_StopsAndSettles()
}

private static int CountPixels(Bitmap bitmap, Color color)
=> CountPixels(bitmap, color, channelTolerance: 0);

private static int CountPixels(Bitmap bitmap, Color color, int channelTolerance)
{
int argb = color.ToArgb();
int count = 0;

for (int y = 0; y < bitmap.Height; y++)
{
for (int x = 0; x < bitmap.Width; x++)
{
if (bitmap.GetPixel(x, y).ToArgb() == argb)
Color pixel = bitmap.GetPixel(x, y);
if (Math.Abs(pixel.R - color.R) <= channelTolerance
&& Math.Abs(pixel.G - color.G) <= channelTolerance
&& Math.Abs(pixel.B - color.B) <= channelTolerance)
{
count++;
}
Expand Down