Skip to content

Commit 036d493

Browse files
crutkasCopilot
andcommitted
Grab and Move: tight warning-gold overlay border + translucent fill
Refine the drag/resize overlay so it hugs the visible window frame instead of the old full-window white wash that was offset by the invisible resize border: - Keep the prior translucent white wash over the visible window and add a tight warning-gold border on top. Both hug the visible frame and are rounded to match the window corners. The accent is Fluent warning gold (#FFB900, the literal equivalent of WinUI SystemFillColorCaution, used as a ThemeResource for warnings across the Settings UI; a Win32 layered window can't resolve a ThemeResource). - Anchor to DWMWA_EXTENDED_FRAME_BOUNDS (inset by the invisible-border margins) instead of GetWindowRect, and match the corner radius via DWMWA_WINDOW_CORNER_PREFERENCE; thickness and radius scale with the window DPI. - The border is drawn just inside the visible edge while Always On Top draws its border just outside it, so the two naturally stack into a clean double layer with no special-casing (constant 4px). Rendering keeps the GDI + UpdateLayeredWindow per-pixel-alpha path and adds GDI+ (a system library) for the antialiased, rounded fill and 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 036d493

3 files changed

Lines changed: 209 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: 206 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,29 @@ 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 metrics. Computed once per drag/resize (cold path) and
54+
// 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 fill and border hug the visible
57+
// window. The border is drawn just inside the visible edge; Always On Top draws
58+
// its own border just outside that edge, so the two stack into a clean double
59+
// layer without Grab and Move having to widen its stroke.
60+
static int g_overlayMarginL = 0, g_overlayMarginT = 0, g_overlayMarginR = 0, g_overlayMarginB = 0;
61+
static int g_overlayCornerRadius = 0; // physical px; 0 = square corners
62+
static int g_overlayBorderThickness = 4; // physical px
63+
64+
// Fluent "warning" gold - the literal equivalent of WinUI SystemFillColorCaution
65+
// (used as a ThemeResource for warnings across the Settings UI). A Win32 layered
66+
// window can't resolve a ThemeResource, so the literal is required here.
67+
static constexpr COLORREF OVERLAY_BORDER_COLOR = RGB(255, 185, 0); // #FFB900
68+
69+
// Border thickness in DIPs (scaled by the target window DPI).
70+
static constexpr int OVERLAY_BORDER_DIP = 4;
71+
72+
// Translucent white wash painted over the visible window during a drag/resize,
73+
// matching the prior overlay. ~40% opacity (premultiplied white = 0x66666666).
74+
static constexpr BYTE OVERLAY_FILL_ALPHA = 0x66;
75+
5076
static bool g_shouldAbsorbAlt =
5177
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)
5278
static bool g_altAbsorbed = false; // true if we absorbed an Alt keydown
@@ -343,9 +369,122 @@ static void SettingsWatcherThread(DWORD mainThreadId)
343369
static int g_overlayRenderedW = 0;
344370
static int g_overlayRenderedH = 0;
345371

