跪拜 Guibai
← All articles
Backend

Dragging a Desktop Pet in Rust: The HWND_TOP Trap That Silently Kills Always-On-Top

By 再吃一根胡萝卜 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Any Windows Rust project that mixes a windowing framework with direct Win32 calls can accidentally overwrite properties the framework set. HWND_TOP vs HWND_TOPMOST is a one-word difference that silently breaks always-on-top, and the failure only becomes visible when another window opens—making it easy to ship broken.

Summary

Building a tiny borderless, transparent, always-on-top desktop pet window in Rust with winit and softbuffer ran into three drag problems. The most obvious were visual ghosting from software rendering presents colliding with window moves, and a slight lag from coordinate truncation. The real bug was subtler: every drag called SetWindowPos with HWND_TOP, which silently demoted the window from the topmost Z-order layer back to the normal layer, breaking always-on-top. The fix uses HWND_TOPMOST, decouples redraws from moves via a size-change guard, switches to GetCursorPos for screen-coordinate consistency, and clamps the window to the monitor work area.

Takeaways
HWND_TOP places a window at the top of the normal Z-order and demotes a topmost window to normal; HWND_TOPMOST keeps it in the always-on-top layer.
Every SetWindowPos call that includes a Z-order parameter redefines the window’s layer unless SWP_NOZORDER is set.
winit 0.30’s CursorMoved event delivers PhysicalPosition, not logical coordinates—mixing units causes drag lag.
WM_SYSCOMMAND / SC_MOVE only works inside a native WndProc mouse-down message context; it is ignored when sent from an async event-loop callback.
Software rendering with softbuffer couples every present() to a full buffer blit; redrawing on every mouse-move tick produces ghosting.
Skipping all redraws during a drag can leave the window transparent if the system triggers a redraw for a DPI or visibility change.
A size-comparison guard—redraw only when the window size actually changes—eliminates ghosting without losing content.
Using GetCursorPos on mouse-down avoids a None starting position when no prior CursorMoved event has fired.
Clamp the drag target to rcWork (monitor work area excluding the taskbar), not rcMonitor, and guard against min > max panics.
MONITORINFO.cbSize must be set manually before calling GetMonitorInfoW, or the call fails.
Conclusions

The least visible symptom—always-on-top silently breaking—was the only actual correctness bug; the visual glitches were performance artifacts. This inversion of severity is common when native API calls quietly override framework state.

HWND_TOP and HWND_TOPMOST differ by only a few characters but have opposite effects on Z-order; the constant name suggests ‘top’ but actually means ‘top of the normal heap, not the topmost heap.’

The SC_MOVE trick is widely shared as a universal drag fix, but it depends on a synchronous message-loop context that event-driven frameworks like winit do not provide—making it a trap for Rust GUI developers.

Decoupling move from redraw via a size-change check is a minimal, low-risk pattern that avoids both ghosting and content loss, and generalizes to any software-rendered overlay window.

Concepts & terms
HWND_TOP vs HWND_TOPMOST
Windows Z-order constants. HWND_TOP places a window at the top of the normal window stack and strips any topmost status. HWND_TOPMOST places it in the always-on-top layer, above all non-topmost windows.
SetWindowPos Z-order side effect
Any SetWindowPos call that passes a Z-order handle (without SWP_NOZORDER) redefines the window’s layer. This can silently undo AlwaysOnTop set earlier by a framework.
WM_SYSCOMMAND / SC_MOVE context requirement
This message initiates a system-managed window drag, but only works when the calling thread is inside the synchronous mouse-button-down message handler. Async event-loop dispatch (winit, Tauri) breaks this context.
softbuffer present ghosting
softbuffer renders by blitting a full pixel buffer. When present() is called on every mouse-move tick during a drag, the blit and DWM compositing desynchronize, leaving visible frame residue.
rcWork vs rcMonitor
rcWork is the monitor’s work area excluding the taskbar and other reserved screen regions. rcMonitor is the full display rectangle. Clamping to rcWork keeps a window from hiding under the taskbar.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