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
17 changes: 15 additions & 2 deletions src/gui/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3876,6 +3876,7 @@ static LRESULT frameHitCode(WindowChrome::FrameHit hit)
case WindowChrome::FrameHit::TopRight: return HTTOPRIGHT;
case WindowChrome::FrameHit::BottomLeft: return HTBOTTOMLEFT;
case WindowChrome::FrameHit::BottomRight: return HTBOTTOMRIGHT;
case WindowChrome::FrameHit::Caption: return HTCAPTION;
case WindowChrome::FrameHit::Client: break;
}
return HTCLIENT;
Expand All @@ -3887,7 +3888,8 @@ bool MainWindow::nativeEvent(const QByteArray& eventType, void* message, qintptr
// Under the expanded client area Qt 6.12 answers WM_NCHITTEST by turning
// the live mouse-button state into synthetic presses, which doubles real
// clicks and closes menus as they open (#6272). Answer it here instead:
// the resize band at the edges, client area everywhere else.
// the resize band at the edges, the title bar's own background as the
// caption so Windows offers Snap (#6825), client area everywhere else.
if (msg && result && msg->message == WM_NCHITTEST
&& windowFlags().testFlag(Qt::ExpandedClientAreaHint)) {
RECT window{};
Expand All @@ -3903,7 +3905,18 @@ bool MainWindow::nativeEvent(const QByteArray& eventType, void* message, qintptr
const QPoint point(GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam));
const QRect rect(window.left, window.top,
window.right - window.left, window.bottom - window.top);
*result = frameHitCode(WindowChrome::expandedFrameHit(point, rect, borderX, borderY));
// The bar's height is in logical pixels and WM_NCHITTEST is physical.
// Only the bar's empty background may answer HTCAPTION: over its tabs,
// menus and buttons that is what closed menus as they opened (#6272),
// and isSystemMoveAreaAt() is the same test the bar's own drag uses, so
// a point either starts a system move or stays client — never both.
int captionHeight = 0;
if (m_titleBar && m_titleBar->isVisible()
&& m_titleBar->isSystemMoveAreaAt(QCursor::pos())) {
captionHeight = int(m_titleBar->height() * devicePixelRatioF());
}
*result = frameHitCode(
WindowChrome::expandedFrameHit(point, rect, borderX, borderY, captionHeight));
return true;
}
// The whole window is client area, inset only while maximized so nothing
Expand Down
52 changes: 36 additions & 16 deletions src/gui/WindowChrome.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,27 +90,47 @@ inline bool claimsWholeWindowAsClient(Qt::WindowFlags flags, const QOperatingSys
// live mouse-button state into synthetic presses, doubling real clicks (#6272).
// Physical pixels; `borderX` / `borderY` are the resize bands on the left/right
// and top/bottom edges (0 while maximized).
enum class FrameHit { Client, Left, Right, Top, Bottom, TopLeft, TopRight, BottomLeft, BottomRight };

inline FrameHit expandedFrameHit(const QPoint& point, const QRect& window, int borderX, int borderY)
//
// `Caption` is the title bar's own draggable background: the answer Windows
// needs before it will offer drag-to-edge Snap and list the window in the Snap
// Assist picker (#6825). Only the caller knows which points are the bar's empty
// background rather than its tabs, menus and buttons, so this reports Caption
// for the whole `captionHeight` strip and MainWindow::nativeEvent narrows it
// with TitleBar::isSystemMoveAreaAt() — answering HTCAPTION over an interactive
// widget is what made menus close as they opened (#6272). A `captionHeight` of
// 0 disables it, which is how every non-main window keeps the old behaviour.
enum class FrameHit { Client, Caption, Left, Right, Top, Bottom, TopLeft, TopRight, BottomLeft, BottomRight };

inline FrameHit expandedFrameHit(const QPoint& point, const QRect& window, int borderX, int borderY,
int captionHeight = 0)
{
if (borderX <= 0 || borderY <= 0 || !window.contains(point)) {
if (!window.contains(point)) {
return FrameHit::Client;
}
const bool left = point.x() < window.left() + borderX;
const bool right = point.x() > window.right() - borderX;
const bool top = point.y() < window.top() + borderY;
const bool bottom = point.y() > window.bottom() - borderY;
if (left) {
return top ? FrameHit::TopLeft : bottom ? FrameHit::BottomLeft : FrameHit::Left;
}
if (right) {
return top ? FrameHit::TopRight : bottom ? FrameHit::BottomRight : FrameHit::Right;
// Maximized and fullscreen have no resize band, but the caption still has
// to answer or the window cannot be dragged back off the top of the screen.
if (borderX > 0 && borderY > 0) {
const bool left = point.x() < window.left() + borderX;
const bool right = point.x() > window.right() - borderX;
const bool top = point.y() < window.top() + borderY;
const bool bottom = point.y() > window.bottom() - borderY;
if (left) {
return top ? FrameHit::TopLeft : bottom ? FrameHit::BottomLeft : FrameHit::Left;
}
if (right) {
return top ? FrameHit::TopRight : bottom ? FrameHit::BottomRight : FrameHit::Right;
}
if (top) {
return FrameHit::Top;
}
if (bottom) {
return FrameHit::Bottom;
}
}
if (top) {
return FrameHit::Top;
if (captionHeight > 0 && point.y() < window.top() + captionHeight) {
return FrameHit::Caption;
}
return bottom ? FrameHit::Bottom : FrameHit::Client;
return FrameHit::Client;
}

// Windows: the native window rect that gives `client`, keeping the frame the
Expand Down
50 changes: 50 additions & 0 deletions tests/unified_title_bar_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,56 @@ int main(int argc, char** argv)
"the left/right band and the top/bottom band follow their own widths");
}

// ── The title bar answers as the caption so Windows offers Snap (#6825) ──
{
using Hit = WindowChrome::FrameHit;
const QRect win(100, 50, 1000, 600);
const int band = 8;
const int caption = 52;

// Without a caption height nothing changes: every existing caller
// (and every window that is not the main window) keeps the old answer.
check(WindowChrome::expandedFrameHit(QPoint(600, 70), win, band, band) == Hit::Client,
"the title bar is client area when no caption height is given");

// The bar's background drags the window, which is what Windows needs
// before it offers drag-to-edge Snap and the Snap Assist picker.
check(WindowChrome::expandedFrameHit(QPoint(600, 70), win, band, band, caption)
== Hit::Caption,
"the title bar's background is the caption");
check(WindowChrome::expandedFrameHit(QPoint(600, 101), win, band, band, caption)
== Hit::Caption,
"the last pixel of the caption strip still drags");
check(WindowChrome::expandedFrameHit(QPoint(600, 102), win, band, band, caption)
== Hit::Client,
"the first pixel below the caption is client area");

// The resize band wins over the caption, or the top edge and the top
// corners would stop resizing the moment the caption was introduced.
check(WindowChrome::expandedFrameHit(QPoint(600, 50), win, band, band, caption) == Hit::Top,
"the top resize band still wins over the caption");
check(WindowChrome::expandedFrameHit(QPoint(101, 51), win, band, band, caption)
== Hit::TopLeft
&& WindowChrome::expandedFrameHit(QPoint(1098, 51), win, band, band, caption)
== Hit::TopRight,
"the top corners still resize under the caption");
check(WindowChrome::expandedFrameHit(QPoint(101, 70), win, band, band, caption)
== Hit::Left,
"the side band still resizes alongside the caption");

// Maximized: no resize band, but the caption must still answer or the
// window cannot be dragged back down off the top of the screen.
check(WindowChrome::expandedFrameHit(QPoint(600, 70), win, 0, 0, caption) == Hit::Caption,
"the caption answers while maximized, so the window can be dragged free");
check(WindowChrome::expandedFrameHit(QPoint(600, 350), win, 0, 0, caption) == Hit::Client,
"the body is still client area while maximized");

// A point outside the window is never the caption.
check(WindowChrome::expandedFrameHit(QPoint(20, 20), win, band, band, caption)
== Hit::Client,
"a point outside the window is not the caption");
}

// ── Native client-rect save/restore for the Windows expanded frame (#6303) ──
{
// Rects measured on Windows 10: the expanded frame keeps an 8 px resize
Expand Down
Loading