372+
// Maps the DWM window corner preference to a base radius in DIPs, matching
373+
// Always On Top (WindowCornerUtils::CornersRadius).
374+
static int CornerRadiusForWindow(HWND hwnd)
375+
{
376+
int pref = 0; // DWMWCP_DEFAULT
377+
if (DwmGetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, &pref, sizeof(pref)) != S_OK)
378+
{
379+
return 0; // pre-Win11 / unsupported -> square corners
380+
}
381+
382+
switch (pref)
383+
{
384+
case DWMWCP_ROUND:
385+
return 8;
386+
case DWMWCP_ROUNDSMALL:
387+
return 4;
388+
case DWMWCP_DEFAULT:
389+
return 8;
390+
default:
391+
return 0; // DWMWCP_DONOTROUND
392+
}
393+
}
394+
395+
// Computes the overlay metrics (margins to the visible frame, corner radius, border
396+
// thickness) for the target window. Cold path only: called at the start of a
397+
// drag/resize and after un-maximize, never from the mouse-move hot path.
398+
static void PrepareOverlayMetrics(HWND target)
399+
{
400+
g_overlayMarginL = g_overlayMarginT = g_overlayMarginR = g_overlayMarginB = 0;
401+
g_overlayCornerRadius = 0;
402+
g_overlayBorderThickness = OVERLAY_BORDER_DIP;
403+
404+
if (!target)
405+
{
406+
return;
407+
}
408+
409+
const UINT dpi = GetDpiForWindow(target);
410+
const float scale = (dpi != 0) ? dpi / 96.0f : 1.0f;
411+
412+
RECT windowRect{};
413+
RECT frameRect{};
414+
if (GetWindowRect(target, &windowRect) &&
415+
SUCCEEDED(DwmGetWindowAttribute(target, DWMWA_EXTENDED_FRAME_BOUNDS, &frameRect, sizeof(frameRect))))
416+
{
417+
g_overlayMarginL = max(0, static_cast<int>(frameRect.left - windowRect.left));
418+
g_overlayMarginT = max(0, static_cast<int>(frameRect.top - windowRect.top));
419+
g_overlayMarginR = max(0, static_cast<int>(windowRect.right - frameRect.right));
420+
g_overlayMarginB = max(0, static_cast<int>(windowRect.bottom - frameRect.bottom));
421+
}
422+
423+
g_overlayCornerRadius = static_cast<int>(CornerRadiusForWindow(target) * scale);
424+
g_overlayBorderThickness = static_cast<int>(OVERLAY_BORDER_DIP * scale);
425+
}
426+
427+
// Draws an antialiased (optionally rounded) border stroke fully inside `rect` using
428+
// GDI+. The stroke hugs the inner edge of `rect` (the visible window frame).
429+
static void DrawOverlayBorder(Gdiplus::Graphics& graphics, const RECT& rect, int thickness, int radius)
430+
{
431+
const int w = rect.right - rect.left;
432+
const int h = rect.bottom - rect.top;
433+
if (w <= 0 || h <= 0 || thickness <= 0)
434+
{
435+
return;
436+
}
437+
438+
// Keep the whole stroke inside the visible frame on every side.
439+
thickness = min(thickness, min(w, h) / 2);
440+
if (thickness <= 0)
441+
{
442+
return;
443+
}
444+
445+
const float half = thickness / 2.0f;
446+
const Gdiplus::RectF path(
447+
rect.left + half,
448+
rect.top + half,
449+
static_cast<Gdiplus::REAL>(w) - thickness,
450+
static_cast<Gdiplus::REAL>(h) - thickness);
451+
452+
graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
453+
Gdiplus::Pen pen(
454+
Gdiplus::Color(255, GetRValue(OVERLAY_BORDER_COLOR), GetGValue(OVERLAY_BORDER_COLOR), GetBValue(OVERLAY_BORDER_COLOR)),
455+
static_cast<Gdiplus::REAL>(thickness));
456+
457+
if (radius <= 0)
458+
{
459+
graphics.DrawRectangle(&pen, path);
460+
return;
461+
}
462+
463+
// The stroke is centred, so the path corner radius is the window radius minus
464+
// half the thickness; that keeps the outer edge aligned with the window corner.
465+
const float pathRadius = max(0.0f, radius - half);
466+
const float diameter = min(pathRadius * 2.0f, min(path.Width, path.Height));
467+
if (diameter <= 0.0f)
468+
{
469+
graphics.DrawRectangle(&pen, path);
470+
return;
471+
}
472+
473+
Gdiplus::GraphicsPath border;
474+
border.AddArc(path.X, path.Y, diameter, diameter, 180.0f, 90.0f);
475+
border.AddArc(path.GetRight() - diameter, path.Y, diameter, diameter, 270.0f, 90.0f);
476+
border.AddArc(path.GetRight() - diameter, path.GetBottom() - diameter, diameter, diameter, 0.0f, 90.0f);
477+
border.AddArc(path.X, path.GetBottom() - diameter, diameter, diameter, 90.0f, 90.0f);
478+
border.CloseFigure();
479+
graphics.DrawPath(&pen, &border);
480+
}
481+
482+
// Renders the overlay surface using per-pixel alpha via UpdateLayeredWindow.
346483
// 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.
484+
// A translucent white wash covers the visible window (matching the prior overlay)
485+
// with a tight warning-gold border on top, both hugging the visible window frame;
486+
// the optional geometry label box is painted fully opaque so it remains legible
487+
// regardless of what is beneath.
349488
static void RenderOverlayContent(HWND hwnd, int cw, int ch)
350489
{
351490
if (!hwnd || cw <= 0 || ch <= 0)
@@ -372,8 +511,52 @@ static void RenderOverlayContent(HWND hwnd, int cw, int ch)
372511
HDC memDC = CreateCompatibleDC(screenDC);
373512
HBITMAP hOldBmp = static_cast<HBITMAP>(SelectObject(memDC, hDib));
374513

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));
514+
// Start fully transparent.
515+
memset(pBits, 0, static_cast<size_t>(cw) * ch * sizeof(DWORD));
516+
517+
// Translucent white wash over the visible window (prior-system fill) with a tight
518+
// warning-gold border on top. The overlay window spans GetWindowRect, so inset by
519+
// the invisible-border margins so both hug the visible edge; Always On Top draws
520+
// its own border just outside that edge, giving a clean double layer.
521+
{
522+
const RECT visible = {
523+
g_overlayMarginL,
524+
g_overlayMarginT,
525+
cw - g_overlayMarginR,
526+
ch - g_overlayMarginB
527+
};
528+
const int vw = visible.right - visible.left;
529+
const int vh = visible.bottom - visible.top;
530+
531+
Gdiplus::Bitmap bitmap(cw, ch, cw * 4, PixelFormat32bppPARGB, reinterpret_cast<BYTE*>(pBits));
532+
Gdiplus::Graphics graphics(&bitmap);
533+
graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
534+
535+
if (vw > 0 && vh > 0)
536+
{
537+
Gdiplus::SolidBrush fillBrush(Gdiplus::Color(OVERLAY_FILL_ALPHA, 255, 255, 255));
538+
if (g_overlayCornerRadius > 0)
539+
{
540+
// Round the wash to match the window corners (and the border).
541+
const float d = min(static_cast<float>(g_overlayCornerRadius) * 2.0f,
542+
static_cast<float>(min(vw, vh)));
543+
Gdiplus::GraphicsPath fillPath;
544+
fillPath.AddArc(static_cast<float>(visible.left), static_cast<float>(visible.top), d, d, 180.0f, 90.0f);
545+
fillPath.AddArc(static_cast<float>(visible.right) - d, static_cast<float>(visible.top), d, d, 270.0f, 90.0f);
546+
fillPath.AddArc(static_cast<float>(visible.right) - d, static_cast<float>(visible.bottom) - d, d, d, 0.0f, 90.0f);
547+
fillPath.AddArc(static_cast<float>(visible.left), static_cast<float>(visible.bottom) - d, d, d, 90.0f, 90.0f);
548+
fillPath.CloseFigure();
549+
graphics.FillPath(&fillBrush, &fillPath);
550+
}
551+
else
552+
{
553+
graphics.FillRectangle(&fillBrush, visible.left, visible.top, vw, vh);
554+
}
555+
}
556+
557+
DrawOverlayBorder(graphics, visible, g_overlayBorderThickness, g_overlayCornerRadius);
558+
graphics.Flush();
559+
}
377560

