diff --git a/.github/actions/spell-check/allow/code.txt b/.github/actions/spell-check/allow/code.txt index 49828e96aa3c..1bec4be5e98d 100644 --- a/.github/actions/spell-check/allow/code.txt +++ b/.github/actions/spell-check/allow/code.txt @@ -17,8 +17,8 @@ LIGHTTURQUOISE NCol OLIVEGREEN PALEBLUE -PArgb -Pbgra +pargb +pbgra SRGBTo WHITEONBLACK diff --git a/src/modules/GrabAndMove/GrabAndMove/GrabAndMove.vcxproj b/src/modules/GrabAndMove/GrabAndMove/GrabAndMove.vcxproj index 26db82fb6d6e..98a2ba8ae09a 100644 --- a/src/modules/GrabAndMove/GrabAndMove/GrabAndMove.vcxproj +++ b/src/modules/GrabAndMove/GrabAndMove/GrabAndMove.vcxproj @@ -88,7 +88,7 @@ - WindowsApp.lib;%(AdditionalDependencies) + WindowsApp.lib;Gdiplus.lib;Dwmapi.lib;%(AdditionalDependencies) diff --git a/src/modules/GrabAndMove/GrabAndMove/main.cpp b/src/modules/GrabAndMove/GrabAndMove/main.cpp index 505b8359dd17..93d91a5f32d1 100644 --- a/src/modules/GrabAndMove/GrabAndMove/main.cpp +++ b/src/modules/GrabAndMove/GrabAndMove/main.cpp @@ -10,6 +10,8 @@ #include "resource.h" +#include + TRACELOGGING_DEFINE_PROVIDER( g_hProvider, "Microsoft.PowerToys", @@ -21,6 +23,7 @@ TRACELOGGING_DEFINE_PROVIDER( // Globals // --------------------------------------------------------------------------- static HINSTANCE g_hInstance = nullptr; +static ULONG_PTR g_gdiplusToken = 0; // GDI+ token for overlay border rendering static HHOOK g_hhkKeyboard = nullptr; static HHOOK g_hhkMouse = nullptr; static HWND g_hMsgWnd = nullptr; @@ -47,6 +50,29 @@ static HWND g_hOverlay = nullptr; // semi-transparent overlay during drag static int g_overlayInfoX = 0, g_overlayInfoY = 0; static int g_overlayInfoW = 0, g_overlayInfoH = 0; +// Visible frame overlay metrics. Computed once per drag/resize (cold path) and +// reused while rendering - never recomputed in the mouse-move hot path. +// Margins are the difference between GetWindowRect and the DWM extended frame +// bounds (the invisible resize border), so the fill and border hug the visible +// window. The border is drawn just inside the visible edge; Always On Top draws +// its own border just outside that edge, so the two stack into a clean double +// layer without Grab and Move having to widen its stroke. +static int g_overlayMarginL = 0, g_overlayMarginT = 0, g_overlayMarginR = 0, g_overlayMarginB = 0; +static int g_overlayCornerRadius = 0; // physical px; 0 = square corners +static int g_overlayBorderThickness = 4; // physical px + +// Fluent "warning" gold - copy of WinUI SystemFillColorCaution +// (used as a ThemeResource for warnings across the Settings UI). A Win32 layered +// window can't resolve a ThemeResource, so the literal is required here. +static constexpr COLORREF OVERLAY_BORDER_COLOR = RGB(255, 185, 0); // #FFB900 + +// Border thickness in DIPs (scaled by the target window DPI). +static constexpr int OVERLAY_BORDER_DIP = 4; + +// Translucent white wash painted over the visible window during a drag/resize, +// matching the prior overlay. ~40% opacity (premultiplied white = 0x66666666). +static constexpr BYTE OVERLAY_FILL_ALPHA = 0x66; + static bool g_shouldAbsorbAlt = 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) static bool g_altAbsorbed = false; // true if we absorbed an Alt keydown @@ -343,9 +369,121 @@ static void SettingsWatcherThread(DWORD mainThreadId) static int g_overlayRenderedW = 0; static int g_overlayRenderedH = 0; +// Maps the DWM window corner preference to a base radius in DIPs, matching +// Always On Top (WindowCornerUtils::CornersRadius). +static int CornerRadiusForWindow(HWND hwnd) +{ + int pref = 0; // DWMWCP_DEFAULT + if (DwmGetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, &pref, sizeof(pref)) != S_OK) + { + return 0; // pre-Win11 / unsupported -> square corners + } + + switch (pref) + { + case DWMWCP_ROUND: + return 8; + case DWMWCP_ROUNDSMALL: + return 4; + case DWMWCP_DEFAULT: + return 8; + default: + return 0; // DWMWCP_DONOTROUND + } +} + +// Computes the overlay metrics (margins to the visible frame, corner radius, border +// thickness) for the target window. Cold path only: called at the start of a +// drag/resize and after un-maximize, never from the mouse-move hot path. +static void PrepareOverlayMetrics(HWND target) +{ + g_overlayMarginL = g_overlayMarginT = g_overlayMarginR = g_overlayMarginB = 0; + g_overlayCornerRadius = 0; + g_overlayBorderThickness = OVERLAY_BORDER_DIP; + + if (!target) + { + return; + } + + const UINT dpi = GetDpiForWindow(target); + const float scale = (dpi != 0) ? dpi / 96.0f : 1.0f; + + RECT windowRect{}; + RECT frameRect{}; + if (GetWindowRect(target, &windowRect) && + SUCCEEDED(DwmGetWindowAttribute(target, DWMWA_EXTENDED_FRAME_BOUNDS, &frameRect, sizeof(frameRect)))) + { + g_overlayMarginL = max(0, static_cast(frameRect.left - windowRect.left)); + g_overlayMarginT = max(0, static_cast(frameRect.top - windowRect.top)); + g_overlayMarginR = max(0, static_cast(windowRect.right - frameRect.right)); + g_overlayMarginB = max(0, static_cast(windowRect.bottom - frameRect.bottom)); + } + + g_overlayCornerRadius = static_cast(CornerRadiusForWindow(target) * scale); + g_overlayBorderThickness = static_cast(OVERLAY_BORDER_DIP * scale); +} + +// Draws an antialiased (optionally rounded) border stroke fully inside `rect` using +// GDI+. The stroke hugs the inner edge of `rect` (the visible window frame). +static void DrawOverlayBorder(Gdiplus::Graphics& graphics, const RECT& rect, int thickness, int radius) +{ + const int w = rect.right - rect.left; + const int h = rect.bottom - rect.top; + if (w <= 0 || h <= 0 || thickness <= 0) + { + return; + } + + // Keep the whole stroke inside the visible frame on every side. + thickness = min(thickness, min(w, h) / 2); + if (thickness <= 0) + { + return; + } + + const float half = thickness / 2.0f; + const Gdiplus::RectF path( + rect.left + half, + rect.top + half, + static_cast(w) - thickness, + static_cast(h) - thickness); + + graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias); + Gdiplus::Pen pen( + Gdiplus::Color(255, GetRValue(OVERLAY_BORDER_COLOR), GetGValue(OVERLAY_BORDER_COLOR), GetBValue(OVERLAY_BORDER_COLOR)), + static_cast(thickness)); + + if (radius <= 0) + { + graphics.DrawRectangle(&pen, path); + return; + } + + // The stroke is centred, so the path corner radius is the window radius minus + // half the thickness; that keeps the outer edge aligned with the window corner. + const float pathRadius = max(0.0f, radius - half); + const float diameter = min(pathRadius * 2.0f, min(path.Width, path.Height)); + if (diameter <= 0.0f) + { + graphics.DrawRectangle(&pen, path); + return; + } + + Gdiplus::GraphicsPath border; + border.AddArc(path.X, path.Y, diameter, diameter, 180.0f, 90.0f); + border.AddArc(path.GetRight() - diameter, path.Y, diameter, diameter, 270.0f, 90.0f); + border.AddArc(path.GetRight() - diameter, path.GetBottom() - diameter, diameter, diameter, 0.0f, 90.0f); + border.AddArc(path.X, path.GetBottom() - diameter, diameter, diameter, 90.0f, 90.0f); + border.CloseFigure(); + graphics.DrawPath(&pen, &border); +} + // Renders the overlay surface using per-pixel alpha via UpdateLayeredWindow. -// The white background is painted at ~40% opacity; the geometry label box is -// painted fully opaque so it remains legible regardless of what is beneath. +// A translucent white wash covers the visible window (matching the prior overlay) +// with a tight warning-gold border on top, both hugging the visible window frame; +// the optional geometry label box is painted fully opaque so it remains legible +// regardless of what is beneath. static void RenderOverlayContent(HWND hwnd, int cw, int ch) { if (!hwnd || cw <= 0 || ch <= 0) @@ -372,8 +510,52 @@ static void RenderOverlayContent(HWND hwnd, int cw, int ch) HDC memDC = CreateCompatibleDC(screenDC); HBITMAP hOldBmp = static_cast(SelectObject(memDC, hDib)); - // Premultiplied white at ~40% opacity: A=0x66, R=G=B=0x66 → 0x66666666 - memset(pBits, 0x66, static_cast(cw) * ch * sizeof(DWORD)); + // Start fully transparent. + memset(pBits, 0, static_cast(cw) * ch * sizeof(DWORD)); + + // We apply a translucent white rect with a gold border. + // The overlay window spans GetWindowRect, so inset by + // the invisible-border margins so both hug the visible edge; Always On Top draws + // its own border just outside that edge, giving a clean double layer. + { + const RECT visible = { + g_overlayMarginL, + g_overlayMarginT, + cw - g_overlayMarginR, + ch - g_overlayMarginB + }; + const int vw = visible.right - visible.left; + const int vh = visible.bottom - visible.top; + + Gdiplus::Bitmap bitmap(cw, ch, cw * 4, PixelFormat32bppPARGB, reinterpret_cast(pBits)); + Gdiplus::Graphics graphics(&bitmap); + graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias); + + if (vw > 0 && vh > 0) + { + Gdiplus::SolidBrush fillBrush(Gdiplus::Color(OVERLAY_FILL_ALPHA, 255, 255, 255)); + if (g_overlayCornerRadius > 0) + { + // Round the wash to match the window corners (and the border). + const float d = min(static_cast(g_overlayCornerRadius) * 2.0f, + static_cast(min(vw, vh))); + Gdiplus::GraphicsPath fillPath; + fillPath.AddArc(static_cast(visible.left), static_cast(visible.top), d, d, 180.0f, 90.0f); + fillPath.AddArc(static_cast(visible.right) - d, static_cast(visible.top), d, d, 270.0f, 90.0f); + fillPath.AddArc(static_cast(visible.right) - d, static_cast(visible.bottom) - d, d, d, 0.0f, 90.0f); + fillPath.AddArc(static_cast(visible.left), static_cast(visible.bottom) - d, d, d, 90.0f, 90.0f); + fillPath.CloseFigure(); + graphics.FillPath(&fillBrush, &fillPath); + } + else + { + graphics.FillRectangle(&fillBrush, visible.left, visible.top, vw, vh); + } + } + + DrawOverlayBorder(graphics, visible, g_overlayBorderThickness, g_overlayCornerRadius); + graphics.Flush(); + } if (g_showGeometry) { @@ -1045,6 +1227,7 @@ static LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam) g_dragTarget = hwnd; g_dragStart = pt; GetWindowRect(hwnd, &g_dragWndRect); + PrepareOverlayMetrics(hwnd); // Show the semi-transparent overlay on top of the target (persistent window – fix #9) ShowOverlay(g_dragWndRect, g_curSizeAll); @@ -1112,6 +1295,7 @@ static LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam) g_resizeTarget = hwnd; g_resizeLast = pt; GetWindowRect(hwnd, &g_resizeWndRect); + PrepareOverlayMetrics(hwnd); g_currentHandle = GetClosestHandle(pt, g_resizeWndRect); ShowOverlay(g_resizeWndRect, CursorForHandle(g_currentHandle)); @@ -1183,6 +1367,9 @@ static void HandleDragMove(POINT pt) g_dragStart = pt; g_dragWndRect = {newX, newY, newX + restoredW, newY + restoredH}; + + // Corner radius / invisible-border margins differ once restored. + PrepareOverlayMetrics(g_dragTarget); } } @@ -1230,6 +1417,9 @@ static void HandleDragResize(POINT pt) SWP_NOZORDER | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS); g_resizeWndRect = {newLeft, newTop, newLeft + newW, newTop + newH}; + // Corner radius / invisible-border margins differ once restored. + PrepareOverlayMetrics(g_resizeTarget); + g_resizeLast = pt; g_currentHandle = GetClosestHandle(pt, g_resizeWndRect); } @@ -1375,6 +1565,10 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR lpCmdLine, int) INITCOMMONCONTROLSEX commonControls = { sizeof(commonControls), ICC_STANDARD_CLASSES }; InitCommonControlsEx(&commonControls); + // Initialise GDI+ for antialiased overlay border rendering + Gdiplus::GdiplusStartupInput gdiplusStartupInput; + Gdiplus::GdiplusStartup(&g_gdiplusToken, &gdiplusStartupInput, nullptr); + // Register a message-only window class WNDCLASSEXW wc = {}; wc.cbSize = sizeof(wc); @@ -1387,13 +1581,13 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR lpCmdLine, int) return 1; } - // Register the overlay window class (white background, ARROW cursor) + // Register the overlay window class (layered per-pixel-alpha surface, ARROW cursor) WNDCLASSEXW overlayWindowClass = {}; overlayWindowClass.cbSize = sizeof(overlayWindowClass); overlayWindowClass.lpfnWndProc = DefWindowProcW; overlayWindowClass.hInstance = hInstance; overlayWindowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW); - overlayWindowClass.hbrBackground = static_cast(GetStockObject(WHITE_BRUSH)); + overlayWindowClass.hbrBackground = nullptr; // per-pixel alpha via UpdateLayeredWindow overlayWindowClass.lpszClassName = OVERLAY_CLASS_NAME; if (!RegisterClassExW(&overlayWindowClass)) { @@ -1482,6 +1676,11 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR lpCmdLine, int) g_hOverlay = nullptr; } RemoveTrayIcon(); + if (g_gdiplusToken) + { + Gdiplus::GdiplusShutdown(g_gdiplusToken); + g_gdiplusToken = 0; + } TraceLoggingUnregister(g_hProvider); return static_cast(msg.wParam); diff --git a/src/modules/GrabAndMove/GrabAndMove/pch.h b/src/modules/GrabAndMove/GrabAndMove/pch.h index e3a8aa12ea96..4c8e9b08c0b3 100644 --- a/src/modules/GrabAndMove/GrabAndMove/pch.h +++ b/src/modules/GrabAndMove/GrabAndMove/pch.h @@ -3,6 +3,8 @@ #define WIN32_LEAN_AND_MEAN #include +#include +#include #include #include #include diff --git a/src/modules/alwaysontop/AlwaysOnTop/Settings.h b/src/modules/alwaysontop/AlwaysOnTop/Settings.h index eda785bb8ad4..ef9d60c1d054 100644 --- a/src/modules/alwaysontop/AlwaysOnTop/Settings.h +++ b/src/modules/alwaysontop/AlwaysOnTop/Settings.h @@ -29,7 +29,7 @@ struct Settings bool roundCornersEnabled = true; bool blockInGameMode = true; bool frameAccentColor = true; - int frameThickness = 15; + int frameThickness = 4; int frameOpacity = 100; COLORREF frameColor = RGB(0, 173, 239); std::vector excludedApps{}; diff --git a/src/settings-ui/Settings.UI.Library/AlwaysOnTopProperties.cs b/src/settings-ui/Settings.UI.Library/AlwaysOnTopProperties.cs index 2ca0b95b206e..80672b80a85d 100644 --- a/src/settings-ui/Settings.UI.Library/AlwaysOnTopProperties.cs +++ b/src/settings-ui/Settings.UI.Library/AlwaysOnTopProperties.cs @@ -15,7 +15,7 @@ public class AlwaysOnTopProperties public static readonly HotkeySettings DefaultDecreaseOpacityHotkeyValue = new HotkeySettings(true, true, false, false, 0xBD); public const bool DefaultFrameEnabled = true; public const bool DefaultShowInSystemMenu = false; - public const int DefaultFrameThickness = 15; + public const int DefaultFrameThickness = 4; public const string DefaultFrameColor = "#0099cc"; public const bool DefaultFrameAccentColor = true; public const int DefaultFrameOpacity = 100;