Skip to content

Commit 85ca4e6

Browse files
crutkasCopilot
andcommitted
Grab and Move: tight warning-gold overlay border (Always On Top aware)
Replace the full translucent white wash with a tight border that hugs the visible window frame, mirroring Always On Top: - Anchor to DWMWA_EXTENDED_FRAME_BOUNDS (inset by the invisible resize-border margins) instead of GetWindowRect, so the border sits on the visible window edge instead of being offset by the ~7px invisible border. - Match the window corner radius via DWMWA_WINDOW_CORNER_PREFERENCE and scale the border thickness and radius by the target window DPI. - Use Fluent "warning" gold (#FFB900, the literal equivalent of WinUI SystemFillColorCaution) so Grab and Move reads as distinct from AoT. - Default 4px; 8px when the window is also pinned by Always On Top (detected via the AlwaysOnTop_Border tool window) so AoT's accent border layers on top for a double-layer effect. Rendering keeps the GDI + UpdateLayeredWindow per-pixel-alpha path and adds GDI+ (a system library) for the antialiased, optionally rounded border. Window-frame metrics are computed once per drag/resize, never in the mouse-move hot path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0a8fb1d commit 85ca4e6

3 files changed

Lines changed: 229 additions & 7 deletions

File tree

src/modules/GrabAndMove/GrabAndMove/GrabAndMove.vcxproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
</AdditionalIncludeDirectories>
8989
</ClCompile>
9090
<Link>
91-
<AdditionalDependencies>WindowsApp.lib;%(AdditionalDependencies)</AdditionalDependencies>
91+
<AdditionalDependencies>WindowsApp.lib;Gdiplus.lib;Dwmapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
9292
</Link>
9393
</ItemDefinitionGroup>
9494
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">

src/modules/GrabAndMove/GrabAndMove/main.cpp

Lines changed: 226 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
#include "resource.h"
1212

13+
#include <dwmapi.h>
14+
1315
TRACELOGGING_DEFINE_PROVIDER(
1416
g_hProvider,
1517
"Microsoft.PowerToys",
@@ -21,6 +23,7 @@ TRACELOGGING_DEFINE_PROVIDER(
2123
// Globals
2224
// ---------------------------------------------------------------------------
2325
static HINSTANCE g_hInstance = nullptr;
26+
static ULONG_PTR g_gdiplusToken = 0; // GDI+ token for overlay border rendering
2427
static HHOOK g_hhkKeyboard = nullptr;
2528
static HHOOK g_hhkMouse = nullptr;
2629
static HWND g_hMsgWnd = nullptr;
@@ -47,6 +50,25 @@ static HWND g_hOverlay = nullptr; // semi-transparent overlay during drag
4750
static int g_overlayInfoX = 0, g_overlayInfoY = 0;
4851
static int g_overlayInfoW = 0, g_overlayInfoH = 0;
4952

53+
// Visible-frame overlay border metrics. Computed once per drag/resize (cold path)
54+
// and reused while rendering - never recomputed in the mouse-move hot path.
55+
// Margins are the difference between GetWindowRect and the DWM extended frame
56+
// bounds (the invisible resize border), so the border hugs the visible window.
57+
static int g_overlayMarginL = 0, g_overlayMarginT = 0, g_overlayMarginR = 0, g_overlayMarginB = 0;
58+
static int g_overlayCornerRadius = 0; // physical px; 0 = square corners
59+
static int g_overlayBorderThickness = 4; // physical px
60+
static bool g_overlayAotActive = false; // target window is pinned by Always On Top
61+
62+
// Fluent "warning" gold - the literal equivalent of WinUI SystemFillColorCaution
63+
// (used as a ThemeResource for warnings across the Settings UI). A Win32 layered
64+
// window can't resolve a ThemeResource, so the literal is required here.
65+
static constexpr COLORREF OVERLAY_BORDER_COLOR = RGB(255, 185, 0); // #FFB900
66+
67+
// Base border thickness in DIPs (scaled by the target window DPI). Doubled when the
68+
// target is also pinned by Always On Top so AoT's own accent border layers on top.
69+
static constexpr int OVERLAY_BORDER_DIP = 4;
70+
static constexpr int OVERLAY_BORDER_DIP_AOT = 8;
71+
5072
static bool g_shouldAbsorbAlt =
5173
true; // true if we want to absorb Alt on the next keydown (set when Alt is pressed without dragging, cleared on next non-Alt key or Alt keyup)
5274
static bool g_altAbsorbed = false; // true if we absorbed an Alt keydown
@@ -128,6 +150,12 @@ static const wchar_t* const CLASS_NAME = L"GrabAndMove_MsgWnd";
128150
static const wchar_t* const OVERLAY_CLASS_NAME = L"GrabAndMove_Overlay";
129151
static const wchar_t* const APP_TITLE = L"GrabAndMove";
130152

153+
// Must match Always On Top's border tool-window class
154+
// (NonLocalizable::ToolWindowClassName in
155+
// src\modules\alwaysontop\AlwaysOnTop\WindowBorder.cpp). Used to detect whether the
156+
// target window is currently pinned by Always On Top.
157+
static const wchar_t* const ALWAYS_ON_TOP_BORDER_CLASS = L"AlwaysOnTop_Border";
158+
131159
// Must match CommonSharedConstants::GRABANDMOVE_REFRESH_SETTINGS_EVENT in shared_constants.h
132160
static const wchar_t* const GRABANDMOVE_REFRESH_SETTINGS_EVENT =
133161
L"Local\\PowerToysGrabAndMove-RefreshSettingsEvent-a7b3c1d2-4e5f-6a7b-8c9d-0e1f2a3b4c5d";
@@ -343,9 +371,168 @@ static void SettingsWatcherThread(DWORD mainThreadId)
343371
static int g_overlayRenderedW = 0;
344372
static int g_overlayRenderedH = 0;
345373

374+
// Maps the DWM window corner preference to a base radius in DIPs, matching
375+
// Always On Top (WindowCornerUtils::CornersRadius).
376+
static int CornerRadiusForWindow(HWND hwnd)
377+
{
378+
int pref = 0; // DWMWCP_DEFAULT
379+
if (DwmGetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, &pref, sizeof(pref)) != S_OK)
380+
{
381+
return 0; // pre-Win11 / unsupported -> square corners
382+
}
383+
384+
switch (pref)
385+
{
386+
case DWMWCP_ROUND:
387+
return 8;
388+
case DWMWCP_ROUNDSMALL:
389+
return 4;
390+
case DWMWCP_DEFAULT:
391+
return 8;
392+
default:
393+
return 0; // DWMWCP_DONOTROUND
394+
}
395+
}
396+
397+
// Returns true if the target window currently has an Always On Top border around it.
398+
// AoT creates a topmost tool window of class ALWAYS_ON_TOP_BORDER_CLASS sized to the
399+
// pinned window; a substantial overlap is treated as a match. Enumerates top-level
400+
// windows, so it is only called at the start of a drag/resize, never per mouse move.
401+
static bool IsWindowPinnedByAlwaysOnTop(HWND target)
402+
{
403+
RECT targetRect{};
404+
if (!GetWindowRect(target, &targetRect))
405+
{
406+
return false;
407+
}
408+
409+
struct EnumCtx
410+
{
411+
RECT target;
412+
bool found;
413+
} ctx{ targetRect, false };
414+
415+
EnumWindows(
416+
[](HWND hwnd, LPARAM lparam) -> BOOL {
417+
auto* c = reinterpret_cast<EnumCtx*>(lparam);
418+
wchar_t cls[64]{};
419+
if (GetClassNameW(hwnd, cls, ARRAYSIZE(cls)) == 0 || wcscmp(cls, ALWAYS_ON_TOP_BORDER_CLASS) != 0)
420+
{
421+
return TRUE;
422+
}
423+
424+
RECT border{};
425+
RECT inter{};
426+
if (GetWindowRect(hwnd, &border) && IntersectRect(&inter, &border, &c->target))
427+
{
428+
const long long interArea = (static_cast<long long>(inter.right) - inter.left) * (static_cast<long long>(inter.bottom) - inter.top);
429+
const long long targetArea = (static_cast<long long>(c->target.right) - c->target.left) * (static_cast<long long>(c->target.bottom) - c->target.top);
430+
if (targetArea > 0 && interArea * 100 >= targetArea * 80)
431+
{
432+
c->found = true;
433+
return FALSE; // stop enumeration
434+
}
435+
}
436+
return TRUE;
437+
},
438+
reinterpret_cast<LPARAM>(&ctx));
439+
440+
return ctx.found;
441+
}
442+
443+
// Computes the overlay border metrics (margins to the visible frame, corner radius,
444+
// thickness, AoT state) for the target window. Cold path only: called at the start of
445+
// a drag/resize and after un-maximize, never from the mouse-move hot path.
446+
static void PrepareOverlayMetrics(HWND target)
447+
{
448+
g_overlayMarginL = g_overlayMarginT = g_overlayMarginR = g_overlayMarginB = 0;
449+
g_overlayCornerRadius = 0;
450+
g_overlayAotActive = false;
451+
g_overlayBorderThickness = OVERLAY_BORDER_DIP;
452+
453+
if (!target)
454+
{
455+
return;
456+
}
457+
458+
const UINT dpi = GetDpiForWindow(target);
459+
const float scale = (dpi != 0) ? dpi / 96.0f : 1.0f;
460+
461+
RECT windowRect{};
462+
RECT frameRect{};
463+
if (GetWindowRect(target, &windowRect) &&
464+
SUCCEEDED(DwmGetWindowAttribute(target, DWMWA_EXTENDED_FRAME_BOUNDS, &frameRect, sizeof(frameRect))))
465+
{
466+
g_overlayMarginL = max(0, static_cast<int>(frameRect.left - windowRect.left));
467+
g_overlayMarginT = max(0, static_cast<int>(frameRect.top - windowRect.top));
468+
g_overlayMarginR = max(0, static_cast<int>(windowRect.right - frameRect.right));
469+
g_overlayMarginB = max(0, static_cast<int>(windowRect.bottom - frameRect.bottom));
470+
}
471+
472+
g_overlayCornerRadius = static_cast<int>(CornerRadiusForWindow(target) * scale);
473+
g_overlayAotActive = IsWindowPinnedByAlwaysOnTop(target);
474+
g_overlayBorderThickness = static_cast<int>((g_overlayAotActive ? OVERLAY_BORDER_DIP_AOT : OVERLAY_BORDER_DIP) * scale);
475+
}
476+
477+
// Draws an antialiased (optionally rounded) border stroke fully inside `rect` using
478+
// GDI+. The stroke hugs the inner edge of `rect` (the visible window frame).
479+
static void DrawOverlayBorder(Gdiplus::Graphics& graphics, const RECT& rect, int thickness, int radius)
480+
{
481+
const int w = rect.right - rect.left;
482+
const int h = rect.bottom - rect.top;
483+
if (w <= 0 || h <= 0 || thickness <= 0)
484+
{
485+
return;
486+
}
487+
488+
// Keep the whole stroke inside the visible frame on every side.
489+
thickness = min(thickness, min(w, h) / 2);
490+
if (thickness <= 0)
491+
{
492+
return;
493+
}
494+
495+
const float half = thickness / 2.0f;
496+
const Gdiplus::RectF path(
497+
rect.left + half,
498+
rect.top + half,
499+
static_cast<Gdiplus::REAL>(w) - thickness,
500+
static_cast<Gdiplus::REAL>(h) - thickness);
501+
502+
graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
503+
Gdiplus::Pen pen(
504+
Gdiplus::Color(255, GetRValue(OVERLAY_BORDER_COLOR), GetGValue(OVERLAY_BORDER_COLOR), GetBValue(OVERLAY_BORDER_COLOR)),
505+
static_cast<Gdiplus::REAL>(thickness));
506+
507+
if (radius <= 0)
508+
{
509+
graphics.DrawRectangle(&pen, path);
510+
return;
511+
}
512+
513+
// The stroke is centred, so the path corner radius is the window radius minus
514+
// half the thickness; that keeps the outer edge aligned with the window corner.
515+
const float pathRadius = max(0.0f, radius - half);
516+
const float diameter = min(pathRadius * 2.0f, min(path.Width, path.Height));
517+
if (diameter <= 0.0f)
518+
{
519+
graphics.DrawRectangle(&pen, path);
520+
return;
521+
}
522+
523+
Gdiplus::GraphicsPath border;
524+
border.AddArc(path.X, path.Y, diameter, diameter, 180.0f, 90.0f);
525+
border.AddArc(path.GetRight() - diameter, path.Y, diameter, diameter, 270.0f, 90.0f);
526+
border.AddArc(path.GetRight() - diameter, path.GetBottom() - diameter, diameter, diameter, 0.0f, 90.0f);
527+
border.AddArc(path.X, path.GetBottom() - diameter, diameter, diameter, 90.0f, 90.0f);
528+
border.CloseFigure();
529+
graphics.DrawPath(&pen, &border);
530+
}
531+
346532
// Renders the overlay surface using per-pixel alpha via UpdateLayeredWindow.
347-
// The white background is painted at ~40% opacity; the geometry label box is
348-
// painted fully opaque so it remains legible regardless of what is beneath.
533+
// The surface is fully transparent except for a tight warning-gold border around the
534+
// visible window frame; the optional geometry label box is painted fully opaque so it
535+
// remains legible regardless of what is beneath.
349536
static void RenderOverlayContent(HWND hwnd, int cw, int ch)
350537
{
351538
if (!hwnd || cw <= 0 || ch <= 0)
@@ -372,8 +559,24 @@ static void RenderOverlayContent(HWND hwnd, int cw, int ch)
372559
HDC memDC = CreateCompatibleDC(screenDC);
373560
HBITMAP hOldBmp = static_cast<HBITMAP>(SelectObject(memDC, hDib));
374561

375-
// Premultiplied white at ~40% opacity: A=0x66, R=G=B=0x66 → 0x66666666
376-
memset(pBits, 0x66, static_cast<size_t>(cw) * ch * sizeof(DWORD));
562+
// Start fully transparent; we paint only a tight border (+ optional label).
563+
memset(pBits, 0, static_cast<size_t>(cw) * ch * sizeof(DWORD));
564+
565+
// Warning-gold border hugging the visible window frame. The overlay window spans
566+
// GetWindowRect, so inset by the invisible-border margins so the stroke lands on
567+
// the visible edge (mirrors Always On Top's tight border).
568+
{
569+
const RECT visible = {
570+
g_overlayMarginL,
571+
g_overlayMarginT,
572+
cw - g_overlayMarginR,
573+
ch - g_overlayMarginB
574+
};
575+
Gdiplus::Bitmap bitmap(cw, ch, cw * 4, PixelFormat32bppPARGB, reinterpret_cast<BYTE*>(pBits));
576+
Gdiplus::Graphics graphics(&bitmap);
577+
DrawOverlayBorder(graphics, visible, g_overlayBorderThickness, g_overlayCornerRadius);
578+
graphics.Flush();
579+
}
377580

378581
if (g_showGeometry)
379582
{
@@ -1045,6 +1248,7 @@ static LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
10451248
g_dragTarget = hwnd;
10461249
g_dragStart = pt;
10471250
GetWindowRect(hwnd, &g_dragWndRect);
1251+
PrepareOverlayMetrics(hwnd);
10481252

10491253
// Show the semi-transparent overlay on top of the target (persistent window – fix #9)
10501254
ShowOverlay(g_dragWndRect, g_curSizeAll);
@@ -1112,6 +1316,7 @@ static LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
11121316
g_resizeTarget = hwnd;
11131317
g_resizeLast = pt;
11141318
GetWindowRect(hwnd, &g_resizeWndRect);
1319+
PrepareOverlayMetrics(hwnd);
11151320

11161321
g_currentHandle = GetClosestHandle(pt, g_resizeWndRect);
11171322
ShowOverlay(g_resizeWndRect, CursorForHandle(g_currentHandle));
@@ -1183,6 +1388,9 @@ static void HandleDragMove(POINT pt)
11831388

11841389
g_dragStart = pt;
11851390
g_dragWndRect = {newX, newY, newX + restoredW, newY + restoredH};
1391+
1392+
// Corner radius / invisible-border margins differ once restored.
1393+
PrepareOverlayMetrics(g_dragTarget);
11861394
}
11871395
}
11881396

@@ -1230,6 +1438,9 @@ static void HandleDragResize(POINT pt)
12301438
SWP_NOZORDER | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS);
12311439
g_resizeWndRect = {newLeft, newTop, newLeft + newW, newTop + newH};
12321440

1441+
// Corner radius / invisible-border margins differ once restored.
1442+
PrepareOverlayMetrics(g_resizeTarget);
1443+
12331444
g_resizeLast = pt;
12341445
g_currentHandle = GetClosestHandle(pt, g_resizeWndRect);
12351446
}
@@ -1375,6 +1586,10 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR lpCmdLine, int)
13751586
INITCOMMONCONTROLSEX commonControls = { sizeof(commonControls), ICC_STANDARD_CLASSES };
13761587
InitCommonControlsEx(&commonControls);
13771588

