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
Expand Up @@ -165,19 +165,6 @@ private ModernComboTargetState ComputeModernComboTargetState()
{
int topInset = chromeInsets.Top + Padding.Top;
int bottomInset = chromeInsets.Bottom + Padding.Bottom;
editBounds.Y += topInset;

editBounds.Height = Math.Max(
1,
editBounds.Height - topInset - bottomInset);

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.
Expand All @@ -189,30 +176,91 @@ 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 = Math.Max(
1,
ScaleHelper.ScaleToDpi(
ModernControlVisualStyles.BorderThickness,
DeviceDpiInternal));
const int bottomCropLogicalPixels = 2;
int simpleBottomShrink = ScaleHelper.ScaleToDpi(bottomCropLogicalPixels, 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;
}

editBounds.Width = Math.Max(
1,
editBounds.Width - leftInset - rightInset - extraButtonWidth);

if (!simpleListBounds.IsEmpty)
{
int simpleListBottom = GetCurrentSimpleListBottom(
simpleListBounds.Bottom);
editBounds.Y += topInset;

simpleListBounds.Y = editBounds.Bottom + bottomInset;
editBounds.Height = Math.Max(
1,
editBounds.Height - topInset - bottomInset);

simpleListBounds.Height = Math.Max(
editBounds.Height = Math.Max(
1,
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);
}
}
}

Expand Down Expand Up @@ -281,6 +329,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)
{
Expand Down Expand Up @@ -464,24 +566,63 @@ 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(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clip region is re-applied on every layout pass. ConfigureModernSimpleListClipRegion runs unconditionally in ApplyChildBounds for the modern Simple list, even when the target bounds are unchanged. While the SetWindowPos call is guarded against no-op moves, this SetWindowRgn(..., fRedraw: true) is not, so every relayout forces a region reset and repaint. Consider skipping the region update when the computed clip rect matches the previously applied one to avoid unnecessary work and potential flicker.

childHandle,
targetBounds);
}
}

private void ConfigureModernSimpleListClipRegion(
HWND listHandle,
Rectangle targetBounds)
{
if (listHandle.IsNull)
{
return;
}

int bottomShrink = ScaleHelper.ScaleToDpi(2, DeviceDpiInternal);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible double bottom-crop. The Simple layout already reserves simpleBottomShrink = ScaleToDpi(bottomCropLogicalPixels) (plus dividerThickness) when computing simpleListBottom (line ~187-188), and here the clip region shrinks the visible height by an additional ScaleToDpi(2). If both are intentional the total reserved space at the bottom is larger than it may appear from either site alone. Could you confirm this is deliberate, and ideally derive both from a single shared constant so they can't drift apart?

int visibleHeight = Math.Max(1, targetBounds.Height - bottomShrink);
using RegionScope region = new(
0,
0,
targetBounds.Width,
targetBounds.Height,
SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE
| SET_WINDOW_POS_FLAGS.SWP_NOZORDER);
visibleHeight);

_modernComboLayoutWriteCount++;
if (PInvoke.SetWindowRgn(
listHandle,
region,
fRedraw: true) != 0)
{
region.RelinquishOwnership();
}
}

private Rectangle GetChildBounds(HWND child)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public override void DrawFlatCombo(
graphics,
clientBounds);
DrawDropDownButton(comboBox, graphics);
DrawSimpleEditListDivider(comboBox, graphics);

switch (_flatStyle)
{
Expand Down Expand Up @@ -395,6 +396,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forcing CBS_NOINTEGRALHEIGHT for the modern Simple combo silently overrides an explicit IntegralHeight = true set by the user. IntegralHeight is a public property, so a consumer setting it to true will now see it ignored with no feedback. Consider only forcing this style when IntegralHeight is at its default (or documenting that modern Simple mode does not honor IntegralHeight), so the public contract isn't silently broken.

{
cp.Style |= PInvoke.CBS_NOINTEGRALHEIGHT;
}
Expand Down Expand Up @@ -2408,6 +2410,12 @@ protected override unsafe void OnHandleCreated(EventArgs e)
bool hasComboBoxInfo = PInvoke.GetComboBoxInfo(
HWND,
ref comboBoxInfo);

if (hasComboBoxInfo)
{
ConfigureModernSimpleListSurface(comboBoxInfo.hwndList);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConfigureModernSimpleListSurface strips WS_BORDER / WS_EX_CLIENTEDGE from the list only once, at handle creation. For a persistently visible Simple list these styles can be re-applied by later theme changes, DPI changes, or a SetWindowTheme/settings-change round-trip, after which the border could reappear. Consider re-running this normalization on the relevant change notifications (e.g. WM_THEMECHANGED / DPI-changed) rather than only in OnHandleCreated.

}

if (Application.IsDarkModeEnabled)
{
// Style the ComboBox Open-Button:
Expand Down Expand Up @@ -3725,14 +3733,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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now routes WM_NCCALCSIZE (and WM_NCHITTEST below) through the modern non-client handling for DropDownStyle.Simple as well, whereas Simple was previously excluded. Since the Simple list is an always-visible child with its own scrollbar, please double-check that expanding/adjusting the client area here does not interfere with the list's vertical scrollbar hit-testing and dragging, mouse wheel, and keyboard navigation. Worth an explicit manual test on the Simple list with more items than fit.

{
RECT* ncRects = (RECT*)(nint)m.LParamInternal;
Expand All @@ -3745,15 +3750,12 @@ 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). Force HTCLIENT across the expanded
// client so interaction remains consistent with the modern layout.
case PInvokeCore.WM_NCHITTEST:
base.WndProc(ref m);
if (UsesModernComboAdapter
&& DropDownStyle != ComboBoxStyle.Simple
&& m.ResultInternal != PInvoke.HTCLIENT)
{
Point hitPoint = PointToClient(PARAM.ToPoint(m.LParamInternal));
Expand Down
Loading