Skip to content

fix(windows)!: restore webview focus for unstable feature - #15625

Merged
Legend-Master merged 14 commits into
tauri-apps:devfrom
Legend-Master:unstable-webview-focus
Jul 22, 2026
Merged

fix(windows)!: restore webview focus for unstable feature#15625
Legend-Master merged 14 commits into
tauri-apps:devfrom
Legend-Master:unstable-webview-focus

Conversation

@Legend-Master

@Legend-Master Legend-Master commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Fix #15624
Fix #15626
Closes tauri-apps/wry#1755

  • Fix webview don't get focus when Alt-Tab back to the window if unstable feature is enabled on Windows
  • Fix WindowEvent::Focused events emitted when dragging the window on Windows
  • CreateWebviewOptions::focused_webview now takes Arc<Mutex<FocusState>> instead of Arc<Mutex<Option<String>>>

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Package Changes Through 4f1c912

There are 14 changes which include tauri with minor, tauri-cli with minor, @tauri-apps/cli with minor, tauri-runtime with minor, tauri-runtime-wry with minor, tauri-utils with minor, tauri-bundler with minor, tauri-build with minor, tauri-macos-sign with minor, tauri-codegen with minor, tauri-macros with minor, tauri-plugin with minor, tauri-driver with minor, @tauri-apps/api with minor

Planned Package Versions

The following package releases are the planned based on the context of changes in this pull request.

package current next
@tauri-apps/api 2.11.1 2.12.0
tauri-utils 2.9.3 2.10.0
tauri-macos-sign 2.3.4 2.4.0
tauri-bundler 2.9.4 2.10.0
tauri-runtime 2.11.3 2.12.0
tauri-runtime-wry 2.11.4 2.12.0
tauri-codegen 2.6.3 2.7.0
tauri-macros 2.6.3 2.7.0
tauri-plugin 2.6.3 2.7.0
tauri-build 2.6.3 2.7.0
tauri 2.11.5 2.12.0
@tauri-apps/cli 2.11.4 2.12.0
tauri-cli 2.11.4 2.12.0
tauri-driver 2.0.6 2.1.0

Add another change file through the GitHub UI by following this link.


Read about change files or the docs at github.com/jbolda/covector

@Legend-Master Legend-Master added this to the 2.12 milestone Jul 1, 2026
let focused_webview = Arc::new(Mutex::new(FocusState::default()));

#[cfg(feature = "unstable")]
let has_children = webview.is_some();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@lucasfernog You might want to double check if this is correct

@amrbashir

Copy link
Copy Markdown
Member

does this PR in wry tauri-apps/wry#1755 tries to fix the same issue? also should we maybe fix it in wry or not?

@Legend-Master

Copy link
Copy Markdown
Contributor Author

does this PR in wry tauri-apps/wry#1755 tries to fix the same issue?

tauri-apps/wry#1755 doesn't make any sense to me at least at a glance, it just tries to attach to the subclass to the first webview in the window.

also should we maybe fix it in wry or not?

If build_as_child is supposed to allow the caller to control the sizing and focusing states, doing it in tauri is correct I think (we should document build_as_child better by the way)

@zendy00

zendy00 commented Jul 2, 2026

Copy link
Copy Markdown

We hit this exact bug in our app (unstable multi-webview on Windows) and spent a while diagnosing it, so sharing one concern about the trigger path.

The restore here runs on tao's WindowEvent::Focused(true), which tao emits only from WM_SETFOCUS on the top-level window (tao 0.35 event_loop.rs — there is no WM_ACTIVATE arm). But in the scenario we reproduced (recent WebView2, ~Edge 149 / Chromium 141+, same family as tauri-apps/wry#1754), reactivating the window (alt-tab or clicking the taskbar icon / another app and back) restores OS focus directly to the WebView2 child HWND (Chrome_WidgetWin_1), so the top-level window never receives WM_SETFOCUS. We measured this with GetGUIThreadInfo: foreground/active = the top-level Tauri window, focus window = Chrome_WidgetWin_1, and keyboard input stays dead until a physical click. In that state tao won't emit Focused(true), so the restore path in this PR wouldn't run.

That's why our wry-side fix (tauri-apps/wry#1755) hooks WM_ACTIVATE instead — it is always delivered to the top-level on activation regardless of where focus lands. One trap we hit there, in case it's useful: calling MoveFocus synchronously during WM_ACTIVATE handling gets overwritten when Windows subsequently restores focus to the child, so it has to be deferred (post a message to self and call MoveFocus after activation routing settles).