378561
if (g_showGeometry)
379562
{
@@ -1045,6 +1228,7 @@ static LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
10451228
g_dragTarget = hwnd;
10461229
g_dragStart = pt;
10471230
GetWindowRect(hwnd, &g_dragWndRect);
1231+
PrepareOverlayMetrics(hwnd);
10481232

10491233
// Show the semi-transparent overlay on top of the target (persistent window – fix #9)
10501234
ShowOverlay(g_dragWndRect, g_curSizeAll);
@@ -1112,6 +1296,7 @@ static LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
11121296
g_resizeTarget = hwnd;
11131297
g_resizeLast = pt;
11141298
GetWindowRect(hwnd, &g_resizeWndRect);
1299+
PrepareOverlayMetrics(hwnd);
11151300

11161301
g_currentHandle = GetClosestHandle(pt, g_resizeWndRect);
11171302
ShowOverlay(g_resizeWndRect, CursorForHandle(g_currentHandle));
@@ -1183,6 +1368,9 @@ static void HandleDragMove(POINT pt)
11831368

11841369
g_dragStart = pt;
11851370
g_dragWndRect = {newX, newY, newX + restoredW, newY + restoredH};
1371+
1372+
// Corner radius / invisible-border margins differ once restored.
1373+
PrepareOverlayMetrics(g_dragTarget);
11861374
}
11871375
}
11881376

