diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/CheckBoxModernAdapter.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/CheckBoxModernAdapter.cs index 11e6d11d5fd..6fb51c24a54 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/CheckBoxModernAdapter.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/CheckBoxModernAdapter.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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; @@ -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 diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/RadioButtonModernAdapter.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/RadioButtonModernAdapter.cs index bbd78441fb1..36941ee5daf 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/RadioButtonModernAdapter.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/RadioButtonModernAdapter.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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; @@ -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 diff --git a/src/System.Windows.Forms/System/Windows/Forms/Rendering/CheckBox/AnimatedToggleSwitchRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Rendering/CheckBox/AnimatedToggleSwitchRenderer.cs index 42a4443bf63..762c4ba98eb 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Rendering/CheckBox/AnimatedToggleSwitchRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Rendering/CheckBox/AnimatedToggleSwitchRenderer.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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; @@ -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) { @@ -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); @@ -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)); diff --git a/src/System.Windows.Forms/System/Windows/Forms/Rendering/RadioButton/AnimatedRadioGlyphRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Rendering/RadioButton/AnimatedRadioGlyphRenderer.cs index 2582400678c..c91543ef617 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Rendering/RadioButton/AnimatedRadioGlyphRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Rendering/RadioButton/AnimatedRadioGlyphRenderer.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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; @@ -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); @@ -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); } @@ -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); } @@ -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 @@ -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; diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/CheckBoxTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/CheckBoxTests.cs index 7a9033f3f64..3680393822d 100644 --- a/src/test/unit/System.Windows.Forms/System/Windows/Forms/CheckBoxTests.cs +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/CheckBoxTests.cs @@ -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) { @@ -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, @@ -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] @@ -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() { diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/CheckBoxToggleSwitchTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/CheckBoxToggleSwitchTests.cs index 7bca4c1e445..21fcd2bc9bd 100644 --- a/src/test/unit/System.Windows.Forms/System/Windows/Forms/CheckBoxToggleSwitchTests.cs +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/CheckBoxToggleSwitchTests.cs @@ -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)); + } } diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/RadioButtonTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/RadioButtonTests.cs index 5ea64b41180..8e0b9fc6e02 100644 --- a/src/test/unit/System.Windows.Forms/System/Windows/Forms/RadioButtonTests.cs +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/RadioButtonTests.cs @@ -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) { @@ -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, @@ -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] @@ -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++; }