1589+
// Initialise GDI+ for antialiased overlay border rendering
1590+
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
1591+
Gdiplus::GdiplusStartup(&g_gdiplusToken, &gdiplusStartupInput, nullptr);
1592+
13781593
// Register a message-only window class
13791594
WNDCLASSEXW wc = {};
13801595
wc.cbSize = sizeof(wc);
@@ -1387,13 +1602,13 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR lpCmdLine, int)
13871602
return 1;
13881603
}
13891604

1390-
// Register the overlay window class (white background, ARROW cursor)
1605+
// Register the overlay window class (layered per-pixel-alpha surface, ARROW cursor)
13911606
WNDCLASSEXW overlayWindowClass = {};
13921607
overlayWindowClass.cbSize = sizeof(overlayWindowClass);
13931608
overlayWindowClass.lpfnWndProc = DefWindowProcW;
13941609
overlayWindowClass.hInstance = hInstance;
13951610
overlayWindowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
1396-
overlayWindowClass.hbrBackground = static_cast<HBRUSH>(GetStockObject(WHITE_BRUSH));
1611+
overlayWindowClass.hbrBackground = nullptr; // per-pixel alpha via UpdateLayeredWindow
13971612
overlayWindowClass.lpszClassName = OVERLAY_CLASS_NAME;
13981613
if (!RegisterClassExW(&overlayWindowClass))
13991614
{
@@ -1482,6 +1697,11 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR lpCmdLine, int)
14821697
g_hOverlay = nullptr;
14831698
}
14841699
RemoveTrayIcon();
1700+
if (g_gdiplusToken)
1701+
{
1702+
Gdiplus::GdiplusShutdown(g_gdiplusToken);
1703+
g_gdiplusToken = 0;
1704+
}
14851705
TraceLoggingUnregister(g_hProvider);
14861706

14871707
return static_cast<int>(msg.wParam);

src/modules/GrabAndMove/GrabAndMove/pch.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
#define WIN32_LEAN_AND_MEAN
44

55
#include <windows.h>
6+
#include <objidl.h>
7+
#include <gdiplus.h>
68
#include <shellapi.h>
79
#include <commctrl.h>
810
#include <TraceLoggingProvider.h>

0 commit comments

Comments
 (0)