Skip to content

fix(windows): make tab drag-and-drop and window lifecycle work - #207

Merged
lambdalisue merged 6 commits into
mainfrom
fix/windows-behavior
Aug 1, 2026
Merged

fix(windows): make tab drag-and-drop and window lifecycle work#207
lambdalisue merged 6 commits into
mainfrom
fix/windows-behavior

Conversation

@lambdalisue

Copy link
Copy Markdown
Member

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_LEFT was hardcoded to 0 — tao does not normalize
DeviceEvent::Button ids: macOS reports NSEvent::buttonNumber() (left = 0),
Windows derives it from the raw-input index as index + 1 (left = 1). The
release never matched, and handle_drag_mouse_release is the only path that
ends a drag.

2. WindowCloseBehaviour::WindowHides was applied on every platform — it
models 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 logicalGetCursorPos
reports physical pixels (tao marks the process PER_MONITOR_AWARE_V2), while
everything 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. pointerdown awaited get_client_rect() before registering the drag
that is a webview IPC round trip, and every pointermove arriving meanwhile was
dropped. 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 is
tracked 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 pointerup end an active drag. DeviceEvent being the
only 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 test passes (clippy -D warnings, 546 Rust tests, 69 renderer
tests) and just build produces a working bundle. Adds 3 unit tests for the
coordinate 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

lambdalisue and others added 5 commits August 1, 2026 12:41
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>
Copilot AI review requested due to automatic review settings August 1, 2026 03:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 DeviceEvent filtering during active drags; scope WindowHides close 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.

Comment thread desktop/src/utils/screen.rs Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@lambdalisue
lambdalisue merged commit 09db365 into main Aug 1, 2026
15 checks passed
@lambdalisue
lambdalisue deleted the fix/windows-behavior branch August 1, 2026 04:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants