Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
55 changes: 40 additions & 15 deletions src/Wpf.Ui/Controls/TitleBar/TitleBar.WindowResize.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ namespace Wpf.Ui.Controls;
/// </remarks>
public partial class TitleBar
{
/// <summary>
/// Bit flags that represent which window border edges the cursor is currently over.
/// </summary>
[Flags]
private enum BorderHitEdges : uint
{
/// <summary>No border edge is hit.</summary>
None = 0,
/// <summary>The left border edge is hit.</summary>
Left = 1 << 0,
/// <summary>The right border edge is hit.</summary>
Right = 1 << 1,
/// <summary>The top border edge is hit.</summary>
Top = 1 << 2,
/// <summary>The bottom border edge is hit.</summary>
Bottom = 1 << 3,
}

private int _borderX;
private int _borderY;

Expand All @@ -60,29 +78,36 @@ private IntPtr GetWindowBorderHitTestResult(IntPtr hwnd, IntPtr lParam)
int x = (short)(lp & 0xFFFF);
int y = (short)((lp >> 16) & 0xFFFF);

uint hit = 0u;
BorderHitEdges hit = BorderHitEdges.None;

#pragma warning disable
if (x < windowRect.left + _borderX)
hit |= 0b0001u; // left
hit |= BorderHitEdges.Left;
if (x >= windowRect.right - _borderX)
hit |= 0b0010u; // right
hit |= BorderHitEdges.Right;
if (y < windowRect.top + _borderY)
hit |= 0b0100u; // top
hit |= BorderHitEdges.Top;
if (y >= windowRect.bottom - _borderY)
hit |= 0b1000u; // bottom
#pragma warning restore
hit |= BorderHitEdges.Bottom;

if (hit == (BorderHitEdges.Top | BorderHitEdges.Right))
{
const int cornerWidth = 1;
if (x < windowRect.right - cornerWidth)
{
hit = BorderHitEdges.Top;
}
}

return hit switch
{
0b0101u => (IntPtr)PInvoke.HTTOPLEFT, // top + left (0b0100 | 0b0001)
0b0110u => (IntPtr)PInvoke.HTTOPRIGHT, // top + right (0b0100 | 0b0010)
0b1001u => (IntPtr)PInvoke.HTBOTTOMLEFT, // bottom + left (0b1000 | 0b0001)
0b1010u => (IntPtr)PInvoke.HTBOTTOMRIGHT, // bottom + right (0b1000 | 0b0010)
0b0100u => (IntPtr)PInvoke.HTTOP, // top
0b0001u => (IntPtr)PInvoke.HTLEFT, // left
0b1000u => (IntPtr)PInvoke.HTBOTTOM, // bottom
0b0010u => (IntPtr)PInvoke.HTRIGHT, // right
BorderHitEdges.Top | BorderHitEdges.Left => (IntPtr)PInvoke.HTTOPLEFT,
BorderHitEdges.Top | BorderHitEdges.Right => (IntPtr)PInvoke.HTTOPRIGHT,
BorderHitEdges.Bottom | BorderHitEdges.Left => (IntPtr)PInvoke.HTBOTTOMLEFT,
BorderHitEdges.Bottom | BorderHitEdges.Right => (IntPtr)PInvoke.HTBOTTOMRIGHT,
BorderHitEdges.Top => (IntPtr)PInvoke.HTTOP,
BorderHitEdges.Left => (IntPtr)PInvoke.HTLEFT,
BorderHitEdges.Bottom => (IntPtr)PInvoke.HTBOTTOM,
BorderHitEdges.Right => (IntPtr)PInvoke.HTRIGHT,

// no match = HTNOWHERE (stop processing)
_ => (IntPtr)PInvoke.HTNOWHERE,
Expand Down
182 changes: 148 additions & 34 deletions src/Wpf.Ui/Controls/TitleBar/TitleBar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -408,16 +408,16 @@ public event TypedEventHandler<TitleBar, RoutedEventArgs> HelpClicked
/// <summary>
/// Gets or sets the <see cref="Action"/> that should be executed when the Maximize button is clicked."/>
/// </summary>
public Action<TitleBar, System.Windows.Window>? MaximizeActionOverride { get; set; }
public Action<TitleBar, Window>? MaximizeActionOverride { get; set; }

/// <summary>
/// Gets or sets what <see cref="Action"/> should be executed when the Minimize button is clicked.
/// </summary>
public Action<TitleBar, System.Windows.Window>? MinimizeActionOverride { get; set; }
public Action<TitleBar, Window>? MinimizeActionOverride { get; set; }

private readonly TitleBarButton?[] _buttons = new TitleBarButton[4];
private readonly TitleBarButton[] _buttons = new TitleBarButton[4];
Comment thread
Nuklon marked this conversation as resolved.
Outdated
private readonly TextBlock _titleBlock;
private System.Windows.Window _currentWindow = null!;
private Window _currentWindow = null!;

/*private System.Windows.Controls.Grid _mainGrid = null!;*/
private System.Windows.Controls.ContentPresenter _icon = null!;
Expand Down Expand Up @@ -468,7 +468,7 @@ protected virtual void OnLoaded(object sender, RoutedEventArgs e)
}

_currentWindow =
System.Windows.Window.GetWindow(this) ?? throw new InvalidOperationException("Window is null");
Window.GetWindow(this) ?? throw new InvalidOperationException("Window is null");
if (_currentWindow.WindowState == WindowState.Maximized)
{
SetCurrentValue(IsMaximizedProperty, true);
Expand Down Expand Up @@ -674,54 +674,160 @@ or PInvoke.WM_NCLBUTTONUP
return IntPtr.Zero;
}

foreach (TitleBarButton? button in _buttons)
bool isMouseOverHeaderContent = false;
bool isMouseOverButtons = false;
IntPtr htResult = (IntPtr)PInvoke.HTNOWHERE;

// For WM_NCHITTEST, perform resize detection first, and skip button hit testing if top-left or top-right corner resize detection succeeds
if (message == PInvoke.WM_NCHITTEST)
{
// Check if button is null to avoid potential NullReferenceException if OnApplyTemplate hasn't been called yet, e.g. when TitleBar has Visibility == Collapsed.
if (button is null || !button.ReactToHwndHook(message, lParam, out IntPtr returnIntPtr))
if (TrailingContent is UIElement || Header is UIElement || CenterContent is UIElement)
{
continue;
UIElement? headerLeftUIElement = Header as UIElement;
UIElement? headerCenterUIElement = CenterContent as UIElement;
UIElement? headerTrailingUiElement = TrailingContent as UIElement;

isMouseOverHeaderContent =
(headerLeftUIElement is not null
&& headerLeftUIElement != _titleBlock
&& TitleBarButton.IsMouseOverNonClient(headerLeftUIElement, lParam)) || (headerCenterUIElement is not null
&& TitleBarButton.IsMouseOverNonClient(headerCenterUIElement, lParam)) || (headerTrailingUiElement is not null
&& TitleBarButton.IsMouseOverNonClient(headerTrailingUiElement, lParam));
}

// Fix for when sometimes, button hover backgrounds aren't cleared correctly, causing multiple buttons to appear as if hovered.
foreach (TitleBarButton? anotherButton in _buttons)
TitleBarButton? rightmostButton = null;
double rightmostRightEdge = double.MinValue;

foreach (TitleBarButton button in _buttons)
{
if (anotherButton is null || anotherButton == button)
if (button is null)
{
continue;
}

if (anotherButton.IsHovered && button.IsHovered)
try
{
anotherButton.RemoveHover();
if (PresentationSource.FromVisual(button) is not null)
{
double buttonRightEdge = button.PointToScreen(new Point(button.RenderSize.Width, 0)).X;

if (buttonRightEdge > rightmostRightEdge)
{
rightmostRightEdge = buttonRightEdge;
rightmostButton = button;
}
}
}
catch
{
// Ignore visual transform errors and keep searching.
}

if (TitleBarButton.IsMouseOverNonClient(button, lParam))
{
isMouseOverButtons = true;
}
}

handled = true;
return returnIntPtr;
}
htResult = GetWindowBorderHitTestResult(hwnd, lParam);

bool isMouseOverHeaderContent = false;
IntPtr htResult = (IntPtr)PInvoke.HTNOWHERE;
// Resize zones always take priority over buttons, matching native Windows behavior.
// The resize strip occupies the outermost few pixels of each edge; GetWindowBorderHitTestResult
// operates in physical pixels throughout, so the zone is correctly positioned at any DPI.
if (htResult != (IntPtr)PInvoke.HTNOWHERE)
{
RemoveButtonHovers();
handled = true;
return htResult;
}

if (message == PInvoke.WM_NCHITTEST)
{
if (TrailingContent is UIElement || Header is UIElement || CenterContent is UIElement)
if (rightmostButton is not null
&& PInvoke.GetCursorPos(out System.Drawing.Point cursorPoint))
{
UIElement? headerLeftUIElement = Header as UIElement;
UIElement? headerCenterUIElement = CenterContent as UIElement;
UIElement? headerRightUiElement = TrailingContent as UIElement;
Point cursorPosition = new(cursorPoint.X, cursorPoint.Y);

isMouseOverHeaderContent =
(
headerLeftUIElement is not null
&& headerLeftUIElement != _titleBlock
&& headerLeftUIElement.IsMouseOverElement(lParam)
try
{
Point rightmostTopLeft = rightmostButton.PointToScreen(new Point(0, 0));
double rightEdge = rightmostButton.PointToScreen(new Point(rightmostButton.RenderSize.Width, 0)).X;
double leftEdge = rightEdge - 1;
double bottomEdge = rightmostButton.PointToScreen(new Point(0, rightmostButton.RenderSize.Height)).Y;

if (
cursorPosition.X >= leftEdge
&& cursorPosition.X <= rightEdge
&& cursorPosition.Y >= rightmostTopLeft.Y
&& cursorPosition.Y <= bottomEdge
)
|| (headerCenterUIElement?.IsMouseOverElement(lParam) ?? false)
|| (headerRightUiElement?.IsMouseOverElement(lParam) ?? false);
{
RemoveButtonHovers();
handled = true;
return (IntPtr)PInvoke.HTRIGHT;
}
}
catch
{
// Ignore transform errors and fall back to default hit testing.
}
}

if (isMouseOverButtons)
{
htResult = (IntPtr)PInvoke.HTNOWHERE;
}
}
else if (message == PInvoke.WM_NCLBUTTONDOWN)
{
// For WM_NCLBUTTONDOWN, also skip button hit testing if within top-left or top-right corner resize area
// This ensures resize handling works correctly
foreach (TitleBarButton button in _buttons)
{
if (button is null)
{
continue;
}

if (TitleBarButton.IsMouseOverNonClient(button, lParam))
{
isMouseOverButtons = true;
break;
}
}

htResult = GetWindowBorderHitTestResult(hwnd, lParam);

if (htResult != (IntPtr)PInvoke.HTNOWHERE)
{
// If within resize area, skip button hit testing
// and let Windows handle the default resize processing
handled = false;
return IntPtr.Zero;
}
}

foreach (TitleBarButton button in _buttons)
{
if (!button.ReactToHwndHook(message, lParam, out IntPtr returnIntPtr))
{
continue;
}

// Fix for when sometimes, button hover backgrounds aren't cleared correctly, causing multiple buttons to appear as if hovered.
foreach (TitleBarButton anotherButton in _buttons)
{
if (anotherButton == button)
{
continue;
}

if (anotherButton.IsHovered && button.IsHovered)
{
anotherButton.RemoveHover();
}
}

handled = true;
return returnIntPtr;
}

var e = new HwndProcEventArgs(hwnd, msg, wParam, lParam, isMouseOverHeaderContent);
Expand All @@ -735,21 +841,29 @@ headerLeftUIElement is not null

switch (message)
{
case PInvoke.WM_NCHITTEST when CloseWindowByDoubleClickOnIcon && _icon.IsMouseOverElement(lParam):
case PInvoke.WM_NCHITTEST when CloseWindowByDoubleClickOnIcon && TitleBarButton.IsMouseOverNonClient(_icon, lParam):
// Ideally, clicking on the icon should open the system menu, but when the system menu is opened manually, double-clicking on the icon does not close the window
handled = true;
return (IntPtr)PInvoke.HTSYSMENU;
case PInvoke.WM_NCHITTEST when htResult != (IntPtr)PInvoke.HTNOWHERE:
handled = true;
return htResult;
case PInvoke.WM_NCHITTEST when this.IsMouseOverElement(lParam) && !isMouseOverHeaderContent:
case PInvoke.WM_NCHITTEST when TitleBarButton.IsMouseOverNonClient(this, lParam) && !isMouseOverHeaderContent:
handled = true;
return (IntPtr)PInvoke.HTCAPTION;
default:
return IntPtr.Zero;
}
}

private void RemoveButtonHovers()
{
foreach (TitleBarButton button in _buttons)
{
button?.RemoveHover();
}
}

/// <summary>
/// Show 'SystemMenu' on mouse right button up.
/// </summary>
Expand Down
Loading
Loading