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 e9f3aecae36..c6c7026a68e 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; @@ -78,7 +78,7 @@ public override void RenderControl(Graphics graphics) ToggleSwitchMetrics metrics = ToggleSwitchMetrics.Create(Control); Size textSize = TextRenderer.MeasureText(Control.Text, Control.Font); - Rectangle contentBounds = ToggleSwitchMetrics.GetContentBounds(Control); + Rectangle contentBounds = ToggleSwitchMetrics.GetContentBounds(Control, metrics); int totalHeight = Math.Max(textSize.Height, metrics.SwitchHeight); int contentTop = contentBounds.Top + Math.Max(0, (contentBounds.Height - totalHeight) / 2); int textY = contentTop + ((totalHeight - textSize.Height) / 2); @@ -90,6 +90,11 @@ public override void RenderControl(Graphics graphics) PaintControlBackground(graphics); + if (Control.Focused && ShowFocusCues) + { + RenderFocusBorder(graphics, metrics); + } + if (contentBounds.Width <= 0 || contentBounds.Height <= 0) { return; @@ -106,15 +111,35 @@ public override void RenderControl(Graphics graphics) RenderSwitch(graphics, switchBounds, metrics); RenderText(graphics, new Point(contentBounds.Left + metrics.SwitchWidth + metrics.TextGap, textY)); } + } - if (Control.Focused && ShowFocusCues) + private void RenderFocusBorder(Graphics graphics, ToggleSwitchMetrics metrics) + { + Rectangle focusBounds = ToggleSwitchMetrics.GetFocusBounds(Control.ClientRectangle, metrics); + if (focusBounds.Width <= 0 || focusBounds.Height <= 0) { - Rectangle focusBounds = Rectangle.Inflate(Control.ClientRectangle, -1, -1); - ControlPaint.DrawFocusRectangle( - graphics, - focusBounds, - Control.ForeColor, - Control.BackColor); + return; + } + + Color focusColor = TextBoxBase.GetVisualStylesFocusColor( + Application.SystemVisualSettings.HighContrastEnabled); + using var focusPen = focusColor.GetCachedPenScope(metrics.FocusBorderThickness); + using GraphicsPath focusPath = new(); + int cornerSize = Math.Max(1, Math.Min(focusBounds.Width, focusBounds.Height)); + + focusPath.AddRoundedRectangle( + focusBounds, + new Size(cornerSize, cornerSize)); + + SmoothingMode previousSmoothingMode = graphics.SmoothingMode; + try + { + graphics.SmoothingMode = SmoothingMode.AntiAlias; + graphics.DrawPath(focusPen, focusPath); + } + finally + { + graphics.SmoothingMode = previousSmoothingMode; } } @@ -161,7 +186,7 @@ private static Rectangle GetSwitchBounds( ToggleSwitchMetrics metrics, Size textSize) { - Rectangle contentBounds = ToggleSwitchMetrics.GetContentBounds(control); + Rectangle contentBounds = ToggleSwitchMetrics.GetContentBounds(control, metrics); int totalHeight = Math.Max(textSize.Height, metrics.SwitchHeight); int contentTop = contentBounds.Top + Math.Max(0, (contentBounds.Height - totalHeight) / 2); int switchY = contentTop + ((totalHeight - metrics.SwitchHeight) / 2); @@ -394,7 +419,9 @@ private ToggleSwitchMetrics( int thumbDiameter, int hoverThumbDiameter, int borderThickness, - int textGap) + int textGap, + int focusBorderThickness, + int focusMargin) { SwitchWidth = switchWidth; SwitchHeight = switchHeight; @@ -402,6 +429,8 @@ private ToggleSwitchMetrics( HoverThumbDiameter = hoverThumbDiameter; BorderThickness = borderThickness; TextGap = textGap; + FocusBorderThickness = focusBorderThickness; + FocusMargin = focusMargin; } internal int SwitchWidth { get; } @@ -416,6 +445,10 @@ private ToggleSwitchMetrics( internal int TextGap { get; } + internal int FocusBorderThickness { get; } + + internal int FocusMargin { get; } + internal static ToggleSwitchMetrics Create(Control control) { int switchHeight = Math.Max( @@ -423,6 +456,8 @@ internal static ToggleSwitchMetrics Create(Control control) (int)(control.Font.Height * 0.9f)); int minimumMetric = Math.Max(1, control.LogicalToDeviceUnits(1)); int borderThickness = Math.Max(minimumMetric, switchHeight / 12); + int focusBorderThickness = Math.Max(minimumMetric, control.LogicalToDeviceUnits(2)); + int focusMargin = focusBorderThickness + Math.Max(minimumMetric, control.LogicalToDeviceUnits(2)); int maximumThumbDiameter = Math.Max(1, switchHeight - (2 * borderThickness)); int thumbDiameter = Math.Max( 1, @@ -440,24 +475,38 @@ internal static ToggleSwitchMetrics Create(Control control) thumbDiameter: thumbDiameter, hoverThumbDiameter: hoverThumbDiameter, borderThickness: borderThickness, - textGap: textGap); + textGap: textGap, + focusBorderThickness: focusBorderThickness, + focusMargin: focusMargin); } internal static Rectangle GetContentBounds(Control control) + => GetContentBounds(control, Create(control)); + + internal static Rectangle GetContentBounds(Control control, ToggleSwitchMetrics metrics) { Padding padding = control.Padding; + int horizontalInset = padding.Horizontal + (2 * metrics.FocusMargin); + int verticalInset = padding.Vertical + (2 * metrics.FocusMargin); + return new Rectangle( - padding.Left, - padding.Top, - Math.Max(0, control.ClientSize.Width - padding.Horizontal), - Math.Max(0, control.ClientSize.Height - padding.Vertical)); + padding.Left + metrics.FocusMargin, + padding.Top + metrics.FocusMargin, + Math.Max(0, control.ClientSize.Width - horizontalInset), + Math.Max(0, control.ClientSize.Height - verticalInset)); + } + + internal static Rectangle GetFocusBounds(Rectangle clientRectangle, ToggleSwitchMetrics metrics) + { + int inset = metrics.FocusBorderThickness; + return Rectangle.Inflate(clientRectangle, -inset, -inset); } internal Size GetPreferredSize(Control control) { Size textSize = TextRenderer.MeasureText(control.Text, control.Font); return new Size( - SwitchWidth + TextGap + textSize.Width + control.Padding.Horizontal, - Math.Max(SwitchHeight, textSize.Height) + control.Padding.Vertical); + SwitchWidth + TextGap + textSize.Width + control.Padding.Horizontal + (2 * FocusMargin), + Math.Max(SwitchHeight, textSize.Height) + control.Padding.Vertical + (2 * FocusMargin)); } } 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 3680393822d..adf333eb6a5 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 @@ -926,6 +926,50 @@ public void CheckBox_ToggleSwitch_HoverAndFocusAnimateWithoutChangingPreferredSi Assert.Equal(preferredSize, box.GetPreferredSize(Size.Empty)); } + [WinFormsFact] + public void CheckBox_ToggleSwitch_FocusedPaintsRoundedSolidFocusBorderAndReservesMargin() + { + using SystemVisualSettingsTestScope settingsScope = new( + clientAreaAnimationEnabled: false, + highContrastEnabled: false, + accentColor: Color.Red); + using CheckBox box = new() + { + Appearance = Appearance.ToggleSwitch, + BackColor = Color.White, + ForeColor = Color.Black, + Size = new Size(140, 36), + Text = "Toggle", + VisualStylesMode = VisualStylesMode.Net11 + }; + + Rendering.CheckBox.ToggleSwitchMetrics metrics = Rendering.CheckBox.ToggleSwitchMetrics.Create(box); + Rectangle contentBounds = Rendering.CheckBox.ToggleSwitchMetrics.GetContentBounds(box, metrics); + Rectangle focusBounds = Rendering.CheckBox.ToggleSwitchMetrics.GetFocusBounds(box.ClientRectangle, metrics); + Size textSize = TextRenderer.MeasureText(box.Text, box.Font); + Size preferredSizeWithoutFocusMargin = new( + metrics.SwitchWidth + metrics.TextGap + textSize.Width + box.Padding.Horizontal, + Math.Max(metrics.SwitchHeight, textSize.Height) + box.Padding.Vertical); + + Assert.Equal(box.Padding.Left + metrics.FocusMargin, contentBounds.Left); + Assert.Equal(box.Padding.Top + metrics.FocusMargin, contentBounds.Top); + Assert.Equal( + preferredSizeWithoutFocusMargin + new Size(2 * metrics.FocusMargin, 2 * metrics.FocusMargin), + box.GetPreferredSize(Size.Empty)); + + 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.TestAccessor.Dynamic.RenderFocusBorder(graphics, metrics); + + Assert.True(CountPixels(bitmap, Color.Red) > 0); + Assert.NotEqual(Color.Red.ToArgb(), bitmap.GetPixel(focusBounds.Left, focusBounds.Top).ToArgb()); + Assert.Equal(0, CountPixelsInColumn(bitmap, box.ClientRectangle.Right - 1, Color.Red)); + } + [WinFormsFact] public void CheckBox_ToggleSwitch_SynchronizeState_SettlesInteractionChannels() { @@ -1034,6 +1078,22 @@ private static int CountPixels(Bitmap bitmap, Color color) return count; } + private static int CountPixelsInColumn(Bitmap bitmap, int column, Color color) + { + int argb = color.ToArgb(); + int count = 0; + + for (int y = 0; y < bitmap.Height; y++) + { + if (bitmap.GetPixel(column, y).ToArgb() == argb) + { + count++; + } + } + + return count; + } + [WinFormsFact] public void CheckBox_GetAutoSizeMode_Invoke_ReturnsExpected() { @@ -1442,10 +1502,10 @@ public void CheckBox_ProcessMnemonic_ValidCases(bool useMnemonic, char charCode, } [WinFormsTheory] - [InlineData(Appearance.Button, FlatStyle.Standard, "Test", 12, 8, 100, 20)] - [InlineData(Appearance.Normal, FlatStyle.System, "Test", 12, 8, 100, 20)] - [InlineData(Appearance.Normal, FlatStyle.Flat, "Test", 12, 8, 100, 20)] - [InlineData(Appearance.Normal, FlatStyle.Standard, "Test", 12, 8, 100, 20)] + [InlineData(Appearance.Button, FlatStyle.Standard, "Test", 12, 8, 100, 20)] + [InlineData(Appearance.Normal, FlatStyle.System, "Test", 12, 8, 100, 20)] + [InlineData(Appearance.Normal, FlatStyle.Flat, "Test", 12, 8, 100, 20)] + [InlineData(Appearance.Normal, FlatStyle.Standard, "Test", 12, 8, 100, 20)] public void CheckBox_GetPreferredSizeCore_VariousStyles_ReturnsExpected( Appearance appearance, FlatStyle flatStyle, string text, int fontSize, int padding, int width, int height) {