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
4 changes: 2 additions & 2 deletions src/modules/GrabAndMove/GrabAndMove/GrabAndMove.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalDependencies>WindowsApp.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>WindowsApp.lib;dwmapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
Expand Down Expand Up @@ -208,4 +208,4 @@
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>
</Project>
276 changes: 275 additions & 1 deletion src/modules/GrabAndMove/GrabAndMove/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ static DWORD g_absorbedFlags = 0; // flags for replay (extended key, etc.)
static bool g_showGeometry = false; // true if we want to draw the X, Y, W and H on the overlay on move and resize
static bool g_doNotActivateOnGameMode = true; // true if GrabAndMove is suppressed when Windows Game Mode is active

static bool g_useAltResize = true; // This can be toggled from the settings. If false, Alt + right click does nothing.
static bool g_useAltResize = true; // This can be toggled from the settings. If false, Alt + right click does nothing.
static bool g_useScreenEdgeMaximize = true; // If true, dragging to the top screen edge maximizes/restores the window.
static bool g_useScreenEdgeSnap = true; // If true, dragging to side/corner screen edges snaps the window.

// Count of non-modifier keys currently held. Used to suppress GrabAndMove when the
// modifier key is pressed while another key is already down (e.g. Q held, then modifier pressed).
Expand All @@ -83,15 +85,30 @@ enum ResizeHandle
RESIZE_LEFT
};

enum class ScreenEdgeSnapTarget
{
None,
Maximize,
LeftHalf,
RightHalf,
TopLeft,
TopRight,
BottomLeft,
BottomRight,
};

static bool g_resizing = false;
static bool g_resizeFirstMove = false;
static HWND g_resizeTarget = nullptr;
static POINT g_resizeLast = {}; // cursor pos from previous frame
static RECT g_resizeWndRect = {}; // current window rect (updated each frame)
static ResizeHandle g_currentHandle = RESIZE_NONE;
static ScreenEdgeSnapTarget g_pendingSnapTarget = ScreenEdgeSnapTarget::None;
static RECT g_pendingSnapRect = {};

static const int MIN_WINDOW_WIDTH = 150;
static const int MIN_WINDOW_HEIGHT = 50;
static constexpr int SCREEN_EDGE_SNAP_THRESHOLD_PX = 16;

// Minimum interval (ms) between move/resize updates. Lower = snappier but
// more CPU/GPU work. 0 = unlimited (every mouse event triggers an update).
Expand Down Expand Up @@ -241,6 +258,230 @@ static void TraceShortcutUse(bool successful, GrabAndMoveShortcutAction action,
TraceLoggingWideString(reason, "Reason"));
}

static bool IsWindowResizable(HWND hwnd)
{
return (GetWindowLongPtrW(hwnd, GWL_STYLE) & WS_THICKFRAME) != 0;
}

static bool IsWindowMaximizable(HWND hwnd)
{
return (GetWindowLongPtrW(hwnd, GWL_STYLE) & WS_MAXIMIZEBOX) != 0;
}

static bool TryGetWorkAreaFromPoint(POINT pt, RECT& workArea)
{
HMONITOR monitor = MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST);
if (!monitor)
{
return false;
}

MONITORINFO monitorInfo = {};
monitorInfo.cbSize = sizeof(monitorInfo);
if (!GetMonitorInfoW(monitor, &monitorInfo))
{
return false;
}

workArea = monitorInfo.rcWork;
return true;
}

static bool TryGetVisibleFrameBounds(HWND hwnd, RECT& frameRect)
{
return SUCCEEDED(DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &frameRect, sizeof(frameRect))) && !IsRectEmpty(&frameRect);
}

static RECT GetWindowRectForVisibleFrame(HWND hwnd, const RECT& desiredFrameRect)
{
RECT windowRect = {};
RECT frameRect = {};
if (!GetWindowRect(hwnd, &windowRect) || !TryGetVisibleFrameBounds(hwnd, frameRect))
{
return desiredFrameRect;
}

return {
desiredFrameRect.left + (windowRect.left - frameRect.left),
desiredFrameRect.top + (windowRect.top - frameRect.top),
desiredFrameRect.right + (windowRect.right - frameRect.right),
desiredFrameRect.bottom + (windowRect.bottom - frameRect.bottom),
};
}

static bool SetWindowVisibleFrameToRect(HWND hwnd, const RECT& desiredFrameRect, UINT flags = 0)
{
RECT adjustedRect = GetWindowRectForVisibleFrame(hwnd, desiredFrameRect);
const int w = adjustedRect.right - adjustedRect.left;
const int h = adjustedRect.bottom - adjustedRect.top;
if (w <= 0 || h <= 0)
{
return false;
}

return SetWindowPos(
hwnd,
nullptr,
adjustedRect.left,
adjustedRect.top,
w,
h,
SWP_NOZORDER | SWP_NOACTIVATE | flags) != FALSE;
}

