Skip to content

v0.1.8

Choose a tag to compare

@github-actions github-actions released this 24 Apr 18:22
· 492 commits to main since this release

Improvements

  • Hide focus border in fullscreen mode — the active window highlight border is now automatically hidden when a window is fullscreened (Ctrl+Alt+Shift+F) and restored when exiting fullscreen
  • Suppress border and drag ghost while paused — focus border hides when tiling is paused and restores on resume; drag ghost overlay is suppressed during pause and any visible ghost is dismissed on pause toggle

Bug Fixes

  • Fix unequal height distribution after minimizing — when a window is minimized from a multi-window column, stale window_min_heights constraints on sibling windows could pin them to their old sizes instead of redistributing evenly; mark_minimized now clears min-size constraints for all column siblings
  • Fix drag ghost showing wrong height with minimized windows — ghost overlay now counts only visible (non-minimized) windows when computing slot height, and converts the raw slot index to a visible-window index so the ghost position is correct when minimized windows precede it
  • Fix focus drift after animated commands (expel, move, etc.) — SetWindowPos during animation could trigger spurious OS focus events that silently overrode focused_column, causing subsequent resize/width commands to operate on the wrong column; the animation landing pass now re-syncs OS foreground to match the workspace's intended focus
  • Skip focus border on ignored windows — show_border now checks that the target hwnd is actually managed before drawing, preventing the border from appearing on windows matched by ignore rules (e.g. Steam Friends List)
  • Fix Slack/Spotify (and other Electron-style apps) overflowing their column boundaries — width-violation detection in the placement layer was using the global border-inset cache, which goes stale when an app changes how it renders its client frame at runtime. The frame-vs-frame math actual_w > requested + cached_insets then silently cancelled out, the violation was never reported, and the column was never widened. Detection now compares DwmGetWindowAttribute(EXTENDED_FRAME_BOUNDS) (the actual visible content rect) directly against the layout engine's requested rect, immune to cache staleness, and evicts the stale insets on any mismatch so the next SetWindowPos re-queries DWM
  • Add symmetric height-violation detection — windows that enforce a minimum height (e.g. Spotify, large media players) are now detected and propagated to the layout engine via a new window_min_heights map; the layout engine pins min-height windows to their minimum and distributes the remainder among flexible windows by weight, with the last window honoring its own minimum even when rounding eats pixels. Replaces the per-frame "Min-size fixup" band-aid that ran for ≥2-window columns and never informed the layout engine
  • Apply size-violation corrections on the same frame — after detection propagates min-width/min-height constraints, apply_layout now triggers a single guarded re-apply (via reapplying_after_violation) so the corrected layout lands on the current frame instead of waiting for the next user-triggered event. Inner re-apply errors propagate to the caller so daemon-paused states from a timed-out worker can't hide behind a successful outer apply
  • Display-change handler now also clears min_heights alongside min_widths — height constraints learned under one DPI/theme metric set could otherwise survive into the next state and distort intra-column distribution
  • Fix Chromium/Electron/Firefox windows getting stuck with broken internal viewports after rapid column scrolling — Beeper, Slack, Spotify, TradingView, Zen and similar apps previously required an app restart to recover their render target size after a rapid Ctrl+Alt+H/L burst. Root cause was a DirectComposition swap-chain desync: animation frames used SWP_ASYNCWINDOWPOS, the target app's UI thread coalesced the rapid position messages, and the final sync landing pass arrived with the same rect as the last async frame so the compositor saw "no size change" and never rebuilt its swap chain. The sync landing pass now sends a one-frame (w-1 → w) SetWindowPos pair for windows matching Chrome_WidgetWin_1 / MozillaWindowClass / CASCADIA_HOSTING_WINDOW_CLASS, forcing a real size delta the compositor observes and rebuilds around. Re-checks IsWindow and the class name between the pair so HWND recycling cannot resize an unrelated window; logs a warning if the restore leg fails (the next apply will correct the 1px strand)
  • Fix cascading full retiles during heavy background CPU load (cargo build, cargo test, any all-cores workload) — EVENT_OBJECT_LOCATIONCHANGE fires for many reasons besides actual movement (Z-order changes, DWM composition, focus shuffles, DPI nudges, app-internal size adjustments). The daemon treated all of them as user-initiated moves and snapped the window back via a full apply_layout, retiling every window on the active workspace. AppState now records the layout rect applied to each window in last_placed_layout_rects; the MovedOrResized handler queries the window's current visible bounds via DwmGetWindowAttribute(EXTENDED_FRAME_BOUNDS) and short-circuits the snap-back when actual and expected rects match within 20 px, so only genuine displacements trigger a retile
  • Fix off-workspace windows being yanked onto the active workspace during transient apply timeouts — when the apply-layout worker exceeded its timeout (common under heavy background CPU), the recovery pass called the panic-grade uncloak_all_visible_windows(), which enumerates every top-level window on the desktop and moves any parked at the MoveOffScreen sentinel back to the primary monitor. That routinely dragged e.g. Spotify on workspace 2 onto workspace 1. run_visibility_recovery_pass now keeps only the scoped operations (restore_windows_moved_offscreen on managed window IDs, uncloak_all_managed_windows); the panic-grade sweep is reserved for the crash/panic hooks where it belongs. APPLY_LAYOUT_TIMEOUT is also raised from 1500 ms to 5000 ms so transient CPU stalls don't trigger the recovery path in the first place
  • Fix stranded min-size constraints when column composition changes during a daemon stall — the constraint clear on add_window / insert_at / remove_window was eager, so if the subsequent apply_layout hit its timeout the column stayed with constraints cleared until the next user event re-triggered detection. Constraint clears now go through a new pending_min_size_clears set on Workspace drained at the start of both apply_layout and send_animation_frame; a timed-out worker can no longer leave the column in a stale state. Pending entries are pruned on actual window removal so the set cannot retain references to dead windows
  • Fix inflated min_height values (2-window columns becoming 75/50 instead of 50/50) detected during CPU pressure — the sync landing pass queried DwmGetWindowAttribute(EXTENDED_FRAME_BOUNDS) immediately after SetWindowPos, but under load the target thread hadn't yet processed WM_SIZE so DWM returned stale pre-resize bounds. The violation detector then recorded the oversized rect as a permanent min_height constraint, breaking subsequent layouts. A DwmFlush() now blocks for one vsync between placement and query, forcing the compositor to present a frame incorporating the new positions; an additional 1.5× sanity cap skips any violation where the reported dimension exceeds the requested size by more than 50 % (almost always a stale-bounds read) and logs a warning
  • Fix cargo test --all disrupting a live daemon — two platform-layer tests exercised real Win32 APIs against the desktop (test_uncloak_all_visible_windows_no_panic called the panic-recovery sweep; test_uncloak_all_managed_with_invalid_ids passed literal HWND values that could collide with a real window and move it). Both are now marked #[ignore] with an explanatory message; run with cargo test -- --ignored for panic-safety verification
  • Skip shell-cloaked windows at the Created event — suspended UWP frames and windows on other virtual desktops are valid HWNDs with WS_VISIBLE but no rendered content. New is_window_shell_cloaked(WindowId) wrapper exposed from platform_win32 keeps them out of the managed set