@@ -1230,6 +1418,9 @@ static void HandleDragResize(POINT pt)
12301418
SWP_NOZORDER | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS);
12311419
g_resizeWndRect = {newLeft, newTop, newLeft + newW, newTop + newH};
12321420

1421+
// Corner radius / invisible-border margins differ once restored.
1422+
PrepareOverlayMetrics(g_resizeTarget);
1423+
12331424
g_resizeLast = pt;
12341425
g_currentHandle = GetClosestHandle(pt, g_resizeWndRect);
12351426
}
@@ -1375,6 +1566,10 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR lpCmdLine, int)
13751566
INITCOMMONCONTROLSEX commonControls = { sizeof(commonControls), ICC_STANDARD_CLASSES };
13761567
InitCommonControlsEx(&commonControls);
13771568

1569+
// Initialise GDI+ for antialiased overlay border rendering
1570+
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
1571+
Gdiplus::GdiplusStartup(&g_gdiplusToken, &gdiplusStartupInput, nullptr);
1572+
13781573
// Register a message-only window class
13791574
WNDCLASSEXW wc = {};
13801575
wc.cbSize = sizeof(wc);
@@ -1387,13 +1582,13 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR lpCmdLine, int)
13871582
return 1;
13881583
}
13891584

1390-
// Register the overlay window class (white background, ARROW cursor)
1585+
// Register the overlay window class (layered per-pixel-alpha surface, ARROW cursor)
13911586
WNDCLASSEXW overlayWindowClass = {};
13921587
overlayWindowClass.cbSize = sizeof(overlayWindowClass);
13931588
overlayWindowClass.lpfnWndProc = DefWindowProcW;
13941589
overlayWindowClass.hInstance = hInstance;
13951590
overlayWindowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
1396-
overlayWindowClass.hbrBackground = static_cast<HBRUSH>(GetStockObject(WHITE_BRUSH));
1591+
overlayWindowClass.hbrBackground = nullptr; // per-pixel alpha via UpdateLayeredWindow
13971592
overlayWindowClass.lpszClassName = OVERLAY_CLASS_NAME;
13981593
if (!RegisterClassExW(&overlayWindowClass))
13991594
{
@@ -1482,6 +1677,11 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR lpCmdLine, int)
14821677
g_hOverlay = nullptr;
14831678
}
14841679
RemoveTrayIcon();
1680+
if (g_gdiplusToken)
1681+
{
1682+
Gdiplus::GdiplusShutdown(g_gdiplusToken);
1683+
g_gdiplusToken = 0;
1684+
}
14851685
TraceLoggingUnregister(g_hProvider);
14861686

14871687
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)