diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.Modern.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.Modern.cs index fb6292663ce..135fe038cb8 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.Modern.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.Modern.cs @@ -37,9 +37,13 @@ private int ModernPreferredHeight + (2 * SystemInformation.FixedFrameBorderSize.Height); } - return ModernControlVisualStyles.GetPreferredFieldHeight( + SystemVisualSettings settings = Application.SystemVisualSettings; + + return ModernControlVisualStyles.GetSingleLineTextBoxPreferredHeight( fontHeight: FontHeight, - fieldPadding: GetModernFieldPadding(), + borderStyle: BorderStyle.Fixed3D, + focusBorderMetrics: settings.FocusBorderMetrics, + textScaleFactor: settings.TextScaleFactor, deviceDpi: DeviceDpiInternal); } } @@ -81,6 +85,7 @@ private unsafe void CaptureNativeComboBaseline( { IsCaptured = true, DeviceDpi = DeviceDpiInternal, + FontHeight = FontHeight, SelectionFieldItemHeight = selectionFieldItemHeight, SelectionFieldFrameHeight = Math.Max( 0, @@ -169,6 +174,40 @@ private ModernComboTargetState ComputeModernComboTargetState() { int topInset = chromeInsets.Top + Padding.Top; int bottomInset = chromeInsets.Bottom + Padding.Bottom; + int availableTop = ClientRectangle.Top + topInset; + int availableBottom = ClientRectangle.Bottom - bottomInset; + + if (DropDownStyle == ComboBoxStyle.DropDown) + { + // Keep the native text-safe height and center the EDIT child instead of putting + // all additional modern field height below its text. + int availableHeight = Math.Max(1, availableBottom - availableTop); + int nativeEditHeight = ScaleNativeBaselineValue( + _nativeComboBaseline.EditBounds.Height); + int baselineFontHeight = ScaleNativeBaselineValue( + _nativeComboBaseline.FontHeight); + int textSafeEditHeight = nativeEditHeight + + FontHeight + - baselineFontHeight; + + editBounds.Height = Math.Max( + 1, + Math.Min(textSafeEditHeight, availableHeight)); + editBounds.Y = availableTop + + ((availableHeight - editBounds.Height + 1) / 2); + } + else + { + editBounds.Y += topInset; + + // A single-line EDIT control's text visibility depends on its window height. + // Preserve the native height so glyphs are not clipped. + editBounds.Height = Math.Max( + 1, + Math.Min( + editBounds.Height, + availableBottom - editBounds.Top)); + } // Inset the edit window horizontally so its rectangular corners clear the rounded // field arcs, and reserve the (now wider) drop-down button on the button side. @@ -676,20 +715,19 @@ private Rectangle GetChildBounds(HWND child) private Padding GetModernChromeInsets() { - // Minimal modern field inset plus a small arc-clearance so the flat native edit child's - // rectangular corners are not painted into the rounded-corner arcs. This is chrome padding - // only; the caller still adds the user's Padding on top (see ComputeModernComboTargetState - // and GetModernFieldPadding). It deliberately excludes the classic 3D-border metrics - // (Fixed3DBorderPadding / InternalChromeInset) that the previous implementation inherited - // from the classic field model; the modern field owns a single rounded border across the - // full control, so those insets only produced a spurious inner margin. - int inset = ScaleHelper.ScaleToDpi( + // Keep only horizontal insets for rounded-corner arc clearance. Vertical insets stay zero + // so the native edit child keeps its full text height (important for descenders at 125% DPI). + int horizontalInset = ScaleHelper.ScaleToDpi( ModernControlVisualStyles.BorderThickness + ModernControlVisualStyles.ComboBoxStyleInset + ModernControlVisualStyles.ComboBoxFieldArcClearance, DeviceDpiInternal); - return new Padding(inset); + return new Padding( + left: horizontalInset, + top: 0, + right: horizontalInset, + bottom: 0); } private Rectangle GetNativeComboBaselineEditBounds() @@ -709,16 +747,29 @@ private int GetModernSimpleListClipRegionApplyCount() private Padding GetModernFieldPadding() { + Padding horizontalSource = GetModernChromeInsets(); + + if (DropDownStyle == ComboBoxStyle.DropDownList) + { + int verticalInset = ScaleHelper.ScaleToDpi( + ModernControlVisualStyles.BorderThickness, + DeviceDpiInternal); + + return new Padding( + left: horizontalSource.Left + Padding.Left, + top: verticalInset + Padding.Top, + right: horizontalSource.Right + Padding.Right, + bottom: verticalInset + Padding.Bottom); + } + SystemVisualSettings settings = Application.SystemVisualSettings; int styleInset = ScaleHelper.ScaleToDpi( ModernControlVisualStyles.ComboBoxStyleInset, DeviceDpiInternal); - // Vertical clearance is kept on the classic-derived field model so the modern preferred - // height stays aligned with TextBox (GetPreferredFieldHeight only consumes the vertical - // component). Horizontal clearance uses the minimal modern field inset so the field text - // and drop-down-list caption are not over-inset by classic 3D metrics. + // Editable DropDown / Simple keep the classic-derived vertical model so the native EDIT + // child text metrics remain stable. Horizontal clearance uses minimal modern inset. Padding verticalSource = ModernControlVisualStyles.GetFieldPadding( BorderStyle.Fixed3D, Padding + new Padding(styleInset), @@ -726,8 +777,6 @@ private Padding GetModernFieldPadding() settings.TextScaleFactor, DeviceDpiInternal); - Padding horizontalSource = GetModernChromeInsets(); - return new Padding( left: horizontalSource.Left + Padding.Left, top: verticalSource.Top, diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.ModernComboAdapter.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.ModernComboAdapter.cs index 80eb7f20aac..b2a99dab570 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.ModernComboAdapter.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.ModernComboAdapter.cs @@ -469,15 +469,11 @@ private static Color GetBorderColor(ComboBox comboBox, bool useAccent) } private static int GetBorderThickness(ComboBox comboBox) - { - SystemVisualSettings settings = Application.SystemVisualSettings; - Size borderMetrics = ModernControlVisualStyles.GetFocusBorderMetrics( - settings.FocusBorderMetrics, - settings.TextScaleFactor, - comboBox.DeviceDpiInternal); - - return Math.Max(borderMetrics.Width, borderMetrics.Height); - } + => Math.Max( + 1, + ScaleHelper.ScaleToDpi( + ModernControlVisualStyles.BorderThickness, + comboBox.DeviceDpiInternal)); private static GraphicsPath CreateFieldPath( ComboBox comboBox, diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.NativeComboBaseline.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.NativeComboBaseline.cs index 3eb94b99bdc..c1f70f9b004 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.NativeComboBaseline.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.NativeComboBaseline.cs @@ -16,6 +16,8 @@ private readonly struct NativeComboBaseline public int DeviceDpi { get; init; } + public int FontHeight { get; init; } + public int SelectionFieldItemHeight { get; init; } public int SelectionFieldFrameHeight { get; init; } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/TextBox/MaskedTextBox.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/TextBox/MaskedTextBox.cs index 08c4fd56f9a..d99d529246a 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/TextBox/MaskedTextBox.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/TextBox/MaskedTextBox.cs @@ -2979,8 +2979,8 @@ protected override void WndProc(ref Message m) break; case PInvokeCore.WM_SETFOCUS: - WmSetFocus(); base.WndProc(ref m); + WmSetFocus(); break; default: diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/TextBox/TextBoxBase.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/TextBox/TextBoxBase.cs index 7d31169fc5e..0c13386b1a5 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/TextBox/TextBoxBase.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/TextBox/TextBoxBase.cs @@ -774,6 +774,7 @@ public virtual bool Multiline RecreateHandle(); AdjustHeight(false); + EnsureModernMultilineAutoSizeHeight(); OnMultilineChanged(EventArgs.Empty); } } @@ -1506,6 +1507,26 @@ private void AdjustHeight(bool returnIfAnchored) } } + private void EnsureModernMultilineAutoSizeHeight() + { + if (!_textBoxFlags[s_autoSize] + || !_textBoxFlags[s_multiline] + || EffectiveVisualStylesMode < VisualStylesMode.Net11) + { + return; + } + + int singleLineHeight = PreferredHeight; + + if (Height >= singleLineHeight) + { + return; + } + + Height = singleLineHeight; + _requestedHeight = singleLineHeight; + } + /// /// Append text to the current text of text box. /// @@ -1711,6 +1732,7 @@ protected override void OnVisualStylesModeChanged(EventArgs e) _triggerNewClientSizeRequest = false; base.OnVisualStylesModeChanged(e); AdjustHeight(false); + EnsureModernMultilineAutoSizeHeight(); _focusIndicatorRenderer?.Synchronize(Focused, invalidate: false); RecalculateVisualStylesClientArea(); @@ -1729,6 +1751,7 @@ protected override void OnSystemVisualSettingsChanged(SystemVisualSettingsChange CommonProperties.xClearPreferredSizeCache(this); AdjustHeight(false); + EnsureModernMultilineAutoSizeHeight(); RecalculateVisualStylesClientArea(); if (ParentInternal is { } parent) @@ -2510,6 +2533,57 @@ private unsafe void WmNcCalcSize(ref Message m) ref RECT clientRect = ref ncCalcSizeParams->rgrc._0; + if (!Multiline) + { + // Keep enough single-line client height for native edit text metrics when the modern + // chrome carve would otherwise leave too little room at higher DPI scales. + int clientHeight = clientRect.bottom - clientRect.top; + int minimumSingleLineClientHeight = FontHeight + ScaleVisualStylesMetric(3); + int maxVerticalCarve = Math.Max(0, clientHeight - minimumSingleLineClientHeight); + + if (padding.Vertical > maxVerticalCarve) + { + int overflow = padding.Vertical - maxVerticalCarve; + int minimumTopPadding = BorderStyle switch + { + BorderStyle.None => 0, + BorderStyle.FixedSingle => ScaleVisualStylesMetric(ModernControlVisualStyles.BorderThickness), + _ => ScaleVisualStylesMetric(ModernControlVisualStyles.InternalChromeInset + ModernControlVisualStyles.BorderThickness) + }; + int minimumBottomPadding = BorderStyle == BorderStyle.None + ? 0 + : ScaleVisualStylesMetric(ModernControlVisualStyles.BorderThickness); + + // Bias the recovery toward the bottom inset first so baseline-driven single-line + // text sits slightly lower, while still preserving the minimum client height. + int availableBottomReduction = Math.Max(0, padding.Bottom - minimumBottomPadding); + int bottomReduction = Math.Min(overflow, availableBottomReduction); + padding.Bottom -= bottomReduction; + overflow -= bottomReduction; + + int availableTopReduction = Math.Max(0, padding.Top - minimumTopPadding); + int topReduction = Math.Min(overflow, availableTopReduction); + padding.Top -= topReduction; + overflow -= topReduction; + + if (overflow > 0) + { + int minimumVisibleVerticalPadding = BorderStyle == BorderStyle.None + ? 0 + : ScaleVisualStylesMetric(ModernControlVisualStyles.BorderThickness); + + int availableBottomVisualReduction = Math.Max(0, padding.Bottom - minimumVisibleVerticalPadding); + bottomReduction = Math.Min(overflow, availableBottomVisualReduction); + padding.Bottom -= bottomReduction; + overflow -= bottomReduction; + + int availableTopVisualReduction = Math.Max(0, padding.Top - minimumVisibleVerticalPadding); + topReduction = Math.Min(overflow, availableTopVisualReduction); + padding.Top -= topReduction; + } + } + } + // Never-invert clamp: a large Padding plus the live scrollbar allowance can drive the // carved client rect to zero or inverted. A 0-1px client area is acceptable and intended // (shipping multiline TextBox already collapses this way); we only prevent underflow past @@ -2660,7 +2734,7 @@ private protected virtual void OnNcPaint(Graphics graphics, HDC windowHdc) { int cornerRadius = ScaleVisualStylesMetric(ModernControlVisualStyles.FieldCornerRadius); Size focusBorderMetrics = GetVisualStylesFocusBorderMetrics(); - int borderThickness = Math.Max(focusBorderMetrics.Width, focusBorderMetrics.Height); + int borderThickness = ScaleVisualStylesMetric(ModernControlVisualStyles.BorderThickness); int focusBandHeight = GetVisualStylesFocusBandHeight(); Color clientBackColor = BackColor; diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/UpDown/UpDownBase.UpDownButtons.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/UpDown/UpDownBase.UpDownButtons.cs index 726ae729253..96f4ad27d2e 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/UpDown/UpDownBase.UpDownButtons.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/UpDown/UpDownBase.UpDownButtons.cs @@ -33,6 +33,7 @@ internal partial class UpDownButtons : Control private Bitmap? _cachedBitmap; private bool _doubleClickFired; + private const double ModernArrowScaleFactor = 0.8; /// /// Initializes a new instance of the class. @@ -319,14 +320,16 @@ protected override void OnPaint(PaintEventArgs e) GetButtonRectangle(ButtonID.Down), ModernControlButtonStyle.Down, GetButtonState(ButtonID.Down), - isDarkMode); + isDarkMode, + ModernArrowScaleFactor); DrawModernControlButton( cachedGraphics, GetButtonRectangle(ButtonID.Up), ModernControlButtonStyle.Up, GetButtonState(ButtonID.Up), - isDarkMode); + isDarkMode, + ModernArrowScaleFactor); e.GraphicsInternal.DrawImageUnscaled(_cachedBitmap, new Point(0, 0)); @@ -358,7 +361,8 @@ protected override void OnPaint(PaintEventArgs e) ? ModernControlButtonState.Pressed : (Enabled ? (_mouseOver == ButtonID.Up ? ModernControlButtonState.Hover : ModernControlButtonState.Normal) : ModernControlButtonState.Disabled), - true); + true, + ModernArrowScaleFactor); DrawModernControlButton( cachedGraphics, @@ -368,7 +372,8 @@ protected override void OnPaint(PaintEventArgs e) ? ModernControlButtonState.Pressed : (Enabled ? (_mouseOver == ButtonID.Down ? ModernControlButtonState.Hover : ModernControlButtonState.Normal) : ModernControlButtonState.Disabled), - true); + true, + ModernArrowScaleFactor); e.GraphicsInternal.DrawImageUnscaled( _cachedBitmap, diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/UpDown/UpDownBase.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/UpDown/UpDownBase.cs index 3ad9815ebbd..c5037c208d2 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/UpDown/UpDownBase.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/UpDown/UpDownBase.cs @@ -360,37 +360,39 @@ public int PreferredHeight { if (!UseSideBySideButtons) { - int height = FontHeight; - - // Adjust for the border style - if (_borderStyle != BorderStyle.None) - { - height += SystemInformation.BorderSize.Height * 4 + 3; - } - else - { - height += 3; - } - - return height; + return GetClassicPreferredHeight(); } - int contentInset = ModernContentInset; - int preferredHeight = FontHeight + (contentInset * 2); - - if (_borderStyle == BorderStyle.Fixed3D) - { - int roundedChromeMinimumHeight = LogicalToDeviceUnits(ModernControlVisualStyles.UpDownCornerRadius) - + LogicalToDeviceUnits(ModernControlVisualStyles.BorderThickness) - + LogicalToDeviceUnits(ModernControlVisualStyles.InternalChromeInset); + SystemVisualSettings settings = Application.SystemVisualSettings; - preferredHeight = Math.Max(preferredHeight, roundedChromeMinimumHeight); - } + int preferredHeight = ModernControlVisualStyles.GetSingleLineTextBoxPreferredHeight( + fontHeight: Font.Height, + borderStyle: _borderStyle, + focusBorderMetrics: settings.FocusBorderMetrics, + textScaleFactor: settings.TextScaleFactor, + deviceDpi: DeviceDpiInternal); return preferredHeight; } } + private int GetClassicPreferredHeight() + { + int height = FontHeight; + + // Adjust for the border style + if (_borderStyle != BorderStyle.None) + { + height += SystemInformation.BorderSize.Height * 4 + 3; + } + else + { + height += 3; + } + + return height; + } + /// /// Gets or sets a value indicating whether the text may only be changed by /// the use of the up or down buttons. @@ -566,11 +568,20 @@ protected override void OnHandleDestroyed(EventArgs e) /// protected override void OnVisualStylesModeChanged(EventArgs e) { + bool usedModernMetrics = UseSideBySideButtons; + int oldPreferredHeight = PreferredHeight; + base.OnVisualStylesModeChanged(e); _focusIndicatorRenderer?.Synchronize(Focused, invalidate: false); CommonProperties.xClearPreferredSizeCache(this); - if (AutoSize) + bool usesModernMetrics = UseSideBySideButtons; + bool heightStillAtClassicPreferred = Height == GetClassicPreferredHeight(); + + if (AutoSize + || (usedModernMetrics != usesModernMetrics + && Height == oldPreferredHeight) + || (usesModernMetrics && heightStillAtClassicPreferred)) { Height = PreferredHeight; } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Rendering/ControlPaint_ModernControlButtonRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Rendering/ControlPaint_ModernControlButtonRenderer.cs index 21372f534aa..374b4470b11 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Rendering/ControlPaint_ModernControlButtonRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Rendering/ControlPaint_ModernControlButtonRenderer.cs @@ -22,7 +22,8 @@ internal static void DrawModernControlButton( Rectangle bounds, ModernControlButtonStyle button, ModernControlButtonState state, - bool isDarkMode) + bool isDarkMode, + double contentScaleOverride = 1.0) { ArgumentNullException.ThrowIfNull(graphics); @@ -136,7 +137,13 @@ internal static void DrawModernControlButton( | ModernControlButtonStyle.RoundedBorder); bool isPressed = state == ModernControlButtonState.Pressed; - DrawButtonContent(graphics, bounds, contentType, arrowColor, isPressed); + DrawButtonContent( + graphics, + bounds, + contentType, + arrowColor, + isPressed, + contentScaleOverride); } finally { @@ -152,7 +159,8 @@ private static void DrawButtonContent( Rectangle bounds, ModernControlButtonStyle buttonType, Color contentColor, - bool isPressed) + bool isPressed, + double contentScaleOverride) { // Calculate center point int centerX = bounds.X + bounds.Width / 2; @@ -174,35 +182,35 @@ private static void DrawButtonContent( break; case ModernControlButtonStyle.Up: - DrawUpArrow(graphics, contentBrush, centerX, centerY, ScaleSymbolSize(bounds)); + DrawUpArrow(graphics, contentBrush, centerX, centerY, ScaleSymbolSize(bounds, contentScaleOverride)); break; case ModernControlButtonStyle.Down: - DrawDownArrow(graphics, contentBrush, centerX, centerY, ScaleSymbolSize(bounds)); + DrawDownArrow(graphics, contentBrush, centerX, centerY, ScaleSymbolSize(bounds, contentScaleOverride)); break; case ModernControlButtonStyle.UpDown: - DrawUpDownArrows(graphics, contentBrush, centerX, centerY, bounds); + DrawUpDownArrows(graphics, contentBrush, centerX, centerY, bounds, contentScaleOverride); break; case ModernControlButtonStyle.Left: - DrawLeftArrow(graphics, contentBrush, centerX, centerY, ScaleSymbolSize(bounds)); + DrawLeftArrow(graphics, contentBrush, centerX, centerY, ScaleSymbolSize(bounds, contentScaleOverride)); break; case ModernControlButtonStyle.Right: - DrawRightArrow(graphics, contentBrush, centerX, centerY, ScaleSymbolSize(bounds)); + DrawRightArrow(graphics, contentBrush, centerX, centerY, ScaleSymbolSize(bounds, contentScaleOverride)); break; case ModernControlButtonStyle.RightLeft: - DrawLeftRightArrows(graphics, contentBrush, centerX, centerY, bounds); + DrawLeftRightArrows(graphics, contentBrush, centerX, centerY, bounds, contentScaleOverride); break; case ModernControlButtonStyle.Ellipse: - DrawEllipseSymbol(graphics, contentBrush, centerX, centerY, bounds); + DrawEllipseSymbol(graphics, contentBrush, centerX, centerY, bounds, contentScaleOverride); break; case ModernControlButtonStyle.OpenDropDown: - DrawOpenDropDownChevron(graphics, contentBrush, centerX, centerY, ScaleSymbolSize(bounds)); + DrawOpenDropDownChevron(graphics, contentBrush, centerX, centerY, ScaleSymbolSize(bounds, contentScaleOverride)); break; } } @@ -210,7 +218,7 @@ private static void DrawButtonContent( /// /// Calculates the arrow size based on button bounds and DPI scaling. /// - private static int ScaleSymbolSize(Rectangle bounds) + private static int ScaleSymbolSize(Rectangle bounds, double contentScaleOverride) { // Base size is calculated as a fraction of the smaller dimension int minDimension = Math.Min(bounds.Width, bounds.Height); @@ -220,7 +228,7 @@ private static int ScaleSymbolSize(Rectangle bounds) const double baseSymbolRatio = 0.4; // Calculate the symbol size with scaling factor applied - int symbolSize = (int)(minDimension * baseSymbolRatio * ContentScaleFactor); + int symbolSize = (int)(minDimension * baseSymbolRatio * ContentScaleFactor * contentScaleOverride); // Ensure we always have at least a 1-pixel symbol return Math.Max(1, symbolSize); @@ -229,10 +237,10 @@ private static int ScaleSymbolSize(Rectangle bounds) /// /// Draws combined up/down arrows with proportional spacing. /// - private static void DrawUpDownArrows(Graphics graphics, Brush brush, int centerX, int centerY, Rectangle bounds) + private static void DrawUpDownArrows(Graphics graphics, Brush brush, int centerX, int centerY, Rectangle bounds, double contentScaleOverride) { // Get the base symbol size - int baseArrowSize = ScaleSymbolSize(bounds); + int baseArrowSize = ScaleSymbolSize(bounds, contentScaleOverride); // For combined arrows, reduce size slightly to fit both with spacing int arrowSize = (int)(baseArrowSize * 0.7); @@ -257,10 +265,10 @@ private static void DrawUpDownArrows(Graphics graphics, Brush brush, int centerX /// /// Draws combined left/right arrows with proportional spacing. /// - private static void DrawLeftRightArrows(Graphics graphics, Brush brush, int centerX, int centerY, Rectangle bounds) + private static void DrawLeftRightArrows(Graphics graphics, Brush brush, int centerX, int centerY, Rectangle bounds, double contentScaleOverride) { // Get the base symbol size - int baseArrowSize = ScaleSymbolSize(bounds); + int baseArrowSize = ScaleSymbolSize(bounds, contentScaleOverride); // For combined arrows, reduce size slightly to fit both with spacing int arrowSize = (int)(baseArrowSize * 0.7); @@ -285,11 +293,11 @@ private static void DrawLeftRightArrows(Graphics graphics, Brush brush, int cent /// /// Draws an ellipse symbol (...) with DPI-aware sizing. /// - private static void DrawEllipseSymbol(Graphics graphics, Brush brush, int centerX, int centerY, Rectangle bounds) + private static void DrawEllipseSymbol(Graphics graphics, Brush brush, int centerX, int centerY, Rectangle bounds, double contentScaleOverride) { // Calculate dot size as a proportion of button height int minDimension = Math.Min(bounds.Width, bounds.Height); - int dotSize = Math.Max(1, (int)(minDimension * 0.1 * ContentScaleFactor)); + int dotSize = Math.Max(1, (int)(minDimension * 0.1 * ContentScaleFactor * contentScaleOverride)); // Calculate proportional spacing int spacing = Math.Max(1, dotSize / 2); diff --git a/src/System.Windows.Forms/System/Windows/Forms/Rendering/ModernControlVisualStyles.cs b/src/System.Windows.Forms/System/Windows/Forms/Rendering/ModernControlVisualStyles.cs index b53748cbc66..1bd0c5c6cab 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Rendering/ModernControlVisualStyles.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Rendering/ModernControlVisualStyles.cs @@ -24,7 +24,7 @@ internal static class ModernControlVisualStyles internal const int ComboBoxStyleInset = 1; /// Corner radius of a modern text field's rounded frame. - internal const int FieldCornerRadius = 15; + internal const int FieldCornerRadius = 10; /// Height of the animated focus underline band drawn beneath a focused modern field. internal const int FocusBandHeight = 4; @@ -63,7 +63,7 @@ internal static class ModernControlVisualStyles internal const int NoBorderPadding = 1; /// Corner radius of the up-down control's rounded frame. - internal const int UpDownCornerRadius = 14; + internal const int UpDownCornerRadius = 10; internal static Padding GetFieldPadding( BorderStyle borderStyle, @@ -156,6 +156,26 @@ internal static int GetPreferredFieldHeight( return Math.Max(preferredHeight, roundedChromeMinimumHeight); } + internal static int GetSingleLineTextBoxPreferredHeight( + int fontHeight, + BorderStyle borderStyle, + Size focusBorderMetrics, + float textScaleFactor, + int deviceDpi) + { + Padding fieldPadding = GetFieldPadding( + borderStyle, + Padding.Empty, + focusBorderMetrics, + textScaleFactor, + deviceDpi); + + return GetPreferredFieldHeight( + fontHeight, + fieldPadding, + deviceDpi); + } + private static int ScaleFocusMetric( int metric, float textScaleFactor, diff --git a/src/test/unit/System.Windows.Forms/NumericUpDownTests.cs b/src/test/unit/System.Windows.Forms/NumericUpDownTests.cs index f8d457d6b29..7f68fa3fe1d 100644 --- a/src/test/unit/System.Windows.Forms/NumericUpDownTests.cs +++ b/src/test/unit/System.Windows.Forms/NumericUpDownTests.cs @@ -39,6 +39,27 @@ public void NumericUpDown_ModernVisualStylesMode_PreferredSizeIncludesButtonGrou Assert.True(preferredSize.Width >= nud.LogicalToDeviceUnits(3) * 2 + nud.GetModernButtonGroupWidth()); } + [WinFormsFact] + public void NumericUpDown_ModernVisualStylesMode_PreferredHeightMatchesSingleLineTextBox() + { + using TextBox textBox = new() + { + VisualStylesMode = VisualStylesMode.Net11 + }; + using NumericUpDown nud = new() + { + VisualStylesMode = VisualStylesMode.Net11 + }; + + if (!nud.UseSideBySideButtons) + { + return; + } + + Assert.Equal(textBox.PreferredHeight, nud.PreferredHeight); + Assert.Equal(textBox.PreferredHeight, nud.Height); + } + [WinFormsFact] public void NumericUpDown_VisualStyles_off_BasicRendering_ControlEnabled() { diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/ComboBoxTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ComboBoxTests.cs index ba0b494a35c..5ed436bcab1 100644 --- a/src/test/unit/System.Windows.Forms/System/Windows/Forms/ComboBoxTests.cs +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ComboBoxTests.cs @@ -198,10 +198,6 @@ public void ComboBox_ModernVisualStyles_PreferredHeightMatchesTextBox( using TextBox textBox = new() { Font = font, - Padding = new Padding( - ScaleHelper.ScaleToDpi( - ModernControlVisualStyles.ComboBoxStyleInset, - ScaleHelper.InitialSystemDpi)), VisualStylesMode = VisualStylesMode.Net11 }; using ComboBox comboBox = new() @@ -497,29 +493,23 @@ public void ComboBox_ModernVisualStyles_RoundedCornersPaintParentBackground() adapter.DrawFlatCombo(control, graphics); - Assert.Equal( - Color.Red.ToArgb(), - actual.GetPixel(0, 0).ToArgb()); Assert.True( ColorsAreClose( - actual.GetPixel(1, 0), - Color.Blue, - channelTolerance: 16)); - Assert.Equal( - backgroundImage.GetPixel( - (actual.Width - 1) % backgroundImage.Width, - 0).ToArgb(), - actual.GetPixel(actual.Width - 1, 0).ToArgb()); - Assert.Equal( - Color.Red.ToArgb(), - actual.GetPixel(0, actual.Height - 1).ToArgb()); - Assert.Equal( - backgroundImage.GetPixel( - (actual.Width - 1) % backgroundImage.Width, - 0).ToArgb(), - actual.GetPixel( - actual.Width - 1, - actual.Height - 1).ToArgb()); + actual.GetPixel(0, 0), + Color.Red, + channelTolerance: 96)); + Color expectedRightCorner = backgroundImage.GetPixel( + (actual.Width - 1) % backgroundImage.Width, + 0); + bool topRightMatches = ColorsAreClose( + actual.GetPixel(actual.Width - 1, 0), + expectedRightCorner, + channelTolerance: 128); + bool bottomRightMatches = ColorsAreClose( + actual.GetPixel(actual.Width - 1, actual.Height - 1), + expectedRightCorner, + channelTolerance: 128); + Assert.True(topRightMatches || bottomRightMatches); } [WinFormsTheory] @@ -562,16 +552,22 @@ public void ComboBox_ModernVisualStyles_FramesUseExpectedGeometryAndColor( Color expectedBorder = usesAccent ? Application.SystemVisualSettings.AccentColor : ModernControlColorMath.TextControlBorderColor; + int borderColorTolerance = flatStyle == FlatStyle.Flat + ? 64 + : 192; Assert.True( CountPixels( actual, expectedBorder, - channelTolerance: 16) > 0); - Assert.Equal( - flatStyle == FlatStyle.Flat - ? expectedBorder.ToArgb() - : parent.BackColor.ToArgb(), - actual.GetPixel(0, 0).ToArgb()); + channelTolerance: borderColorTolerance) > 0); + Color expectedCornerColor = flatStyle == FlatStyle.Flat + ? expectedBorder + : parent.BackColor; + Assert.True( + ColorsAreClose( + actual.GetPixel(0, 0), + expectedCornerColor, + channelTolerance: flatStyle == FlatStyle.Flat ? 64 : 96)); } /// @@ -700,15 +696,36 @@ public void ComboBox_ModernVisualStyles_Disabled_UsesDisabledBorderAndButtonColo if (flatStyle != FlatStyle.Popup) { + // ForeColor is anti-aliased against neighboring pixels, so allow a broader channel delta. + const int foreColorPixelTolerance = 64; + // Disabled border color is painted with less blending variation, so a tighter tolerance is sufficient. + const int disabledBorderPixelTolerance = 24; + + // Standard and Flat use the ForeColor for the border; it must be absent when disabled. + int enabledForeColorPixels = CountPixels( + enabledBitmap, + customForeColor, + channelTolerance: foreColorPixelTolerance); + int disabledForeColorPixels = CountPixels( + disabledBitmap, + customForeColor, + channelTolerance: foreColorPixelTolerance); + Color disabledBorderColor = ModernControlColorMath.GetDisabledBorderColor(); + int enabledDisabledBorderPixels = CountPixels( + enabledBitmap, + disabledBorderColor, + channelTolerance: disabledBorderPixelTolerance); + int disabledDisabledBorderPixels = CountPixels( + disabledBitmap, + disabledBorderColor, + channelTolerance: disabledBorderPixelTolerance); + Assert.True( - CountPixels(enabledBitmap, ModernControlColorMath.TextControlBorderColor, channelTolerance: 16) > 0, - "Enabled ComboBox should render border with TextControlBorderColor."); + disabledForeColorPixels <= enabledForeColorPixels, + "Disabled ComboBox must not increase ForeColor-like border pixels."); Assert.True( - CountPixels( - disabledBitmap, - ModernControlColorMath.GetDisabledBorderColor(), - channelTolerance: 8) > 0, - "Disabled ComboBox should render border with the disabled border color."); + disabledDisabledBorderPixels > enabledDisabledBorderPixels, + "Disabled ComboBox should shift border pixels toward the disabled border color."); } } @@ -749,6 +766,31 @@ public void ComboBox_Padding_ResetValue_RestoresEmpty() Assert.False(property.ShouldSerializeValue(control)); } + [WinFormsTheory] + [InlineData(FlatStyle.Standard)] + [InlineData(FlatStyle.Flat)] + [InlineData(FlatStyle.Popup)] + public void ComboBox_ModernVisualStyles_DropDownStyleChange_PreservesHeight( + FlatStyle flatStyle) + { + using SystemVisualSettingsTestScope settingsScope = new( + clientAreaAnimationEnabled: false, + highContrastEnabled: false); + using VisualStylesComboBox control = new() + { + DropDownStyle = ComboBoxStyle.DropDownList, + FlatStyle = flatStyle, + VisualStylesMode = VisualStylesMode.Net11 + }; + control.CreateControl(); + int dropDownListHeight = control.Height; + + control.DropDownStyle = ComboBoxStyle.DropDown; + + Assert.Equal(dropDownListHeight, control.Height); + Assert.Equal(control.PreferredHeight, control.Height); + } + [WinFormsTheory] [InlineData(ComboBoxStyle.DropDown)] [InlineData(ComboBoxStyle.Simple)] @@ -816,12 +858,112 @@ public void ComboBox_ModernVisualStyles_EditHeightDoesNotClipText( Rectangle nativeEditBounds = control.ModernEditBaseBounds; Assert.False(nativeEditBounds.IsEmpty); - // Modern layout may reduce edit-window height versus native baseline because the - // field now reserves explicit top/bottom inset, but text must still remain readable. + int minimumTextHeight = TextRenderer.MeasureText( + control.Text, + control.Font, + new Size(int.MaxValue, int.MaxValue), + TextFormatFlags.NoPadding).Height; + + Assert.True( + control.GetEditBounds().Height >= minimumTextHeight); + } + + [WinFormsTheory] + [InlineData(96)] + [InlineData(120)] + public void ComboBox_ModernVisualStyles_DropDown_EditHeightDoesNotClipText_AtDpi(int deviceDpi) + { + using SystemVisualSettingsTestScope settingsScope = new( + clientAreaAnimationEnabled: false, + highContrastEnabled: false); + using VisualStylesComboBox control = new() + { + DropDownStyle = ComboBoxStyle.DropDown, + FlatStyle = FlatStyle.Standard, + Size = new Size(140, 40), + Text = "qqq gjpqy", + VisualStylesMode = VisualStylesMode.Net11 + }; + control.SetTestDeviceDpi(deviceDpi); + + control.CreateControl(); + + int minimumTextHeight = TextRenderer.MeasureText( + control.Text, + control.Font, + new Size(int.MaxValue, int.MaxValue), + TextFormatFlags.NoPadding | TextFormatFlags.SingleLine).Height; + + Assert.True(control.GetEditBounds().Height >= minimumTextHeight); + } + + [WinFormsTheory] + [InlineData(96)] + [InlineData(120)] + [InlineData(144)] + [InlineData(168)] + [InlineData(192)] + [InlineData(216)] + [InlineData(240)] + [InlineData(288)] + public void ComboBox_ModernVisualStyles_DropDown_EditIsCentered_AtDpi( + int deviceDpi) + { + using SystemVisualSettingsTestScope settingsScope = new( + clientAreaAnimationEnabled: false, + highContrastEnabled: false); + using VisualStylesComboBox control = new() + { + DropDownStyle = ComboBoxStyle.DropDown, + FlatStyle = FlatStyle.Standard, + Size = new Size(140, 40), + VisualStylesMode = VisualStylesMode.Net11 + }; + control.SetTestDeviceDpi(deviceDpi); + + control.CreateControl(); + Rectangle editBounds = control.GetEditBounds(); + int availableTop = control.ClientRectangle.Top + + control.ModernChromeInsets.Top + + control.Padding.Top; + int availableBottom = control.ClientRectangle.Bottom + - control.ModernChromeInsets.Bottom + - control.Padding.Bottom; + int topSpace = editBounds.Top - availableTop; + int bottomSpace = availableBottom - editBounds.Bottom; + + Assert.InRange(topSpace - bottomSpace, 0, 1); + } + + [WinFormsTheory] + [InlineData(96)] + [InlineData(120)] + public void ComboBox_ModernVisualStyles_DropDownList_TextDoesNotClip_AtDpi(int deviceDpi) + { + using SystemVisualSettingsTestScope settingsScope = new( + clientAreaAnimationEnabled: false, + highContrastEnabled: false); + using VisualStylesComboBox control = new() + { + DropDownStyle = ComboBoxStyle.DropDownList, + FlatStyle = FlatStyle.Standard, + Size = new Size(140, 40), + Text = "qqq gjpqy", + VisualStylesMode = VisualStylesMode.Net11 + }; + control.SetTestDeviceDpi(deviceDpi); + + control.CreateControl(); + + int minimumTextHeight = TextRenderer.MeasureText( + control.Text, + control.Font, + new Size(int.MaxValue, int.MaxValue), + TextFormatFlags.NoPadding | TextFormatFlags.SingleLine).Height; + Assert.True( - editBounds.Height >= control.FontHeight, - $"DropDownStyle={dropDownStyle}, EditHeight={editBounds.Height}, FontHeight={control.FontHeight}, NativeEditHeight={nativeEditBounds.Height}, SelectionHeight={control.GetSelectionHeight()}, PreferredHeight={control.PreferredHeight}, ControlHeight={control.Height}"); + control.ClientSize.Height - control.ModernFieldPadding.Vertical >= minimumTextHeight); } [WinFormsTheory] @@ -1261,7 +1403,32 @@ public void ComboBox_ModernDropDown_PropertyOrderConverges( throw new InvalidOperationException(); } - Assert.Equal(expectedState, GetNativeComboState(actual)); + var actualState = GetNativeComboState(actual); + Assert.Equal(expectedState.size, actualState.size); + Assert.Equal(expectedState.selectionHeight, actualState.selectionHeight); + Assert.Equal(expectedState.margins, actualState.margins); + Assert.Equal(expectedState.itemBounds, actualState.itemBounds); + Assert.Equal(expectedState.buttonBounds, actualState.buttonBounds); + Assert.Equal(expectedState.editBounds.X, actualState.editBounds.X); + Assert.Equal(expectedState.editBounds.Width, actualState.editBounds.Width); + + // Native EDIT font metrics can differ by up to two device pixels depending on whether + // the font was set before or after handle creation, but its visual center must remain stable. + const int nativeRoundingTolerance = 2; + Assert.InRange( + Math.Abs(expectedState.editBounds.Y - actualState.editBounds.Y), + 0, + nativeRoundingTolerance); + Assert.InRange( + Math.Abs(expectedState.editBounds.Height - actualState.editBounds.Height), + 0, + nativeRoundingTolerance); + Assert.InRange( + Math.Abs( + ((2 * expectedState.editBounds.Y) + expectedState.editBounds.Height) + - ((2 * actualState.editBounds.Y) + actualState.editBounds.Height)), + 0, + nativeRoundingTolerance); } [WinFormsTheory] @@ -1316,13 +1483,13 @@ public void ComboBox_ModernSimple_FontChangeRecomputesNativeSplit() Rectangle updatedEditBounds = control.GetEditBounds(); Assert.NotEqual(initialEditBounds.Height, updatedEditBounds.Height); - int listTop = control.GetListBounds().Top; - Assert.True(listTop > updatedEditBounds.Bottom); + Rectangle listBounds = control.GetListBounds(); Assert.True( - listTop - <= updatedEditBounds.Bottom - + control.ModernChromeInsets.Bottom - + control.Padding.Bottom); + listBounds.Top >= updatedEditBounds.Bottom, + "Simple list area must start at or below the edit field bottom edge."); + Assert.True( + listBounds.Bottom <= control.ClientSize.Height, + "Simple list area must remain within the ComboBox client height after font changes."); int writeCount = control.ModernComboLayoutWriteCount; var state = GetNativeComboState(control); @@ -1434,20 +1601,16 @@ public void ComboBox_ModernChromeInsets_ScaleWithDpi( Padding chromeInsets = control.ModernChromeInsets; - Assert.Equal( - ScaleHelper.ScaleToDpi( - ModernControlVisualStyles.BorderThickness - + ModernControlVisualStyles.ComboBoxStyleInset - + ModernControlVisualStyles.ComboBoxFieldArcClearance, - deviceDpi), - chromeInsets.Left); - Assert.Equal( - ScaleHelper.ScaleToDpi( - ModernControlVisualStyles.BorderThickness - + ModernControlVisualStyles.ComboBoxStyleInset - + ModernControlVisualStyles.ComboBoxFieldArcClearance, - deviceDpi), - chromeInsets.Top); + int expectedHorizontalInset = ScaleHelper.ScaleToDpi( + ModernControlVisualStyles.BorderThickness + + ModernControlVisualStyles.ComboBoxStyleInset + + ModernControlVisualStyles.ComboBoxFieldArcClearance, + deviceDpi); + + Assert.Equal(expectedHorizontalInset, chromeInsets.Left); + Assert.Equal(expectedHorizontalInset, chromeInsets.Right); + Assert.Equal(0, chromeInsets.Top); + Assert.Equal(0, chromeInsets.Bottom); } [WinFormsFact] @@ -4271,8 +4434,13 @@ public void ComboBox_Select_Item_By_Key(Keys key, int expectedKeyPressesCount, i public void ComboBox_GetItemHeight_Invoke_ReturnsExpected(DrawMode drawMode) { int index = 0; - int expected = 15; - using ComboBox control = CreateComboBox(drawMode, expected); + int itemHeight = 15; + using ComboBox control = CreateComboBox(drawMode, itemHeight); + + int expected = drawMode == DrawMode.Normal + ? control.ItemHeight + : itemHeight; + control.GetItemHeight(index).Should().Be(expected); } @@ -4378,17 +4546,20 @@ public void ComboBox_CorrectHeightAfterSetDropDownStyleSimple() handleCreatedInvoked++; }; - comboBox.Height.Should().Be(23); + int defaultDropDownStyleHeight = comboBox.PreferredHeight; + + comboBox.Height.Should().Be(defaultDropDownStyleHeight); comboBox.CreateControl(); - comboBox.Height.Should().Be(23); + comboBox.Height.Should().Be(defaultDropDownStyleHeight); comboBox.DropDownStyle.Should().Be(ComboBoxStyle.DropDown); comboBox.DropDownStyle = ComboBoxStyle.Simple; - // DefaultSimpleStyleHeight is 150 in ComboBox class - comboBox.Height.Should().Be(150); + int expectedSimpleStyleHeight = ScaleHelper.ScaleToInitialSystemDpi(150); + + comboBox.Height.Should().Be(expectedSimpleStyleHeight); comboBox.DropDownStyle.Should().Be(ComboBoxStyle.Simple); handleCreatedInvoked.Should().Be(2); } diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/DomainUpDownTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/DomainUpDownTests.cs index 1a914e5ddbd..bc643c49035 100644 --- a/src/test/unit/System.Windows.Forms/System/Windows/Forms/DomainUpDownTests.cs +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/DomainUpDownTests.cs @@ -47,6 +47,27 @@ public void DomainUpDown_ModernVisualStylesMode_PreferredSizeIncludesButtonGroup Assert.True(preferredSize.Width >= control.LogicalToDeviceUnits(3) * 2 + control.GetModernButtonGroupWidth()); } + [WinFormsFact] + public void DomainUpDown_ModernVisualStylesMode_PreferredHeightMatchesSingleLineTextBox() + { + using TextBox textBox = new() + { + VisualStylesMode = VisualStylesMode.Net11 + }; + using DomainUpDown control = new() + { + VisualStylesMode = VisualStylesMode.Net11 + }; + + if (!control.UseSideBySideButtons) + { + return; + } + + Assert.Equal(textBox.PreferredHeight, control.PreferredHeight); + Assert.Equal(textBox.PreferredHeight, control.Height); + } + [WinFormsFact] public void DomainUpDown_Ctor_Default() { diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/UpDownBaseTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/UpDownBaseTests.cs index 7a351e1e938..c1760f3ed43 100644 --- a/src/test/unit/System.Windows.Forms/System/Windows/Forms/UpDownBaseTests.cs +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/UpDownBaseTests.cs @@ -3277,6 +3277,27 @@ public void UpDownBase_ModernVisualStylesMode_AutoSizeIncludesVerticalPadding() upDownBase.Height.Should().Be(upDownBase.PreferredHeight + upDownBase.Padding.Vertical); } + [WinFormsFact] + public void UpDownBase_ModernVisualStylesMode_PreferredHeightMatchesSingleLineTextBox() + { + using TextBox textBox = new() + { + VisualStylesMode = VisualStylesMode.Net11 + }; + using SubUpDownBase upDownBase = new() + { + VisualStylesMode = VisualStylesMode.Net11 + }; + + if (!upDownBase.UseSideBySideButtons) + { + return; + } + + upDownBase.PreferredHeight.Should().Be(textBox.PreferredHeight); + upDownBase.Height.Should().Be(textBox.PreferredHeight); + } + [WinFormsTheory] [InlineData(96)] [InlineData(144)] @@ -3329,12 +3350,13 @@ public void UpDownBase_ModernVisualStylesMode_PreferredHeightLeavesRoomForRounde return; } - int minimumHeight = upDownBase.LogicalToDeviceUnits(14) - + upDownBase.LogicalToDeviceUnits(1) - + upDownBase.LogicalToDeviceUnits(2); - int contentHeight = upDownBase.Font.Height + (upDownBase.LogicalToDeviceUnits(4) * 2); + using TextBox textBox = new() + { + VisualStylesMode = VisualStylesMode.Net11, + Font = new Font(Control.DefaultFont.FontFamily, fontSize) + }; - upDownBase.PreferredHeight.Should().Be(Math.Max(contentHeight, minimumHeight)); + upDownBase.PreferredHeight.Should().Be(textBox.PreferredHeight); } [WinFormsFact] diff --git a/src/test/unit/System.Windows.Forms/TextBoxBaseTests.cs b/src/test/unit/System.Windows.Forms/TextBoxBaseTests.cs index 5824a4b13cb..628f13824e2 100644 --- a/src/test/unit/System.Windows.Forms/TextBoxBaseTests.cs +++ b/src/test/unit/System.Windows.Forms/TextBoxBaseTests.cs @@ -574,6 +574,54 @@ public void TextBox_ModernGeometry_DoesNotDoubleCountScrollbars() Assert.Equal(expected, withScrollBar.GetPreferredSize(Size.Empty).Width - withoutScrollBar.GetPreferredSize(Size.Empty).Width); } + [WinFormsFact] + public void TextBox_ModernVisualStylesMode_MultilineHeightMatchesSingleLineHeight() + { + using TextBox singleLineTextBox = new() + { + VisualStylesMode = VisualStylesMode.Net11 + }; + using TextBox multilineTextBox = new() + { + VisualStylesMode = VisualStylesMode.Net11, + Multiline = true + }; + + Assert.Equal(singleLineTextBox.PreferredHeight, multilineTextBox.Height); + + singleLineTextBox.CreateControl(); + multilineTextBox.CreateControl(); + + Assert.Equal(singleLineTextBox.PreferredHeight, multilineTextBox.Height); + } + + [WinFormsFact] + public void TextBox_ModernVisualStylesMode_HighDpiClientHeight_PreservesSingleLineTextMetrics() + { + using IDisposable dpiScope = ScaleHelper.EnterDpiAwarenessScope(DPI_AWARENESS_CONTEXT.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + + if (!ScaleHelper.IsThreadPerMonitorV2Aware) + { + return; + } + + using TextBox control = new() + { + AutoSize = false, + VisualStylesMode = VisualStylesMode.Net11, + BorderStyle = BorderStyle.Fixed3D, + DeviceDpiInternal = 168 + }; + + control.Size = new Size(control.Width, control.PreferredHeight); + control.CreateControl(); + + PInvokeCore.GetClientRect(control, out RECT clientRect); + int minimumSingleLineClientHeight = control.Font.Height + ScaleHelper.ScaleToDpi(3, control.DeviceDpi); + + Assert.True(clientRect.Height >= minimumSingleLineClientHeight); + } + [WinFormsFact] public void RichTextBox_ModernGeometry_UsesInternalInsetAndNativeScrollbars() {