Skip to content

feat: grab-and-move — move windows by modifier + drag from anywhere (incl. drag-drop tree fix)#1407

Open
KhangHLe wants to merge 2 commits into
glzr-io:mainfrom
KhangHLe:feat/grab-and-move
Open

feat: grab-and-move — move windows by modifier + drag from anywhere (incl. drag-drop tree fix)#1407
KhangHLe wants to merge 2 commits into
glzr-io:mainfrom
KhangHLe:feat/grab-and-move

Conversation

@KhangHLe

Copy link
Copy Markdown

Summary

Grab-and-move: holding a modifier key (alt or win) and left-dragging from anywhere on a managed window moves it — with full title-bar-drag semantics (tiling windows reflow on drop, floating windows reposition, ignored windows are untouched). This is the only mouse-move path for title-bar-less windows (terminals like WezTerm, custom-frame apps), and matches the feature PowerToys recently shipped as "Grab And Move" — but WM-aware.

Also includes a standalone fix for drag-drop tree corruption that is reachable from native title-bar drags today (first commit — independently cherry-pickable).

Config

general:
  grab_and_move:
    enabled: true   # default: false
    modifier: 'alt' # or 'win'

Implementation notes

  • WH_MOUSE_LL hook on the dispatcher thread, modeled on the existing keyboard_hook.rs. The modifier+click is swallowed (the app never sees it) and a drag session starts; a dedicated mover thread applies the newest cursor position with synchronous SetWindowPos, coalescing so a slow window (e.g. Firefox) receives one current move whenever it's ready instead of a rubber-banding backlog. The hook itself does no syscalls on mouse-moves — one atomic load when idle.
  • Why manual moving: delegating to the native modal move loop was tried first — posting WM_NCLBUTTONDOWN/HTCAPTION, then WM_SYSCOMMAND/SC_MOVE|2, with and without swallowing the click — and custom-frame apps (Firefox, WezTerm) ignore all of it. Those are exactly the windows this feature exists for.
  • Only managed windows are draggable: the WM pushes its managed-window set to the hook on every WM event (diff-checked), so window rules are respected with no per-click WM round-trip.
  • Drag lifecycle events feed the same interactive move start/end handling as a native title-bar drag (active_drag, tiling reflow) via a small process_window_drag method.
  • Modifier detection uses GetKeyState (the same mechanism keybinding matching uses on this thread) — GetAsyncKeyState reads modifiers as released from the hook context.
  • macOS: inert no-op stub (WH_MOUSE_LL is Windows-only).

The bundled fix (first commit)

drop_as_tiling_window resolved target_parent/nearest_container before update_window_state(→ Tiling) mutates the tree (re-inserting the window and flattening redundant split containers — move_container_within_tree's own doc warns the flatten "can cause the target parent to become detached"). Dropping a tiling window where no re-tile target exists could then fail with "No common ancestor between containers" and reparent a real window into an orphaned subtree, after which every platform_sync errors "Window has no workspace" forever. The fix resolves drop targets after the conversion, excludes the moved window from its own target search, and clears active_drag even when the drop errors.

Testing

Built and daily-driven on Windows 11: tiling drops on all four quadrants (split + insertion), drops with no re-tile target (the previous error case), floating drags, ignored windows passing clicks through, plain clicks unaffected, WM pause/config-reload handling, drag smoothness on slow-frame apps (Firefox) via the coalescing mover.


This lives in and ships from KhangHLe/glazewm (with prebuilt releases) for anyone who wants it before it lands here. Happy to split the fix into its own PR, adjust config naming, or rework anything reviewers prefer.

— Ada

KhangHLe added 2 commits July 12, 2026 14:20
drop_as_tiling_window captured target_parent and nearest_container
BEFORE calling update_window_state(-> Tiling), which mutates the tree:
it re-inserts the window at its remembered tiling position and can
flatten redundant split containers (move_container_within_tree's own
doc warns this 'can cause the target parent to become detached'). The
subsequent wrap/move against those stale references then fails with
'No common ancestor between containers' - and when the wrap succeeded
against a detached parent first, the nearest window got reparented
into an orphaned subtree, making platform_sync error 'Window has no
workspace' on every event thereafter.

- Convert to tiling FIRST; resolve containers_at_point/target_parent/
  nearest_container against the post-mutation tree, excluding the
  moved window itself (it may now be a tiling child at the target).
- The empty-workspace branch no longer needs its own late conversion.
- Clear active_drag even when the drop errors (on whichever container
  instance now represents the window) so a failed drop can't leave
  every subsequent event re-entering drag handling.

Reachable from native title-bar drags too; surfaced immediately by
grab-and-move since it makes arbitrary drop points easy to produce.
Holding a modifier key (alt or win) and left-dragging anywhere on a
managed window moves it, exactly like a title-bar drag — the feature
that makes title-bar-less windows (WezTerm, yazi popups) mouse-movable
at all. Opt-in via config:

  general:
    grab_and_move:
      enabled: true
      modifier: 'alt'

Architecture (WH_MOUSE_LL hook on the dispatcher thread, modeled on
keyboard_hook):

- The hook swallows the modifier+click (the app never sees it) and
  runs a manual drag session: a dedicated mover thread applies the
  newest cursor position with synchronous SetWindowPos, coalescing so
  a slow window (Firefox) gets one current move when ready instead of
  a rubber-banding backlog of stale ones. The hook thread itself does
  no syscalls on moves — one atomic load when idle.
- Delegating to the native modal move loop was tried first (posting
  WM_NCLBUTTONDOWN/HTCAPTION, then WM_SYSCOMMAND/SC_MOVE|2, with and
  without swallowing the click) and is ignored by custom-frame apps
  (Firefox, WezTerm) — exactly the windows this feature targets.
- Only managed windows are draggable: the WM pushes its window set to
  the hook on every WM event (diff-checked), so 'ignore' rules (games)
  pass clicks through untouched.
- Drag lifecycle events feed WindowManager::process_window_drag, which
  runs the same interactive move start/end handling as a native
  title-bar drag — active_drag and tiling reflow behave identically.
- Modifier state via GetKeyState (the mechanism keybinding matching
  already uses on this thread); GetAsyncKeyState read modifiers as
  released from the hook context.
- macOS: inert no-op stub (WH_MOUSE_LL is Windows-only).
@github-project-automation github-project-automation Bot moved this to 📬 Needs triage in glazewm Jul 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: 📬 Needs triage

Development

Successfully merging this pull request may close these issues.

1 participant