fix(windows): make tab drag-and-drop and window lifecycle work - #207
Merged
Conversation
tao does not normalize DeviceEvent::Button ids across platforms: macOS reports NSEvent::buttonNumber() (left = 0) while Windows derives it from the raw-input button index as index + 1 (left = 1). The constant was hardcoded to 0, so on Windows the release event never matched. That failure is silent and unrecoverable: handle_drag_mouse_release is the only path that ends a tab drag, so the drag never finished. The detached preview window kept following the cursor and the full-window drag overlay stayed mounted, swallowing every click. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
WindowCloseBehaviour::WindowHides was applied on every platform. It models the macOS dock lifecycle, where an app outlives its windows and is reopened from the dock. Windows and Linux have no such concept, so closing the last window left a headless process running. That is worse than a cosmetic difference here: the single-instance IPC then swallows every subsequent launch into the invisible process, so the app stops starting altogether until it is killed by hand. Keep the behaviour on macOS and fall back to the Dioxus default (WindowCloses, exiting on last window close) elsewhere. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mouse::get_mouse_position() does not report the same coordinate space on every platform. macOS (core-graphics) reports logical points, but Windows (GetCursorPos) reports physical pixels because tao marks the process DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2. Everything the drag code compares against is logical: DOM screen_coordinates(), the tab bar bounds measured from the DOM, and the rects is_point_in_window derives as physical / scale. At 150% scaling the cursor therefore read 1.5x too far, so every window hit test missed, the drag detached immediately, and the preview window was positioned far off target. Normalize where the raw value enters the system, and do the same in resolve_window_position_from_cursor, which compares the cursor against logical display bounds to place a new window at the cursor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pointerdown awaited get_client_rect() before registering the pending drag, purely to compute a precise grab offset. That await is a round trip through the webview IPC bridge, and every pointermove arriving in the meantime was dropped because no pending drag existed yet. On Windows the round trip is slow enough to outlast the whole gesture: the drag was registered from a stale position only after the button had already been released, so it started on mouse-up and then never ended, since the release that would have ended it was already gone. Reordering tabs within a window failed for the same reason. macOS hides this because the round trip resolves fast enough to win the race. Register immediately with the offset within the event target, then refine it once the exact element rect arrives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A tab drag is tracked exclusively by its source window, but tao defaults to DeviceEventFilter::Unfocused, which drops device events for unfocused windows. Detaching a tab creates and focuses a preview window, which unfocuses the source - so tracking died mid-drag. Traced on a real drag: motion events stopped for 6.7s and only 2 button events arrived in a whole session, leaving a drag that followed the cursor for 29s. Relax the filter while dragging and restore it afterwards, so idle windows do not pay for device events they do not need. This is Windows-only by nature: tao ignores the filter everywhere else, which is exactly why the drag architecture works on macOS as written. Also let a DOM pointerup end an active drag. DeviceEvent was the only release path, so whenever it failed to arrive the drag never ended and the overlay swallowed every click - in the same trace, 8 pointerups were ignored while the user tried to drop. Releasing over another window still relies on DeviceEvent; handle_drag_mouse_release is idempotent, so a duplicate from both paths is harmless. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes multiple Windows-specific issues that prevented tab drag-and-drop from completing correctly and also corrects platform-specific window close lifecycle behavior.
Changes:
- Normalize cursor coordinates to logical space (fixes Windows DPI scaling mismatches during drag/window placement).
- Ensure tab drag is registered synchronously on
pointerdown, then refine grab offset once the DOM rect resolves. - Adjust Windows drag tracking by relaxing tao’s
DeviceEventfiltering during active drags; scopeWindowHidesclose behavior to macOS only.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| desktop/src/window/settings.rs | Uses logical cursor coordinates (normalized by display scale factor) when resolving window position from cursor. |
| desktop/src/utils/screen.rs | Adds cursor logical normalization helpers and unit tests for Windows/non-Windows behavior. |
| desktop/src/components/tab/tab_item.rs | Registers pending drag synchronously; refines grab offset asynchronously after rect resolves. |
| desktop/src/components/tab/tab_bar.rs | Allows pointerup to end an active drag in the source window; wires refined grab offset into pending state. |
| desktop/src/components/main_app.rs | Applies WindowCloseBehaviour::WindowHides on macOS only. |
| desktop/src/components/app/drag_handlers.rs | Uses logical cursor position during drag; exports release handler for reuse by pointerup path. |
| desktop/src/components/app.rs | Fixes platform-dependent left-button ID; syncs tao device event filter with drag activity on Windows. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes five Windows-only defects. Four of them made tab drag-and-drop unusable —
dragging a tab out produced a window that followed the cursor forever, could not
be dropped, and left an overlay that swallowed every click. Reordering tabs
inside a window failed too. None of it reproduces on macOS, and CI does not
build Windows, so all of it was invisible upstream.
Each commit is a standalone fix and compiles on its own.
The bugs
1.
MOUSE_BUTTON_LEFTwas hardcoded to0— tao does not normalizeDeviceEvent::Buttonids: macOS reportsNSEvent::buttonNumber()(left = 0),Windows derives it from the raw-input index as
index + 1(left = 1). Therelease never matched, and
handle_drag_mouse_releaseis the only path thatends a drag.
2.
WindowCloseBehaviour::WindowHideswas applied on every platform — itmodels the macOS dock lifecycle. On Windows/Linux, closing the last window left
a headless process, and the single-instance IPC then swallowed every subsequent
launch into it, so the app stopped starting at all.
3. Cursor coordinates were mixed physical and logical —
GetCursorPosreports physical pixels (tao marks the process
PER_MONITOR_AWARE_V2), whileeverything it was compared against is logical. At 150% scaling the cursor read
1.5x too far: every window hit test missed, the drag detached instantly, and the
preview window was placed far off target.
4.
pointerdownawaitedget_client_rect()before registering the drag —that is a webview IPC round trip, and every
pointermovearriving meanwhile wasdropped. On Windows it outlasted the whole gesture, so the drag started on
mouse-up from a stale position and then could not end.
5. tao drops
DeviceEvents for unfocused windows by default — but a drag istracked exclusively by its source window, and detaching a tab focuses the new
preview window. Tracking died mid-drag. This filter is a no-op on every platform
except Windows, which is why the architecture works on macOS as written.
Fix 5 also lets a DOM
pointerupend an active drag.DeviceEventbeing theonly release path is what turned each of the bugs above into an unrecoverable
UI rather than a glitch.
Verification
Traced on the real app (
RUST_LOG=arto=trace) on Windows 11 ARM64 at 150%scaling. Before, one drag: motion events stopped for 6.7s, only 2 button events
arrived in the whole session, 8
pointerups were ignored while trying to drop,and the drag ran for 29s. After, three consecutive drags each started with the
button down, tracked continuously (605 / 337 / 499 motion events), and stopped
within 2ms of release. The device event filter was relaxed and restored 3 times
each, so idle windows do not pay for events they do not need.
just check testpasses (clippy-D warnings, 546 Rust tests, 69 renderertests) and
just buildproduces a working bundle. Adds 3 unit tests for thecoordinate normalization.
Note for review
Commit 2 also changes Linux behaviour (closing the last window now quits). The
reasoning is the same as for Windows, but it was not verified on Linux.
🤖 Generated with Claude Code