Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand All @@ -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;
}
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -394,14 +419,18 @@ private ToggleSwitchMetrics(
int thumbDiameter,
int hoverThumbDiameter,
int borderThickness,
int textGap)
int textGap,
int focusBorderThickness,
int focusMargin)
{
SwitchWidth = switchWidth;
SwitchHeight = switchHeight;
ThumbDiameter = thumbDiameter;
HoverThumbDiameter = hoverThumbDiameter;
BorderThickness = borderThickness;
TextGap = textGap;
FocusBorderThickness = focusBorderThickness;
FocusMargin = focusMargin;
}

internal int SwitchWidth { get; }
Expand All @@ -416,13 +445,19 @@ private ToggleSwitchMetrics(

internal int TextGap { get; }

internal int FocusBorderThickness { get; }

internal int FocusMargin { get; }

internal static ToggleSwitchMetrics Create(Control control)
{
int switchHeight = Math.Max(
control.LogicalToDeviceUnits(13),
(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,
Expand All @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -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()
{
Expand Down Expand Up @@ -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)
{
Expand Down