static RECT GetScreenEdgeSnapRect(ScreenEdgeSnapTarget target, const RECT& workArea)
{
const int midX = workArea.left + (workArea.right - workArea.left) / 2;
const int midY = workArea.top + (workArea.bottom - workArea.top) / 2;

switch (target)
{
case ScreenEdgeSnapTarget::Maximize:
return workArea;
case ScreenEdgeSnapTarget::LeftHalf:
return { workArea.left, workArea.top, midX, workArea.bottom };
case ScreenEdgeSnapTarget::RightHalf:
return { midX, workArea.top, workArea.right, workArea.bottom };
case ScreenEdgeSnapTarget::TopLeft:
return { workArea.left, workArea.top, midX, midY };
case ScreenEdgeSnapTarget::TopRight:
return { midX, workArea.top, workArea.right, midY };
case ScreenEdgeSnapTarget::BottomLeft:
return { workArea.left, midY, midX, workArea.bottom };
case ScreenEdgeSnapTarget::BottomRight:
return { midX, midY, workArea.right, workArea.bottom };
default:
return {};
}
}

static ScreenEdgeSnapTarget GetScreenEdgeSnapTarget(POINT pt, const RECT& workArea)
{
const bool nearLeft = pt.x <= workArea.left + SCREEN_EDGE_SNAP_THRESHOLD_PX;
const bool nearRight = pt.x >= workArea.right - SCREEN_EDGE_SNAP_THRESHOLD_PX;
const bool nearTop = pt.y <= workArea.top + SCREEN_EDGE_SNAP_THRESHOLD_PX;
const bool nearBottom = pt.y >= workArea.bottom - SCREEN_EDGE_SNAP_THRESHOLD_PX;

if (nearTop && nearLeft)
{
return ScreenEdgeSnapTarget::TopLeft;
}

if (nearTop && nearRight)
{
return ScreenEdgeSnapTarget::TopRight;
}

if (nearBottom && nearLeft)
{
return ScreenEdgeSnapTarget::BottomLeft;
}

if (nearBottom && nearRight)
{
return ScreenEdgeSnapTarget::BottomRight;
}

if (nearTop)
{
return ScreenEdgeSnapTarget::Maximize;
}

if (nearLeft)
{
return ScreenEdgeSnapTarget::LeftHalf;
}

if (nearRight)
{
return ScreenEdgeSnapTarget::RightHalf;
}

return ScreenEdgeSnapTarget::None;
}

static bool IsScreenEdgeSnapTargetAllowed(HWND hwnd, ScreenEdgeSnapTarget target)
{
if (target == ScreenEdgeSnapTarget::None)
{
return false;
}

if (target == ScreenEdgeSnapTarget::Maximize)
{
return IsWindowMaximizable(hwnd);
}

return IsWindowResizable(hwnd);
}

static bool IsScreenEdgeSnapTargetEnabled(ScreenEdgeSnapTarget target)
{
if (target == ScreenEdgeSnapTarget::Maximize)
{
return g_useScreenEdgeMaximize;
}

return target != ScreenEdgeSnapTarget::None && g_useScreenEdgeSnap;
}

static void UpdatePendingScreenEdgeSnap(POINT pt)
{
g_pendingSnapTarget = ScreenEdgeSnapTarget::None;
g_pendingSnapRect = {};

if (!g_dragTarget)
{
return;
}

RECT workArea = {};
if (!TryGetWorkAreaFromPoint(pt, workArea))
{
return;
}

ScreenEdgeSnapTarget target = GetScreenEdgeSnapTarget(pt, workArea);
if (!IsScreenEdgeSnapTargetEnabled(target) || !IsScreenEdgeSnapTargetAllowed(g_dragTarget, target))
{
return;
}

g_pendingSnapTarget = target;
g_pendingSnapRect = GetScreenEdgeSnapRect(target, workArea);
}

static bool ToggleWindowMaximized(HWND hwnd)
{
if (!hwnd || !IsWindowMaximizable(hwnd))
{
return false;
}

ShowWindow(hwnd, IsZoomed(hwnd) ? SW_RESTORE : SW_MAXIMIZE);
return true;
}

static bool ApplyPendingScreenEdgeSnap()
{
if (!g_dragTarget || !IsScreenEdgeSnapTargetAllowed(g_dragTarget, g_pendingSnapTarget))
{
return false;
}

if (g_pendingSnapTarget == ScreenEdgeSnapTarget::Maximize)
{
return ToggleWindowMaximized(g_dragTarget);
}

if (IsZoomed(g_dragTarget))
{
ShowWindow(g_dragTarget, SW_RESTORE);
}

return SetWindowVisibleFrameToRect(g_dragTarget, g_pendingSnapRect);
}