Has this been tested against a WebView2 runtime new enough to show the direct-to-child focus restore behavior? If Focused(true) still fires there, great — and tracking the last focused webview here is a nice improvement over restoring the first webview (which is what #1755 does). The two fixes could compose well.

@Legend-Master

Copy link
Copy Markdown
Contributor Author

@zendy00 This is tested on Webview2 149, and alt-tab does give the focus to the tao window for me

@zendy00

zendy00 commented Jul 2, 2026

Copy link
Copy Markdown

Thanks for checking this.

I re-tested in our app by applying this PR through [patch.crates-io], pinning the Tauri crates to a4c387e67dd0e672d22e6471413bfaa2cdce7c29. Importantly, I removed my wry patch for the test, so wry resolves back to crates.io 0.55.1.

With that setup, Alt+Tab focus restoration works correctly for our Windows unstable multi-webview case. So my concern above about WindowEvent::Focused(true) not being enough was wrong for this PR/test case.

Really glad to see this fixed on the Tauri side. Thanks for the quick clarification and for working on this.

@Legend-Master
Legend-Master merged commit 08acfb3 into tauri-apps:dev Jul 22, 2026
23 checks passed
@Legend-Master
Legend-Master deleted the unstable-webview-focus branch July 22, 2026 09:27
mattd-tg pushed a commit to mattd-tg/tauri that referenced this pull request Aug 12, 2026
Backport of upstream 08acfb3 ("fix(windows)!: restore webview focus for
unstable feature", tauri-apps#15625), merged for 2.12 while 2.11.5 is still the latest
release. Cherry-picked rather than waited for because a downstream consumer
needs it now: a window with a child webview could not tell "the keyboard moved
into my own webview" from "the user switched application".

Both look the same from the parent. Windows sends WM_KILLFOCUS to a top-level
whenever the keyboard leaves it -- including into a child of that very window --
and tao turns that into Focused(false). So the first click into web content
reported the window as unfocused, and an app that dismisses itself on blur
dismissed itself the instant it was first clicked.

The check that survived that reported the opposite: a thread's focus state
outlives it losing the foreground, so once a click had put the keyboard in the
WebView2 child the panel's own thread went on naming that child even after
another application owned the desktop, and the dismissal never fired at all.
Neither read can be fixed downstream -- at WM_KILLFOCUS time the activation
change has not completed, so a synchronous GetGUIThreadInfo(0) either still
reports us or answers a null hwndFocus mid-transition.

Upstream's answer is to stop asking the parent. With a child webview present the
raw parent blur is suppressed and WebView2's own add_LostFocus becomes the one
blur producer, which fires after focus has genuinely moved away. Focus gains are
deduplicated through a three-state FocusState, which also restores focus to the
last focused webview on Alt+Tab back and stops a window drag emitting Focused.

CONSEQUENCE FOR CALLERS, and it is not optional: a window whose child webview
has never held focus now emits no blur at all, because the parent's is
suppressed and the webview has none to lose. Anything relying on aggregate blur
must focus the child explicitly when it reveals the window.

The three .changes/ fragments are deliberately not carried. No commit on this
patch branch has ever carried one, covector never runs downstream, and upstream
will have consumed and deleted those files by the release that makes this
backport redundant -- so keeping them would only make the rebase that drops
this commit conflict over files that no longer exist.
mattd-tg added a commit to mattd-tg/tauri that referenced this pull request Aug 12, 2026
WebView2 raises `LostFocus` whenever the webview stops holding the keyboard, and
that includes the keyboard moving to the webview's OWN top-level window. Every
`SetWindowPos` that moves or resizes the webview does exactly that. The
`add_LostFocus` handler took "this webview lost focus" to mean "this window lost
focus", so a window which repositions itself while open raises a spurious
`WindowEvent::Focused(false)` per frame it applies.

MEASURED, on a downstream tray panel that sizes itself to its content: 30 of 31
blur events landed within 50ms of a window move and 15 of them exactly 1ms after,
one pair per show, with the window closing itself 27-37ms after every open. The
previous commit is what surfaced it -- before it, the raw parent blur was the one
an app saw and this one was redundant -- so the two belong together even though
only the other is upstream.

Upstream is aware of the phenomenon in the other direction: the same PR carries
"Fix `WindowEvent::Focused` events emitted when dragging the window on Windows",
which dedupes the focus GAIN. The loss still fires.

`GetFocus` is the right call here and would be wrong almost anywhere else. It
answers about the CALLING THREAD's message queue, and a WebView2 event handler
runs on the thread that owns the controller, so this is the one place that can
ask it and get a meaningful answer. It is also correctly timed: `LostFocus` is
raised after the focus change has completed, unlike `WM_KILLFOCUS`, where the
activation change is still in flight -- which is exactly why the same question
cannot be answered from a window-blur handler, and why downstream attempts to do
so failed. A null answer is not "unknown": it means our thread holds no focus at
all, which is the real departure this event exists to report. Rooted with
`GA_ROOT` because the focus may be on any descendant and the container is itself
a child.

The check runs BEFORE the state machine, so a move leaves `FocusState` alone.
Recording `Blured` there would emit a blur nobody caused and make the focus gain
that follows look like a fresh activation rather than the continuation it is.

Focus moving to a different top-level of the same process still reads as a real
blur, correctly: that window's root is not ours.

NOT AN UPSTREAM CHERRY-PICK, unlike the commit below it, so it does not
disappear when this branch rebases onto a release containing tauri-apps#15625. It wants to
be a PR against `tauri-apps/tauri`; until then it is a carried patch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

4 participants