Dragging the only window of a space onto a single-window space applies no frames (mouse_drop_action_warp)
- Dominant language
- C
- Stars
- 29.6k
- Forks
- 758
- PR merge metrics
- No merged PRs in 30d
Description
### Summary
`mouse_drop_action_warp()` skips capturing any windows when the dragged window was the only window in its source space **and** the destination space held exactly one window. `window_manager_animate_window_list()` is then called with an empty list, so no frame is applied: the bsp tree is updated correctly, but the dragged window keeps the frame it had on its previous display and overlaps the destination window until some unrelated event forces a relayout.
Reproduced on v7.1.25 and on master (`dd84572`).
### Steps to reproduce
Two displays, `layout bsp`, `mouse_drop_action warp` (also reproduces with `swap` configured, since the no-target path ends up in `warp`).
1. Prepare an empty space (e.g. space 3 on display 1) and a space holding exactly one window (e.g. space 4 on display 2).
2. Focus the empty space and open an application so it creates its first window there:
```
yabai -m space --focus 3
open -a "Visual Studio Code"
```
3. Drag that window with the mouse (no modifier) onto the window in space 4 and drop it.
**Expected:** both windows are tiled side by side on display 2.
**Actual:** the dropped window keeps its old frame (the full-screen size of display 1) and is drawn on top of the destination window. `yabai -m query --windows --space 4` shows both windows correctly attached to the space; only the frames are stale. `yabai -m space 4 --balance` fixes the display.
Using `mouse_modifier` + drag does not reproduce, because that path resizes the window through yabai itself.
### Cause
In `src/mouse_handler.c`, `mouse_drop_action_warp()`:
```c
struct window_node *src_node_rm = view_remove_window_node(src_view, src_window);
...
struct window_node *src_node_add = view_add_window_node_with_insertion_point(dst_view, src_window, dst_window->id);
...
struct window_capture *window_list = NULL;
if (src_node_rm) {
window_node_capture_windows(src_node_rm, &window_list);
}
if (src_node_rm != src_node_add && src_node_rm != src_node_add->parent) {
window_node_capture_windows(src_node_add, &window_list);
}
window_manager_animate_window_list(window_list, ts_buf_len(window_list));
```
`view_remove_window_node()` returns `NULL` when the removed node was the view root, i.e. the dragged window was alone in the source space. The first capture block is then skipped and `window_list` is still empty when the second guard is evaluated.
If the newly inserted node also became the root of the destination view (the destination space held exactly one window), `src_node_add->parent` is `NULL` too. The second comparison is therefore `NULL != NULL`, which is false, and the destination capture is skipped as well.
Both blocks being skipped leaves `window_list` empty, so no frames are applied.
### Proposed fix
Skip the guard when `src_node_rm` is `NULL`, since nothing has been captured at that point:
```diff
--- a/src/mouse_handler.c
+++ b/src/mouse_handler.c
@@ -208,7 +208,7 @@ void mouse_drop_action_warp(struct window_manager *wm, struct view *src_view, st
window_node_capture_windows(src_node_rm, &window_list);
}
- if (src_node_rm != src_node_add && src_node_rm != src_node_add->parent) {
+ if (!src_node_rm || (src_node_rm != src_node_add && src_node_rm != src_node_add->parent)) {
window_node_capture_windows(src_node_add, &window_list);
}
```
### Verification
I added a temporary trace right before `window_manager_animate_window_list()` printing `src_node_rm`, `src_node_add`, `src_node_add->parent` and the list length.
| Build | Trigger condition met | Result |
| --- | --- | --- |
| v7.1.25 + trace | yes | `count=0`, reproduced |
| v7.1.25 + fix | yes | `count=2`, 4/4 runs OK |
| master `dd84572` + trace | yes | `count=0`, reproduced |
| master `dd84572` + fix | yes | `count=2`, 3/3 runs OK |
| master + fix, `-O3`, no trace, via launchd | — | 3/3 runs OK |
The trace confirmed the trigger condition was actually met on every run, including the ones that no longer reproduced, so "the condition simply did not occur" is ruled out.
I have been running the patched build (master + fix) as my daily driver since, with no regression observed.
### Environment
- yabai v7.1.25 and master `dd84572`
- macOS Darwin 25.5.0, Apple Silicon
- 2 displays: display 1 `1440x900`, display 2 `2560x1440`
- `layout bsp`, `mouse_modifier fn`, `mouse_action1 move`, `mouse_drop_action swap`, `window_placement second_child`
I understand the repository does not take pull requests, so the diff is inlined above. The same change is on my fork at https://github.com/bilzard/yabai/pull/1 if a reference is useful.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start in src/mouse_handler.c at mouse_drop_action_warp() and inspect the window-node capture conditions around window_manager_animate_window_list(). Reproduce with two displays, layout bsp, and mouse_drop_action warp; done means the dragged window receives an updated frame and both windows tile side by side without requiring a later relayout.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c
- Domain
- desktop
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100