// ---------------------------------------------------------------------------
// Settings file helpers
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -270,6 +511,16 @@ static void LoadSettingsFromFile()
g_useAltResize = *v;
}

if (auto v = values.get_bool_value(L"useScreenEdgeMaximize"))
{
g_useScreenEdgeMaximize = *v;
}

if (auto v = values.get_bool_value(L"useScreenEdgeSnap"))
{
g_useScreenEdgeSnap = *v;
}

if (auto v = values.get_int_value(L"modifierKey"))
{
g_modifierKey = (*v == 1) ? GrabAndMoveModifier::Win : GrabAndMoveModifier::Alt;
Expand Down Expand Up @@ -509,6 +760,8 @@ static void StopDragging()
g_dragging = false;
g_dragFirstMove = false;
g_dragTarget = nullptr;
g_pendingSnapTarget = ScreenEdgeSnapTarget::None;
g_pendingSnapRect = {};
HideOverlay();
}

Expand Down Expand Up @@ -1045,6 +1298,8 @@ static LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
g_dragTarget = hwnd;
g_dragStart = pt;
GetWindowRect(hwnd, &g_dragWndRect);
g_pendingSnapTarget = ScreenEdgeSnapTarget::None;
g_pendingSnapRect = {};

// Show the semi-transparent overlay on top of the target (persistent window – fix #9)
ShowOverlay(g_dragWndRect, g_curSizeAll);
Expand Down Expand Up @@ -1078,6 +1333,13 @@ static LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
int newY = g_dragWndRect.top + dy;
int w = g_dragWndRect.right - g_dragWndRect.left;
int h = g_dragWndRect.bottom - g_dragWndRect.top;
UpdatePendingScreenEdgeSnap(pt);
if (g_pendingSnapTarget != ScreenEdgeSnapTarget::None && ApplyPendingScreenEdgeSnap())
{
EndInteraction(true, false);
return 1;
}

SetWindowPos(g_dragTarget, nullptr, newX, newY, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS);
EndInteraction(true, false);
return 1; // swallow the release
Expand Down Expand Up @@ -1193,10 +1455,22 @@ static void HandleDragMove(POINT pt)
int w = g_dragWndRect.right - g_dragWndRect.left;
int h = g_dragWndRect.bottom - g_dragWndRect.top;

UpdatePendingScreenEdgeSnap(pt);
if (g_pendingSnapTarget != ScreenEdgeSnapTarget::None)
{
RepositionOverlay(
g_pendingSnapRect.left,
g_pendingSnapRect.top,
g_pendingSnapRect.right - g_pendingSnapRect.left,
g_pendingSnapRect.bottom - g_pendingSnapRect.top);
return;
}

// Move target + overlay (separate SetWindowPos – DeferWindowPos doesn't
// work reliably for cross-process target windows)
SetWindowPos(g_dragTarget, nullptr, newX, newY, 0, 0,
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS);

RepositionOverlay(newX, newY, w, h);
}

Expand Down
1 change: 1 addition & 0 deletions src/modules/GrabAndMove/GrabAndMove/pch.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#define WIN32_LEAN_AND_MEAN

#include <windows.h>
#include <dwmapi.h>
#include <shellapi.h>
#include <commctrl.h>
#include <TraceLoggingProvider.h>
Expand Down
8 changes: 8 additions & 0 deletions src/settings-ui/Settings.UI.Library/GrabAndMoveProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public GrabAndMoveProperties()
DoNotActivateOnGameMode = new BoolProperty(true);
ShowGeometry = new BoolProperty(false);
UseAltResize = new BoolProperty(true);
UseScreenEdgeMaximize = new BoolProperty(true);
UseScreenEdgeSnap = new BoolProperty(true);
ExcludedApps = new StringProperty();
ModifierKey = new IntProperty(0); // 0 = Alt, 1 = Win
}
Expand All @@ -30,6 +32,12 @@ public GrabAndMoveProperties()
[JsonPropertyName("useAltResize")]
public BoolProperty UseAltResize { get; set; }

[JsonPropertyName("useScreenEdgeMaximize")]
public BoolProperty UseScreenEdgeMaximize { get; set; }

[JsonPropertyName("useScreenEdgeSnap")]
public BoolProperty UseScreenEdgeSnap { get; set; }

[JsonPropertyName("doNotActivateOnGameMode")]
public BoolProperty DoNotActivateOnGameMode { get; set; }

Expand Down
Loading