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 a700347b0c4..fb6292663ce 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 @@ -14,6 +14,10 @@ public partial class ComboBox private bool _nativeComboHandleInitialized; private bool _normalizingNativeComboBaseline; private int _modernComboLayoutWriteCount; + private HWND _modernSimpleListClipRegionHandle; + private Size _modernSimpleListClipRegionSize; + private int _modernSimpleListClipRegionApplyCount; + private const int ModernSimpleBottomCropLogicalPixels = 2; internal bool UsesModernComboAdapter => EffectiveVisualStylesMode >= VisualStylesMode.Net11 @@ -165,17 +169,6 @@ private ModernComboTargetState ComputeModernComboTargetState() { int topInset = chromeInsets.Top + Padding.Top; int bottomInset = chromeInsets.Bottom + Padding.Bottom; - 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, - ClientRectangle.Bottom - - bottomInset - - 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. @@ -187,30 +180,104 @@ private ModernComboTargetState ComputeModernComboTargetState() ModernControlVisualStyles.ComboBoxButtonExtraWidth, DeviceDpiInternal); - if (RightToLeft == RightToLeft.Yes) + if (DropDownStyle == ComboBoxStyle.Simple) { - // In RTL the drop-down button sits on the left, so its reservation goes there. - editBounds.X += rightInset + extraButtonWidth; + int dividerThickness = GetModernSimpleDividerThickness(); + int simpleBottomShrink = ScaleHelper.ScaleToDpi( + ModernSimpleBottomCropLogicalPixels, + DeviceDpiInternal); + int simpleListBottom = ClientRectangle.Bottom - simpleBottomShrink - dividerThickness; + int simpleEditTop = topInset + ScaleHelper.ScaleToDpi(1, DeviceDpiInternal); + int selectionFieldHeight = Math.Max( + 1, + (int)PInvokeCore.SendMessage( + this, + PInvoke.CB_GETITEMHEIGHT, + (WPARAM)(-1))); + int minimumReadableHeight = Math.Max( + 1, + FontHeight + ScaleHelper.ScaleToDpi(2, DeviceDpiInternal)); + int preferredSimpleEditHeight = Math.Max(selectionFieldHeight, minimumReadableHeight) + + ScaleHelper.ScaleToDpi(2, DeviceDpiInternal); + int maxSimpleEditHeight = Math.Max( + 1, + simpleListBottom - dividerThickness - simpleEditTop); + + // Keep the editor in the top field lane with enough height for full glyph rendering, + // draw an accent divider below it, and let the permanent list consume the remaining + // space down to the rounded field border. + editBounds.Y = simpleEditTop; + editBounds.Height = Math.Min(preferredSimpleEditHeight, maxSimpleEditHeight); + editBounds.X = leftInset; + editBounds.Width = Math.Max( + 1, + ClientRectangle.Width - leftInset - rightInset); + + if (!simpleListBounds.IsEmpty) + { + simpleListBounds.X = editBounds.Left; + simpleListBounds.Width = editBounds.Width; + simpleListBounds.Y = editBounds.Bottom + dividerThickness; + simpleListBounds.Height = Math.Max( + 1, + simpleListBottom - simpleListBounds.Y); + } } else { - editBounds.X += leftInset; - } + // Keep enough vertical room for glyphs in editable DropDown mode: when chrome + // insets become large at some DPIs/styles, shrinking both top and bottom can make + // the native edit window shorter than the rendered text box line height. + int minimumReadableHeight = Math.Max(1, FontHeight); + int totalVerticalInset = topInset + bottomInset; + int availableHeight = Math.Max(1, editBounds.Height - totalVerticalInset); + + if (availableHeight < minimumReadableHeight) + { + int required = minimumReadableHeight - availableHeight; + int topReduction = Math.Min(topInset, (required + 1) / 2); + topInset -= topReduction; + required -= topReduction; + bottomInset -= Math.Min(bottomInset, required); + } + + editBounds.Y += topInset; - editBounds.Width = Math.Max( - 1, - editBounds.Width - leftInset - rightInset - extraButtonWidth); - - if (!simpleListBounds.IsEmpty) - { - int simpleListBottom = GetCurrentSimpleListBottom( - simpleListBounds.Bottom); - - simpleListBounds.Y = editBounds.Bottom + bottomInset; + editBounds.Height = Math.Max( + 1, + editBounds.Height - topInset - bottomInset); - simpleListBounds.Height = Math.Max( + editBounds.Height = Math.Max( + minimumReadableHeight, + Math.Min( + editBounds.Height, + ClientRectangle.Bottom + - bottomInset + - editBounds.Top)); + + if (RightToLeft == RightToLeft.Yes) + { + // In RTL the drop-down button sits on the left, so its reservation goes there. + editBounds.X += rightInset + extraButtonWidth; + } + else + { + editBounds.X += leftInset; + } + + editBounds.Width = Math.Max( 1, - simpleListBottom - simpleListBounds.Y); + editBounds.Width - leftInset - rightInset - extraButtonWidth); + + if (!simpleListBounds.IsEmpty) + { + int simpleListBottom = GetCurrentSimpleListBottom( + simpleListBounds.Bottom); + simpleListBounds.Y = editBounds.Bottom + bottomInset; + simpleListBounds.Height = Math.Max( + 1, + simpleListBottom - simpleListBounds.Y); + } } } @@ -279,6 +346,60 @@ private void GetAdjustedNativeChildBounds( } } + private void ConfigureModernSimpleListSurface(HWND listHandle) + { + if (DropDownStyle != ComboBoxStyle.Simple + || !UsesModernComboAdapter + || listHandle.IsNull) + { + return; + } + + WINDOW_STYLE style = (WINDOW_STYLE)PInvokeCore.GetWindowLong( + listHandle, + WINDOW_LONG_PTR_INDEX.GWL_STYLE); + WINDOW_EX_STYLE exStyle = (WINDOW_EX_STYLE)PInvokeCore.GetWindowLong( + listHandle, + WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); + + WINDOW_STYLE updatedStyle = style & ~WINDOW_STYLE.WS_BORDER; + WINDOW_EX_STYLE updatedExStyle = exStyle & ~WINDOW_EX_STYLE.WS_EX_CLIENTEDGE; + + if (updatedStyle == style && updatedExStyle == exStyle) + { + return; + } + + if (updatedStyle != style) + { + PInvokeCore.SetWindowLong( + listHandle, + WINDOW_LONG_PTR_INDEX.GWL_STYLE, + (nint)updatedStyle); + } + + if (updatedExStyle != exStyle) + { + PInvokeCore.SetWindowLong( + listHandle, + WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, + (nint)updatedExStyle); + } + + PInvoke.SetWindowPos( + listHandle, + HWND.Null, + 0, + 0, + 0, + 0, + SET_WINDOW_POS_FLAGS.SWP_NOMOVE + | SET_WINDOW_POS_FLAGS.SWP_NOSIZE + | SET_WINDOW_POS_FLAGS.SWP_NOZORDER + | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE + | SET_WINDOW_POS_FLAGS.SWP_FRAMECHANGED); + } + private unsafe int GetCurrentSimpleListBottom( int fallback) { @@ -364,6 +485,8 @@ private unsafe void ApplyModernComboLayout() return; } + ConfigureModernSimpleListSurface(comboBoxInfo.hwndList); + if (!target.EditBounds.IsEmpty) { ApplyEditMargins( @@ -462,26 +585,83 @@ private void ApplyEditMargins( private void ApplyChildBounds(HWND childHandle, Rectangle targetBounds) { + bool isModernSimpleList = UsesModernComboAdapter + && DropDownStyle == ComboBoxStyle.Simple + && childHandle == _childListBox?.HWND; + if (childHandle.IsNull - || targetBounds.IsEmpty - || GetChildBounds(childHandle) == targetBounds) + || targetBounds.IsEmpty) { return; } - PInvoke.SetWindowPos( - childHandle, - HWND.Null, - targetBounds.Left, - targetBounds.Top, + if (GetChildBounds(childHandle) != targetBounds) + { + PInvoke.SetWindowPos( + childHandle, + HWND.Null, + targetBounds.Left, + targetBounds.Top, + targetBounds.Width, + targetBounds.Height, + SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE + | SET_WINDOW_POS_FLAGS.SWP_NOZORDER); + + _modernComboLayoutWriteCount++; + } + + if (isModernSimpleList) + { + ConfigureModernSimpleListClipRegion( + childHandle, + targetBounds); + } + } + + private void ConfigureModernSimpleListClipRegion( + HWND listHandle, + Rectangle targetBounds) + { + if (listHandle.IsNull) + { + return; + } + + Size targetRegionSize = new( targetBounds.Width, - targetBounds.Height, - SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE - | SET_WINDOW_POS_FLAGS.SWP_NOZORDER); + Math.Max(1, targetBounds.Height)); - _modernComboLayoutWriteCount++; + if (listHandle == _modernSimpleListClipRegionHandle + && targetRegionSize == _modernSimpleListClipRegionSize) + { + return; + } + + using RegionScope region = new( + 0, + 0, + targetRegionSize.Width, + targetRegionSize.Height); + + if (PInvoke.SetWindowRgn( + listHandle, + region, + fRedraw: true) != 0) + { + region.RelinquishOwnership(); + _modernSimpleListClipRegionHandle = listHandle; + _modernSimpleListClipRegionSize = targetRegionSize; + _modernSimpleListClipRegionApplyCount++; + } } + private int GetModernSimpleDividerThickness() + => Math.Max( + 1, + ScaleHelper.ScaleToDpi( + ModernControlVisualStyles.BorderThickness, + DeviceDpiInternal)); + private Rectangle GetChildBounds(HWND child) { PInvokeCore.GetWindowRect(child, out RECT bounds); @@ -524,6 +704,9 @@ private int GetNativeComboBaselineSelectionFieldItemHeight() private int GetModernComboLayoutWriteCount() => _modernComboLayoutWriteCount; + private int GetModernSimpleListClipRegionApplyCount() + => _modernSimpleListClipRegionApplyCount; + private Padding GetModernFieldPadding() { SystemVisualSettings settings = Application.SystemVisualSettings; 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 3665e8a1275..9e70559e783 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 @@ -92,6 +92,7 @@ public override void DrawFlatCombo( graphics, clientBounds); DrawDropDownButton(comboBox, graphics); + DrawSimpleEditListDivider(comboBox, graphics); switch (_flatStyle) { @@ -389,6 +390,45 @@ private static void CutOutRoundedCorners( parentColor); } + private static void DrawSimpleEditListDivider( + ComboBox comboBox, + Graphics graphics) + { + if (comboBox.DropDownStyle != ComboBoxStyle.Simple + || comboBox._childListBox is null) + { + return; + } + + Rectangle listBounds = comboBox.GetChildBounds(comboBox._childListBox.HWND); + if (listBounds.IsEmpty) + { + return; + } + + int dividerThickness = Math.Max( + 1, + ScaleHelper.ScaleToDpi( + ModernControlVisualStyles.BorderThickness, + comboBox.DeviceDpiInternal)); + int lineY = listBounds.Top - dividerThickness; + if (lineY < 0) + { + return; + } + + Padding chromeInsets = comboBox.GetModernChromeInsets(); + int left = chromeInsets.Left + comboBox.Padding.Left; + int right = comboBox.ClientRectangle.Right - chromeInsets.Right - comboBox.Padding.Right - 1; + if (right < left) + { + return; + } + + using var pen = Application.SystemVisualSettings.AccentColor.GetCachedPenScope(dividerThickness); + graphics.DrawLine(pen, left, lineY, right, lineY); + } + private static void DrawRoundedBorder( ComboBox comboBox, Graphics graphics, diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.cs index fca5b5c33ef..697f4e530d1 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.cs @@ -355,7 +355,9 @@ protected override CreateParams CreateParams cp.Style |= (int)WINDOW_STYLE.WS_VSCROLL | PInvoke.CBS_HASSTRINGS | PInvoke.CBS_AUTOHSCROLL; cp.ExStyle |= (int)WINDOW_EX_STYLE.WS_EX_CLIENTEDGE; - if (!_integralHeight) + if (!_integralHeight + || (UsesModernComboAdapter + && DropDownStyle == ComboBoxStyle.Simple)) { cp.Style |= PInvoke.CBS_NOINTEGRALHEIGHT; } @@ -491,7 +493,7 @@ public int DropDownHeight // The dropDownHeight is not reflected unless the // ComboBox integralHeight == false.. - IntegralHeight = false; + SetIntegralHeightCore(value: false); } } } @@ -613,13 +615,15 @@ public override Color ForeColor public bool IntegralHeight { get => _integralHeight; - set + set => SetIntegralHeightCore(value); + } + + private void SetIntegralHeightCore(bool value) + { + if (_integralHeight != value) { - if (_integralHeight != value) - { - _integralHeight = value; - RecreateHandle(); - } + _integralHeight = value; + RecreateHandle(); } } @@ -2421,6 +2425,12 @@ protected override unsafe void OnHandleCreated(EventArgs e) bool hasComboBoxInfo = PInvoke.GetComboBoxInfo( HWND, ref comboBoxInfo); + + if (hasComboBoxInfo) + { + ConfigureModernSimpleListSurface(comboBoxInfo.hwndList); + } + if (Application.IsDarkModeEnabled) { // Style the ComboBox Open-Button: @@ -2479,6 +2489,9 @@ protected override void OnHandleDestroyed(EventArgs e) _nativeComboHandleInitialized = false; _normalizingNativeComboBaseline = false; _modernComboLayoutWriteCount = 0; + _modernSimpleListClipRegionHandle = HWND.Null; + _modernSimpleListClipRegionSize = Size.Empty; + _modernSimpleListClipRegionApplyCount = 0; _dropDownHandle = HWND.Null; if (Disposing) { @@ -3738,14 +3751,11 @@ protected override unsafe void WndProc(ref Message m) { switch (m.MsgInternal) { - // Modern VisualStyles: expand the client area to the full window so the drop-down - // button, which the themed ComboBox otherwise reserves as non-client (outside - // ClientRectangle), becomes part of the client and is covered by our rounded field. - // Simple combos have no drop-down button (their client already spans the full width - // and hosts a permanent list), so they must never be expanded. + // Modern VisualStyles: expand the client area to the full window so themed non-client + // reservations (for example the ComboBox-hosted scroll strip in Simple mode) are folded + // into client painting and can be normalized by modern layout. case PInvokeCore.WM_NCCALCSIZE: if (UsesModernComboAdapter - && DropDownStyle != ComboBoxStyle.Simple && m.WParamInternal != 0u) { RECT* ncRects = (RECT*)(nint)m.LParamInternal; @@ -3758,11 +3768,11 @@ protected override unsafe void WndProc(ref Message m) base.WndProc(ref m); break; - // Modern VisualStyles: comctl32 still reports the drop-down button region as a - // non-client hit (HTVSCROLL) even though WM_NCCALCSIZE folded it into our client - // area, so a button click would arrive as WM_NCLBUTTONDOWN and never reach the - // WM_LBUTTONDOWN hit-test below. Force HTCLIENT across the expanded client so the - // button click is delivered as a normal client message we can act on. + // Modern VisualStyles: after WM_NCCALCSIZE expands the client area, comctl32 can still + // report the folded strip as non-client (HTVSCROLL). For editable/drop-down-list combos, + // force HTCLIENT across the expanded client so interaction remains consistent with modern + // layout. Keep native hit-testing for Simple to avoid interfering with the hosted list's + // always-visible scrollbar interactions. case PInvokeCore.WM_NCHITTEST: base.WndProc(ref m); if (UsesModernComboAdapter 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 145c80a4d3d..680565c5d89 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 @@ -820,8 +820,12 @@ 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. + Rectangle editBounds = control.GetEditBounds(); Assert.True( - control.GetEditBounds().Height >= nativeEditBounds.Height); + editBounds.Height >= control.FontHeight, + $"DropDownStyle={dropDownStyle}, EditHeight={editBounds.Height}, FontHeight={control.FontHeight}, NativeEditHeight={nativeEditBounds.Height}, SelectionHeight={control.GetSelectionHeight()}, PreferredHeight={control.PreferredHeight}, ControlHeight={control.Height}"); } [WinFormsTheory] @@ -854,17 +858,20 @@ public void ComboBox_ModernPadding_PositionsEditUsingTopAndBottom( Assert.True( bottomWeightedBounds.Top > topWeightedBounds.Top); - Assert.Equal( - topWeightedBounds.Height, - bottomWeightedBounds.Height); if (dropDownStyle == ComboBoxStyle.Simple) { + Assert.True(bottomWeightedBounds.Height >= control.FontHeight); Rectangle listBounds = control.GetListBounds(); - Assert.Equal( - bottomWeightedBounds.Bottom - + control.ModernChromeInsets.Bottom - + control.Padding.Bottom, - listBounds.Top); + Assert.True( + listBounds.Top + > bottomWeightedBounds.Bottom); + } + else + { + Assert.InRange( + bottomWeightedBounds.Height, + topWeightedBounds.Height - 2, + topWeightedBounds.Height + 2); } } @@ -1071,11 +1078,14 @@ public void ComboBox_ModernSimple_ResizeRefreshesListBounds() Assert.True( resizedListBounds.Bottom > initialListBounds.Bottom); - Assert.Equal( - control.GetEditBounds().Bottom - + control.ModernChromeInsets.Bottom - + control.Padding.Bottom, - resizedListBounds.Top); + Assert.True( + resizedListBounds.Top + > control.GetEditBounds().Bottom); + Assert.True( + resizedListBounds.Top + <= control.GetEditBounds().Bottom + + control.ModernChromeInsets.Bottom + + control.Padding.Bottom); } [WinFormsFact] @@ -1310,11 +1320,13 @@ public void ComboBox_ModernSimple_FontChangeRecomputesNativeSplit() Rectangle updatedEditBounds = control.GetEditBounds(); Assert.NotEqual(initialEditBounds.Height, updatedEditBounds.Height); - Assert.Equal( - updatedEditBounds.Bottom - + control.ModernChromeInsets.Bottom - + control.Padding.Bottom, - control.GetListBounds().Top); + int listTop = control.GetListBounds().Top; + Assert.True(listTop > updatedEditBounds.Bottom); + Assert.True( + listTop + <= updatedEditBounds.Bottom + + control.ModernChromeInsets.Bottom + + control.Padding.Bottom); int writeCount = control.ModernComboLayoutWriteCount; var state = GetNativeComboState(control); @@ -1442,6 +1454,178 @@ public void ComboBox_ModernChromeInsets_ScaleWithDpi( chromeInsets.Top); } + [WinFormsFact] + public void ComboBox_ModernSimple_EditFillsAvailableFieldWidth() + { + using SystemVisualSettingsTestScope settingsScope = new( + clientAreaAnimationEnabled: false, + highContrastEnabled: false); + using VisualStylesComboBox control = new() + { + DropDownStyle = ComboBoxStyle.Simple, + FlatStyle = FlatStyle.Standard, + Padding = Padding.Empty, + Size = new Size(180, 120), + VisualStylesMode = VisualStylesMode.Net11 + }; + control.CreateControl(); + + Rectangle editBounds = control.GetEditBounds(); + Padding chromeInsets = control.ModernChromeInsets; + int expectedLeft = chromeInsets.Left; + int expectedRight = control.ClientRectangle.Right - chromeInsets.Right; + + Assert.InRange(editBounds.Left, expectedLeft - 1, expectedLeft + 1); + Assert.InRange(editBounds.Right, expectedRight - 1, expectedRight + 1); + } + + [WinFormsFact] + public void ComboBox_ModernSimple_ListFillsAvailableFieldWidth() + { + using SystemVisualSettingsTestScope settingsScope = new( + clientAreaAnimationEnabled: false, + highContrastEnabled: false); + using VisualStylesComboBox control = new() + { + DropDownStyle = ComboBoxStyle.Simple, + FlatStyle = FlatStyle.Standard, + Padding = Padding.Empty, + Size = new Size(180, 120), + VisualStylesMode = VisualStylesMode.Net11 + }; + control.Items.AddRange(["one", "two", "three", "four", "five", "six", "seven", "eight"]); + control.CreateControl(); + + Rectangle listBounds = control.GetListBounds(); + Padding chromeInsets = control.ModernChromeInsets; + int expectedLeft = chromeInsets.Left; + int expectedRight = control.ClientRectangle.Right - chromeInsets.Right; + + Assert.True(listBounds.Left <= expectedLeft); + Assert.True(listBounds.Right >= expectedRight - 1); + } + + [WinFormsFact] + public void ComboBox_ModernSimple_EditAndList_DoNotOverlapAndFitFont() + { + using SystemVisualSettingsTestScope settingsScope = new( + clientAreaAnimationEnabled: false, + highContrastEnabled: false); + using Font font = new(Control.DefaultFont.FontFamily, 14f); + using VisualStylesComboBox control = new() + { + DropDownStyle = ComboBoxStyle.Simple, + FlatStyle = FlatStyle.Standard, + Font = font, + Padding = Padding.Empty, + Size = new Size(220, 140), + VisualStylesMode = VisualStylesMode.Net11 + }; + control.Items.AddRange(["one", "two", "three", "four", "five", "six", "seven", "eight"]); + control.CreateControl(); + + Rectangle editBounds = control.GetEditBounds(); + Rectangle listBounds = control.GetListBounds(); + + Assert.True(editBounds.Height >= control.FontHeight); + Assert.True(listBounds.Top > editBounds.Bottom); + } + + [WinFormsFact] + public void ComboBox_ModernSimple_ListIsBorderlessAndFillsRoundedField() + { + using SystemVisualSettingsTestScope settingsScope = new( + clientAreaAnimationEnabled: false, + highContrastEnabled: false); + using VisualStylesComboBox control = new() + { + DropDownStyle = ComboBoxStyle.Simple, + FlatStyle = FlatStyle.Standard, + Padding = Padding.Empty, + Size = new Size(220, 140), + VisualStylesMode = VisualStylesMode.Net11 + }; + control.Items.AddRange(["one", "two", "three", "four", "five", "six", "seven", "eight"]); + control.CreateControl(); + + Rectangle listBounds = control.GetListBounds(); + + Assert.False(control.ListHasBorderStyle()); + Assert.False(control.ListHasClientEdgeExStyle()); + Assert.True(listBounds.Left <= control.ModernChromeInsets.Left); + Assert.True(listBounds.Right >= control.ClientRectangle.Right - control.ModernChromeInsets.Right - 1); + } + + [WinFormsFact] + public void ComboBox_ModernSimple_ApplyModernLayout_ReappliesBorderlessListSurface() + { + using SystemVisualSettingsTestScope settingsScope = new( + clientAreaAnimationEnabled: false, + highContrastEnabled: false); + using VisualStylesComboBox control = new() + { + DropDownStyle = ComboBoxStyle.Simple, + FlatStyle = FlatStyle.Standard, + Padding = Padding.Empty, + Size = new Size(220, 140), + VisualStylesMode = VisualStylesMode.Net11 + }; + control.Items.AddRange(["one", "two", "three", "four", "five", "six", "seven", "eight"]); + control.CreateControl(); + + Assert.False(control.ListHasBorderStyle()); + Assert.False(control.ListHasClientEdgeExStyle()); + + control.ReapplyListBorderAndClientEdgeStyles(); + Assert.True(control.ListHasBorderStyle()); + Assert.True(control.ListHasClientEdgeExStyle()); + + control.ApplyModernComboLayout(); + + Assert.False(control.ListHasBorderStyle()); + Assert.False(control.ListHasClientEdgeExStyle()); + } + + [WinFormsFact] + public void ComboBox_ModernSimple_DrawsAccentDividerBetweenEditAndList() + { + using SystemVisualSettingsTestScope settingsScope = new( + clientAreaAnimationEnabled: false, + highContrastEnabled: false); + using VisualStylesComboBox control = new() + { + DropDownStyle = ComboBoxStyle.Simple, + FlatStyle = FlatStyle.Standard, + Padding = Padding.Empty, + Size = new Size(220, 140), + VisualStylesMode = VisualStylesMode.Net11 + }; + control.Items.AddRange(["one", "two", "three", "four", "five", "six", "seven", "eight"]); + control.CreateControl(); + using Bitmap bitmap = new(control.Width, control.Height); + + control.DrawToBitmap(bitmap, new Rectangle(Point.Empty, control.Size)); + + Rectangle listBounds = control.GetListBounds(); + int dividerThickness = control.ModernSimpleDividerThickness; + int sampleX = Math.Clamp(control.Width / 2, 0, control.Width - 1); + int startY = Math.Max(0, listBounds.Top - dividerThickness); + int endY = Math.Min(control.Height - 1, listBounds.Top + 1); + bool foundDivider = false; + + for (int y = startY; y <= endY; y++) + { + Color pixel = bitmap.GetPixel(sampleX, y); + if (ColorsAreClose(pixel, Application.SystemVisualSettings.AccentColor, channelTolerance: 3)) + { + foundDivider = true; + break; + } + } + + Assert.True(foundDivider); + } + [WinFormsFact] public void ComboBox_ModernSimple_RepeatedMetricChangeDoesNotAccumulatePadding() { @@ -1495,6 +1679,41 @@ public void ComboBox_ModernSimple_RepeatedMetricChangeDoesNotAccumulatePadding() } } + [WinFormsFact] + public void ComboBox_ModernSimple_ApplyModernLayout_DoesNotReapplyUnchangedClipRegion() + { + using SystemVisualSettingsTestScope settingsScope = new( + clientAreaAnimationEnabled: false, + highContrastEnabled: false); + using VisualStylesComboBox control = new() + { + DropDownStyle = ComboBoxStyle.Simple, + FlatStyle = FlatStyle.Standard, + Padding = Padding.Empty, + Size = new Size(220, 140), + VisualStylesMode = VisualStylesMode.Net11 + }; + control.Items.AddRange(["one", "two", "three", "four", "five", "six", "seven", "eight"]); + control.CreateControl(); + + int initialApplyCount = control.ModernSimpleListClipRegionApplyCount; + + control.ApplyModernComboLayout(); + int stableApplyCount = control.ModernSimpleListClipRegionApplyCount; + + Assert.Equal(initialApplyCount, stableApplyCount); + + control.Height += 20; + + Assert.True(control.ModernSimpleListClipRegionApplyCount > stableApplyCount); + + int resizedApplyCount = control.ModernSimpleListClipRegionApplyCount; + + control.ApplyModernComboLayout(); + + Assert.Equal(resizedApplyCount, control.ModernSimpleListClipRegionApplyCount); + } + [Theory] [InlineData(10, 0, 21999, false)] [InlineData(10, 0, 22000, true)] @@ -1547,6 +1766,49 @@ public void ComboBox_CreateParams_GetDefault_ReturnsExpected() Assert.False(control.IsHandleCreated); } + [WinFormsFact] + public void ComboBox_CreateParams_Net11Simple_ContainsVScroll() + { + using SubComboBox control = new() + { + VisualStylesMode = VisualStylesMode.Net11, + DropDownStyle = ComboBoxStyle.Simple + }; + + CreateParams createParams = control.CreateParams; + + Assert.NotEqual(0, createParams.Style & (int)WINDOW_STYLE.WS_VSCROLL); + } + + [WinFormsFact] + public void ComboBox_CreateParams_Net11Simple_ContainsNoIntegralHeight() + { + using SubComboBox control = new() + { + VisualStylesMode = VisualStylesMode.Net11, + DropDownStyle = ComboBoxStyle.Simple + }; + + CreateParams createParams = control.CreateParams; + + Assert.NotEqual(0, createParams.Style & PInvoke.CBS_NOINTEGRALHEIGHT); + } + + [WinFormsFact] + public void ComboBox_CreateParams_Net11SimpleWithExplicitIntegralHeight_StillForcesNoIntegralHeight() + { + using SubComboBox control = new() + { + VisualStylesMode = VisualStylesMode.Net11, + DropDownStyle = ComboBoxStyle.Simple, + IntegralHeight = true + }; + + CreateParams createParams = control.CreateParams; + + Assert.NotEqual(0, createParams.Style & PInvoke.CBS_NOINTEGRALHEIGHT); + } + [WinFormsTheory] [InvalidEnumData] public void ComboBox_AutoCompleteMode_SetInvalidValue_ThrowsInvalidEnumArgumentException(AutoCompleteMode value) @@ -4275,6 +4537,61 @@ public Rectangle GetButtonBounds() comboBoxInfo.rcButton.Height); } + public bool ListHasBorderStyle() + { + COMBOBOXINFO comboBoxInfo = GetComboBoxInfo(); + WINDOW_STYLE listStyle = (WINDOW_STYLE)PInvokeCore.GetWindowLong( + comboBoxInfo.hwndList, + WINDOW_LONG_PTR_INDEX.GWL_STYLE); + + return (listStyle & WINDOW_STYLE.WS_BORDER) != 0; + } + + public bool ListHasClientEdgeExStyle() + { + COMBOBOXINFO comboBoxInfo = GetComboBoxInfo(); + WINDOW_EX_STYLE listExStyle = (WINDOW_EX_STYLE)PInvokeCore.GetWindowLong( + comboBoxInfo.hwndList, + WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); + + return (listExStyle & WINDOW_EX_STYLE.WS_EX_CLIENTEDGE) != 0; + } + + public void ReapplyListBorderAndClientEdgeStyles() + { + COMBOBOXINFO comboBoxInfo = GetComboBoxInfo(); + HWND listHandle = comboBoxInfo.hwndList; + + WINDOW_STYLE style = (WINDOW_STYLE)PInvokeCore.GetWindowLong( + listHandle, + WINDOW_LONG_PTR_INDEX.GWL_STYLE); + WINDOW_EX_STYLE exStyle = (WINDOW_EX_STYLE)PInvokeCore.GetWindowLong( + listHandle, + WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); + + PInvokeCore.SetWindowLong( + listHandle, + WINDOW_LONG_PTR_INDEX.GWL_STYLE, + (nint)(style | WINDOW_STYLE.WS_BORDER)); + PInvokeCore.SetWindowLong( + listHandle, + WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, + (nint)(exStyle | WINDOW_EX_STYLE.WS_EX_CLIENTEDGE)); + + PInvoke.SetWindowPos( + listHandle, + HWND.Null, + 0, + 0, + 0, + 0, + SET_WINDOW_POS_FLAGS.SWP_NOMOVE + | SET_WINDOW_POS_FLAGS.SWP_NOSIZE + | SET_WINDOW_POS_FLAGS.SWP_NOZORDER + | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE + | SET_WINDOW_POS_FLAGS.SWP_FRAMECHANGED); + } + private unsafe COMBOBOXINFO GetComboBoxInfo() { COMBOBOXINFO comboBoxInfo = default; @@ -4304,6 +4621,14 @@ public int ModernComboLayoutWriteCount => (int)this.TestAccessor.Dynamic .GetModernComboLayoutWriteCount(); + public int ModernSimpleListClipRegionApplyCount + => (int)this.TestAccessor.Dynamic + .GetModernSimpleListClipRegionApplyCount(); + + public int ModernSimpleDividerThickness + => (int)this.TestAccessor.Dynamic + .GetModernSimpleDividerThickness(); + public int NativeSelectionHeight => (int)this.TestAccessor.Dynamic .GetNativeComboBaselineSelectionFieldItemHeight();