asmvik / asmvik/yabai

Dragging the only window of a space onto a single-window space applies no frames (mouse_drop_action_warp)

Open
#2,817 0 comments 0 reactions 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.