Building a Better Aero Snap

May 12, 2018

In Windows 7 Microsoft added a feature called AeroSnap. It’s key feature is to snap an application to the left or right half of your monitor via WinKey+Left/Right. It’s super duper handy for positioning two things side-by-side.

But this isn’t good enough. Sure it works fine with a landscape mode 1920x1080 monitor. It works less well on a monitor in 1080x1920 portrait mode. AeroSnap produces two super skinny 540x1920 windows on the left/right. When what I want is two 1080x960 windows split top/bottom. Or even three 1080x640 vertical windows if the display is large enough.

Unfortunately AeroSnap can’t do this. It’s one size fits all. Which means we’ll have to do it ourselves.

TL;DR

I built a customizable AeroSnap-like tool (for Windows) called fts_winsnap. It solves a simple problem in exactly the way I want. I use it everyday. It may or may not be useful to you.

Screenshot of Tool

Snapping Rectangles

This rest of this post discusses random problems I encountered while making fts_winsnap. None of them are mindblowing. But the Win32 API is esoteric and painful enough that hopefully some reader will benefit from my suffering.

The concept of fts_winsnap is simple. Define some rectangles and snap applications windows to those rects. Easy right? Ha!

What do you think happens if you call a MoveWindow(0, 0, 1920, 1080)? Did you guess place the top-left corner of a window in the top-left corner of a monitor and make it full screen? That sounds reasonable.

Nope! The top of your window will be along the top-edge of the screen. But there will be a mysterious ~8 pixel gap on the left and right. This function operates on the “client” area of a window. There is magical padding for… windows drop shadows? Scroll bar selection bounds? Maybe it made sense back on Windows 95.

We can calculate this difference and adjust for this oddity.

GetWindowRect(window, out windowRect);
GetClientRect(window, out clientRect);

int padding = (windowRect.Width - clientRect.Width)
x -= padding/2;
width += padding;
height += padding/2;

MoveWindow(windowHandle, x, y, width, height);

Minimize and Maximize

AeroSnap can minimize and maximize windows via WinKey+Up/Down. Replicating this proved to be surprisingly nuanced.

You can trivially minimize, maximize, or restore a window via ShowWindowAsync. The problem is when want you restore from maximized AND move in one seamless operation. Naive calls to ShowWindowAsync and MoveWindow cause the window to animate to it’s old position then snap to it’s new position. Flipping the call order doesn’t help.

The solution is to use SetWindowPlacement instead. This function takes the new bounds and new state at the same time.

It’s critical to maintain a window’s state flag. If you call maximize then move you must update the state to normal. Otherwise Windows will think the window is still maximized and attempts to re-maximize will silently fail. The upper-right corner icons will reflect this fake-maximized state.

Mixed DPI

At home I use a standard 1920x1080 monitor connected to a high-dpi 3200x1800 laptop at 250% scale. This turned out to be a huge pain in the ass. Windows 10 has gotten… slightly better at high-dpi. But it’s still pretty broken.

Laptops

We previously encountered some unexpected window padding issues. An off-by-one pixel error can cause a window that is 99.9% on a low-dpi monitor to inherit the high-dpi monitor scale. ¯\_(ツ)_/¯

Furthermore, moving a window from a low-dpi to a high-dpi is completely fubar. SetWindowPlacement will correctly place the upper-left corner of the window. But it also gives a width/height 2.5x larger than what you asked for. ¯\_(ツ)_/¯

My solution is to verify size after calling SetWindowPlacement. If it’s incorrect then recalculate padding and set pos/size again.

SetWindowPlacement(window, ref p);
GetWindowRect(window, out wr);

if (wr.Width != p.pos.Width || wr.Height != p.pos.Height) {
    // GetWindowRect + GetClientRect (again)
    // padding = blah
    // x, y, width, height = blah
    SetWindowPos(x, y, w, h, NO_SIZE | NO_REDRAW);
    SetWindowPos(x, y, w, h, NO_MOVE);
}

This produces the correct result. There is a brief flicker when moving a window between monitors. I could not find a way to eliminate it. :(

Algorithm

Windows puts all monitors in a global coordinate space. This lets users control relative monitor placement. Given a set of monitors each with a set of rectangles it’s not completely obvious how to move between them. The user shouldn’t have to manually define connections between rectangles.

fts_winsnap works with an abritrary set of non-overlapping rectangles. When the user hits Ctrl+Alt+ArrowKey the app determines what rectangle makes sense to move to next.

It first determines the “current” rectangle. If the window is not aligned to an existing rectangle then it snaps to the rectangle with the most overlap.

If the window is already aligned then it needs to find a new rectangle. This requires looking for the closest rect beyond the edge in the direction of movement. In this case of a 2x2 grid there may be two neighbors. The winner is whichever has the most overlap along the perpindicular axis.

Moving up from a window at the top of the screen causes a maximize operation. Moving up while maximized does either nothing OR moves to a new monitor if there is one. Minimize is similar.

The code assumes non-overlapping rects. It behaves poorly if rects overlap. It somewhat assumes complete coverage of a monitor. If for some reason you wanted gaps it might work? I promise nothing.

Misc

All calculations are performed in working space coordinates. These coords exclude the start menu which may be any size and on any edge of the monitor.

I use Windows hot keys to process input in fts_winsnap.exe even when another app is focused. Much to my surprise this worked out of the box with no issues. The only hangup is that the OS reserves the Windows key. Hrmph.

Minimize to tray was a mild pain. If you look at the source the relevant code has to do with _notifyIcon.

Running on startup was easy with some help from StackOverflow. It a simple registry setting.

Detecting that the app auto-ran on startup so it can auto-minimize to tray was less clean. I check the working directory to detect auto-start. Then I set the windowState to minimized and showInTaskbar to false.

fts_winsnap isn’t perfect. Unfortunately AeroSnap also has bugs and pixel “overflow”. For example you may get one or two pixels on another monitor. An extra pad param exists because of these bugs. Different setups require different magic numbers. Different apps also require different magic number. Excel and Visual Studio behave differently than Notepad and VS Code. These numbers may be calculable, but it’s not trivial. #ThanksMicrosoft

Laptop picture

I’ve only tested on Windows 10. I don’t care about Windows 7 or 8.

Cache invalidation is one of the two hard problems in computer science. As such fts_winsnap caches very little. It only does work on infrequent user input so performance is not a concern. This enables simple code that doesn’t have to detect events such as changes to the start menu.

Sometimes I want to snap windows to something other than a perfect one-by-two layout. Window’s AeroSnap doesn’t let me customize my snap alignments. So I did it myself. If you have a portrait or 21:9 monitor you too may find it useful.