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..1f123df3399 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; @@ -77,35 +77,29 @@ public override void RenderControl(Graphics graphics) focused: Control.Focused && ShowFocusCues); ToggleSwitchMetrics metrics = ToggleSwitchMetrics.Create(Control); - Size textSize = TextRenderer.MeasureText(Control.Text, Control.Font); + TextFormatFlags textFormatFlags = GetTextFormatFlags(Control); + Size textSize = MeasureText(Control, textFormatFlags); Rectangle contentBounds = ToggleSwitchMetrics.GetContentBounds(Control); - 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); Rectangle switchBounds = GetSwitchBounds( Control, RtlTranslatedCheckAlign, metrics, textSize); + Rectangle textBounds = GetTextBounds( + contentBounds, + switchBounds, + metrics, + RtlTranslatedCheckAlign); - graphics.Clear(Control.BackColor); + PaintControlBackground(graphics); if (contentBounds.Width <= 0 || contentBounds.Height <= 0) { return; } - if (IsSwitchOnRight(RtlTranslatedCheckAlign)) - { - int textX = Math.Max(contentBounds.Left, switchBounds.Left - metrics.TextGap - textSize.Width); - RenderSwitch(graphics, switchBounds, metrics); - RenderText(graphics, new Point(textX, textY)); - } - else - { - RenderSwitch(graphics, switchBounds, metrics); - RenderText(graphics, new Point(contentBounds.Left + metrics.SwitchWidth + metrics.TextGap, textY)); - } + RenderSwitch(graphics, switchBounds, metrics); + RenderText(graphics, textBounds, textFormatFlags); if (Control.Focused && ShowFocusCues) { @@ -149,11 +143,14 @@ internal static Rectangle GetSwitchBounds( Control control, ContentAlignment checkAlign, ToggleSwitchMetrics metrics) - => GetSwitchBounds( + { + TextFormatFlags textFormatFlags = GetTextFormatFlags(control); + return GetSwitchBounds( control, checkAlign, metrics, - TextRenderer.MeasureText(control.Text, control.Font)); + MeasureText(control, textFormatFlags)); + } private static Rectangle GetSwitchBounds( Control control, @@ -165,23 +162,102 @@ private static Rectangle GetSwitchBounds( 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); - int switchX = IsSwitchOnRight(checkAlign) - ? Math.Max(contentBounds.Left, contentBounds.Right - metrics.SwitchWidth) - : contentBounds.Left; + int minimumSwitchX = contentBounds.Left; + int maximumSwitchX = Math.Max(minimumSwitchX, contentBounds.Right - metrics.SwitchWidth); + int edgeInset = Math.Max(1, (metrics.BorderThickness / 2) + 1); + int targetSwitchX = IsSwitchOnRight(checkAlign) + ? maximumSwitchX - edgeInset + : minimumSwitchX + edgeInset; + int switchX = Math.Max(minimumSwitchX, Math.Min(maximumSwitchX, targetSwitchX)); return new Rectangle(switchX, switchY, metrics.SwitchWidth, metrics.SwitchHeight); } - private void RenderText(Graphics graphics, Point position) + internal static Rectangle GetTextBounds( + Control control, + ContentAlignment checkAlign, + ToggleSwitchMetrics metrics) + { + TextFormatFlags textFormatFlags = GetTextFormatFlags(control); + Size textSize = MeasureText(control, textFormatFlags); + Rectangle contentBounds = ToggleSwitchMetrics.GetContentBounds(control); + Rectangle switchBounds = GetSwitchBounds(control, checkAlign, metrics, textSize); + return GetTextBounds(contentBounds, switchBounds, metrics, checkAlign); + } + + private static Rectangle GetTextBounds( + Rectangle contentBounds, + Rectangle switchBounds, + ToggleSwitchMetrics metrics, + ContentAlignment checkAlign) + { + // Keep a small symmetric inset between the switch track and text to avoid visual collision + // with the rounded switch border in high DPI while preserving readability in narrow widths. + int textGapInset = GetSwitchTextInset(metrics); + int minimumTextGap = Math.Max(0, metrics.TextGap - textGapInset); + + if (IsSwitchOnRight(checkAlign)) + { + int right = Math.Min( + contentBounds.Right, + switchBounds.Left - minimumTextGap); + right = Math.Max(contentBounds.Left, right); + return Rectangle.FromLTRB(contentBounds.Left, contentBounds.Top, right, contentBounds.Bottom); + } + + int left = Math.Max( + contentBounds.Left, + switchBounds.Right + minimumTextGap); + left = Math.Min(contentBounds.Right, left); + return Rectangle.FromLTRB(left, contentBounds.Top, contentBounds.Right, contentBounds.Bottom); + } + + private static int GetSwitchTextInset(ToggleSwitchMetrics metrics) + => Math.Max(1, (metrics.BorderThickness / 2) + 1); + + private static Size MeasureText(Control control, TextFormatFlags textFormatFlags) + => TextRenderer.MeasureText( + text: control.Text, + font: control.Font, + proposedSize: new Size(int.MaxValue, int.MaxValue), + flags: textFormatFlags); + + private void RenderText( + Graphics graphics, + Rectangle textBounds, + TextFormatFlags textFormatFlags) { + if (textBounds.Width <= 0 || textBounds.Height <= 0) + { + return; + } + TextRenderer.DrawText( graphics, Control.Text, Control.Font, - position, - GetTextColor()); + textBounds, + GetTextColor(), + textFormatFlags); } + private static TextFormatFlags GetTextFormatFlags(Control control) => control switch + { + Forms.CheckBox { FlatStyle: FlatStyle.System } checkBox => ControlPaint.CreateTextFormatFlags( + checkBox, + checkBox.TextAlign, + checkBox.ShowToolTip, + checkBox.UseMnemonic), + Forms.RadioButton { FlatStyle: FlatStyle.System } radioButton => ControlPaint.CreateTextFormatFlags( + radioButton, + radioButton.TextAlign, + radioButton.ShowToolTip, + radioButton.UseMnemonic), + Forms.CheckBox checkBox => checkBox.CreateTextFormatFlags(), + Forms.RadioButton radioButton => radioButton.CreateTextFormatFlags(), + _ => TextFormatFlags.WordBreak | TextFormatFlags.TextBoxControl + }; + internal Color GetTextColor() => Control.Enabled ? Control.ForeColor @@ -329,6 +405,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/test/unit/System.Windows.Forms/System/Windows/Forms/CheckBoxTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/CheckBoxTests.cs index 7a9033f3f64..04d30f6fc95 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 @@ -773,9 +773,78 @@ public void CheckBox_ToggleSwitch_CheckAlign_PositionsSwitch( metrics); Rectangle contentBounds = Rendering.CheckBox.ToggleSwitchMetrics.GetContentBounds(box); - Assert.Equal( - switchOnRight ? contentBounds.Right : contentBounds.Left, - switchOnRight ? switchBounds.Right : switchBounds.Left); + int edgeTolerance = Math.Max(1, (metrics.BorderThickness / 2) + 1); + if (switchOnRight) + { + Assert.InRange( + switchBounds.Right, + contentBounds.Right - edgeTolerance, + contentBounds.Right); + } + else + { + Assert.InRange( + switchBounds.Left, + contentBounds.Left, + contentBounds.Left + edgeTolerance); + } + } + + [WinFormsFact] + public void CheckBox_ToggleSwitch_RtlLongText_AutoSizeFalse_TextBounds_DoNotOverlapSwitch() + { + using CheckBox box = new() + { + Appearance = Appearance.ToggleSwitch, + AutoSize = false, + RightToLeft = RightToLeft.Yes, + CheckAlign = ContentAlignment.MiddleLeft, + Text = "This is a very long toggle-switch label to verify RTL clipping does not overlap the switch glyph.", + Size = new Size(160, 30) + }; + + box.VisualStylesMode = VisualStylesMode.Net11; + box.CreateControl(); + + Rendering.CheckBox.ToggleSwitchMetrics metrics = Rendering.CheckBox.ToggleSwitchMetrics.Create(box); + Rectangle switchBounds = Rendering.CheckBox.AnimatedToggleSwitchRenderer.GetSwitchBounds( + box, + box.RtlTranslatedCheckAlign, + metrics); + Rectangle textBounds = Rendering.CheckBox.AnimatedToggleSwitchRenderer.GetTextBounds( + box, + box.RtlTranslatedCheckAlign, + metrics); + Rectangle overlap = Rectangle.Intersect(switchBounds, textBounds); + + Assert.True(overlap.IsEmpty); + } + + [WinFormsFact] + public void CheckBox_ToggleSwitch_NarrowWidth_TextBoundsCanCollapseWithoutThrowing() + { + using CheckBox box = new() + { + Appearance = Appearance.ToggleSwitch, + AutoSize = false, + Text = "Narrow control text", + Size = new Size(8, 30), + VisualStylesMode = VisualStylesMode.Net11 + }; + using Bitmap bitmap = new(box.Width, box.Height); + + Exception exception = Record.Exception( + () => box.DrawToBitmap(bitmap, new Rectangle(Point.Empty, box.Size))); + + Assert.Null(exception); + + Rendering.CheckBox.ToggleSwitchMetrics metrics = Rendering.CheckBox.ToggleSwitchMetrics.Create(box); + Rectangle textBounds = Rendering.CheckBox.AnimatedToggleSwitchRenderer.GetTextBounds( + box, + box.RtlTranslatedCheckAlign, + metrics); + + Assert.InRange(textBounds.Width, 0, 1); } [WinFormsTheory] @@ -950,6 +1019,25 @@ public void CheckBox_ToggleSwitch_ThreeStateChange_UpdatesOwnerDraw() Assert.False(box.GetStyle(ControlStyles.UserPaint)); } + [WinFormsFact] + public void CheckBox_ToggleSwitch_FlatStyleSystem_DrawToBitmap_DoesNotThrow() + { + using CheckBox box = new() + { + Appearance = Appearance.ToggleSwitch, + FlatStyle = FlatStyle.System, + VisualStylesMode = VisualStylesMode.Net11, + Text = "Toggle", + Size = new Size(120, 30) + }; + using Bitmap bitmap = new(box.Width, box.Height); + + Exception exception = Record.Exception( + () => box.DrawToBitmap(bitmap, new Rectangle(Point.Empty, box.Size))); + + Assert.Null(exception); + } + private static int CountPixels(Bitmap bitmap, Color color) { int argb = color.ToArgb(); @@ -1377,10 +1465,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) {