src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs (189 lines of code) (raw):
using System;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.VisualTree;
using static Avalonia.Win32.Interop.UnmanagedMethods;
namespace Avalonia.Win32
{
internal partial class WindowImpl
{
private HitTestValues HitTestNCA(IntPtr hWnd, IntPtr wParam, IntPtr lParam)
{
// Get the point coordinates for the hit test (screen space).
var ptMouse = PointFromLParam(lParam);
// Get the window rectangle.
GetWindowRect(hWnd, out var rcWindow);
var scaling = (uint)(RenderScaling * StandardDpi);
var relativeScaling = RenderScaling / PrimaryScreenRenderScaling;
// Get the frame rectangle, adjusted for the style without a caption.
var rcFrame = new RECT();
var borderThickness = new RECT();
if (Win32Platform.WindowsVersion < PlatformConstants.Windows10_1607)
{
AdjustWindowRectEx(ref rcFrame, (uint)(WindowStyles.WS_OVERLAPPEDWINDOW & ~WindowStyles.WS_CAPTION), false, 0);
rcFrame.top = (int)(rcFrame.top * relativeScaling);
rcFrame.right = (int)(rcFrame.right * relativeScaling);
rcFrame.left = (int)(rcFrame.left * relativeScaling);
rcFrame.bottom = (int)(rcFrame.bottom * relativeScaling);
AdjustWindowRectEx(ref borderThickness, (uint)GetStyle(), false, 0);
borderThickness.top = (int)(borderThickness.top * relativeScaling);
borderThickness.right = (int)(borderThickness.right * relativeScaling);
borderThickness.left = (int)(borderThickness.left * relativeScaling);
borderThickness.bottom = (int)(borderThickness.bottom * relativeScaling);
}
else
{
AdjustWindowRectExForDpi(ref rcFrame, WindowStyles.WS_OVERLAPPEDWINDOW & ~WindowStyles.WS_CAPTION, false, 0, scaling);
AdjustWindowRectExForDpi(ref borderThickness, GetStyle(), false, 0, scaling);
}
borderThickness.left *= -1;
borderThickness.top *= -1;
if (_extendTitleBarHint >= 0)
{
borderThickness.top = (int)(_extendedMargins.Top * RenderScaling);
}
// Determine if the hit test is for resizing. Default middle (1,1).
ushort uRow = 1;
ushort uCol = 1;
bool onResizeBorder = false;
// Determine if the point is at the left or right of the window.
if (ptMouse.X >= rcWindow.left && ptMouse.X < rcWindow.left + borderThickness.left)
{
uCol = 0; // left side
}
else if (ptMouse.X < rcWindow.right && ptMouse.X >= rcWindow.right - borderThickness.right)
{
uCol = 2; // right side
}
// Determine if the point is at the top or bottom of the window.
if (ptMouse.Y >= rcWindow.top && ptMouse.Y < rcWindow.top + borderThickness.top)
{
onResizeBorder = (ptMouse.Y < (rcWindow.top - rcFrame.top));
// Two cases where we have a valid row 0 hit test:
// - window resize border (top resize border hit)
// - area below resize border that is actual titlebar (caption hit).
if (onResizeBorder || uCol == 1)
{
uRow = 0;
}
}
else if (ptMouse.Y < rcWindow.bottom && ptMouse.Y >= rcWindow.bottom - borderThickness.bottom)
{
uRow = 2;
}
var captionAreaHitTest = WindowState == WindowState.FullScreen ? HitTestValues.HTNOWHERE : HitTestValues.HTCAPTION;
ReadOnlySpan<HitTestValues> hitZones = stackalloc HitTestValues[]
{
HitTestValues.HTTOPLEFT, onResizeBorder ? HitTestValues.HTTOP : captionAreaHitTest,
HitTestValues.HTTOPRIGHT, HitTestValues.HTLEFT, HitTestValues.HTNOWHERE, HitTestValues.HTRIGHT,
HitTestValues.HTBOTTOMLEFT, HitTestValues.HTBOTTOM, HitTestValues.HTBOTTOMRIGHT
};
var zoneIndex = uRow * 3 + uCol;
return hitZones[zoneIndex];
}
protected virtual IntPtr CustomCaptionProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, ref bool callDwp)
{
RawPointerEventArgs? e = null;
IntPtr lRet = IntPtr.Zero;
callDwp = !DwmDefWindowProc(hWnd, msg, wParam, lParam, ref lRet);
switch ((WindowsMessage)msg)
{
case WindowsMessage.WM_DWMCOMPOSITIONCHANGED:
// TODO handle composition changed.
break;
case WindowsMessage.WM_NCHITTEST:
if (lRet == IntPtr.Zero)
{
var hittestResult = HitTestNCA(hWnd, wParam, lParam);
if (hittestResult is HitTestValues.HTNOWHERE or HitTestValues.HTCAPTION)
{
var visualHittestResult = HitTestVisual(lParam);
if (visualHittestResult != HitTestValues.HTNOWHERE)
{
hittestResult = visualHittestResult;
}
}
if (hittestResult != HitTestValues.HTNOWHERE)
{
lRet = (IntPtr)hittestResult;
callDwp = false;
}
}
break;
// Normally, Avalonia doesn't handles non-client input as a special NonClientLeftButtonDown, ignoring move and up events.
// What makes it a problem, Avalonia has to mark templated caption buttons as a non-client area.
// Meaning, these buttons no longer can accept normal client input.
// These messages are needed to explicitly fake this normal client input from non-client messages.
// For both WM_NCMOUSE and WM_NCPOINTERUPDATE
case WindowsMessage.WM_NCMOUSEMOVE when !IsMouseInPointerEnabled:
case WindowsMessage.WM_NCLBUTTONDOWN when !IsMouseInPointerEnabled:
case WindowsMessage.WM_NCLBUTTONUP when !IsMouseInPointerEnabled:
if (lRet == IntPtr.Zero
&& ShouldRedirectNonClientInput(hWnd, wParam, lParam))
{
e = new RawPointerEventArgs(
_mouseDevice,
unchecked((uint)GetMessageTime()),
Owner,
(WindowsMessage)msg switch
{
WindowsMessage.WM_NCMOUSEMOVE => RawPointerEventType.Move,
WindowsMessage.WM_NCLBUTTONDOWN => RawPointerEventType.LeftButtonDown,
WindowsMessage.WM_NCLBUTTONUP => RawPointerEventType.LeftButtonUp,
_ => throw new ArgumentOutOfRangeException(nameof(msg), msg, null)
},
PointToClient(PointFromLParam(lParam)),
RawInputModifiers.None);
}
break;
case WindowsMessage.WM_NCPOINTERUPDATE when _wmPointerEnabled:
case WindowsMessage.WM_NCPOINTERDOWN when _wmPointerEnabled:
case WindowsMessage.WM_NCPOINTERUP when _wmPointerEnabled:
if (lRet == IntPtr.Zero
&& ShouldRedirectNonClientInput(hWnd, wParam, lParam))
{
uint timestamp = 0;
GetDevicePointerInfo(wParam, out var device, out var info, out var point, out var modifiers, ref timestamp);
var eventType = (WindowsMessage)msg switch
{
WindowsMessage.WM_NCPOINTERUPDATE => RawPointerEventType.Move,
WindowsMessage.WM_NCPOINTERDOWN => RawPointerEventType.LeftButtonDown,
WindowsMessage.WM_NCPOINTERUP => RawPointerEventType.LeftButtonUp,
_ => throw new ArgumentOutOfRangeException(nameof(msg), msg, null)
};
e = CreatePointerArgs(device, timestamp, eventType, point, modifiers, info.pointerId);
}
break;
}
if (e is not null && Input is not null)
{
Input(e);
if (e.Handled)
{
callDwp = false;
return IntPtr.Zero;
}
}
return lRet;
}
private HitTestValues HitTestVisual(IntPtr lParam)
{
var position = PointToClient(PointFromLParam(lParam));
if (_owner is Window window)
{
var visual = window.GetVisualAt(position, x =>
{
if (x is IInputElement ie && (!ie.IsHitTestVisible || !ie.IsEffectivelyVisible))
{
return false;
}
return true;
});
if (visual != null)
{
var hitTest = Win32Properties.GetNonClientHitTestResult(visual);
return (HitTestValues)hitTest;
}
}
return HitTestValues.HTNOWHERE;
}
private bool ShouldRedirectNonClientInput(IntPtr hWnd, IntPtr wParam, IntPtr lParam)
{
// We touched frame borders or caption, don't redirect.
if (HitTestNCA(hWnd, wParam, lParam) is not (HitTestValues.HTNOWHERE or HitTestValues.HTCAPTION))
return false;
// Redirect only for buttons.
return HitTestVisual(lParam)
is HitTestValues.HTMINBUTTON
or HitTestValues.HTMAXBUTTON
or HitTestValues.HTCLOSE
or HitTestValues.HTHELP
or HitTestValues.HTMENU
or HitTestValues.HTSYSMENU;
}
}
}