fix(startup): defer Windows window activation (#37)

- Keep the Windows WebView host hidden and unfocused during native construction.\n- Queue deep-link, torrent, single-instance, and extension restore requests until Tauri is ready.\n- Reveal and focus the main window through one startup-safe path.\n\nFixes #37.
This commit is contained in:
NimBold
2026-08-27 17:00:57 +03:30
parent 8163e2241a
commit 9c0ba106d6
2 changed files with 49 additions and 29 deletions
+7 -9
View File
@@ -308,18 +308,16 @@ async fn download_handler(
None => return Err(StatusCode::BAD_REQUEST), None => return Err(StatusCode::BAD_REQUEST),
}; };
if let Some(window) = state.app_handle.get_webview_window("main") { let is_hidden = state
let is_visible = window.is_visible().unwrap_or(true); .app_handle
if !is_visible { .get_webview_window("main")
let _ = window.show(); .and_then(|window| window.is_visible().ok())
let _ = window.set_focus(); .is_some_and(|is_visible| !is_visible);
crate::restore_main_window(&state.app_handle);
if is_hidden {
// Sleep briefly to let the webview wake up from macOS App Nap // Sleep briefly to let the webview wake up from macOS App Nap
// otherwise the IPC event emitted immediately after is dropped. // otherwise the IPC event emitted immediately after is dropped.
tokio::time::sleep(std::time::Duration::from_millis(300)).await; tokio::time::sleep(std::time::Duration::from_millis(300)).await;
} else {
let _ = window.show();
let _ = window.set_focus();
}
} }
if !wait_for_frontend(&state.frontend_ready).await { if !wait_for_frontend(&state.frontend_ready).await {
+39 -17
View File
@@ -3659,6 +3659,7 @@ pub struct AppState {
#[derive(Default)] #[derive(Default)]
struct MainWindowRestoreState { struct MainWindowRestoreState {
requested: AtomicBool, requested: AtomicBool,
startup_complete: AtomicBool,
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -3954,7 +3955,26 @@ where
.collect() .collect()
} }
fn restore_main_window(app_handle: &tauri::AppHandle) { pub(crate) fn restore_main_window(app_handle: &tauri::AppHandle) {
let startup_complete = app_handle
.try_state::<MainWindowRestoreState>()
.is_none_or(|state| state.startup_complete.load(Ordering::Acquire));
if !startup_complete {
if let Some(state) = app_handle.try_state::<MainWindowRestoreState>() {
// The first window can be requested by a single-instance callback
// or an opened .torrent path while the native host is still being
// constructed. Do not touch the HWND until RunEvent::Ready.
state.requested.store(true, Ordering::Release);
// Ready may have won the transition between the first load and
// the request store. Re-check before returning so that request is
// serviced by this call instead of being left pending forever.
if !state.startup_complete.load(Ordering::Acquire) {
return;
}
} else {
return;
}
}
let Some(window) = app_handle.get_webview_window("main") else { let Some(window) = app_handle.get_webview_window("main") else {
if let Some(state) = app_handle.try_state::<MainWindowRestoreState>() { if let Some(state) = app_handle.try_state::<MainWindowRestoreState>() {
state.requested.store(true, Ordering::Release); state.requested.store(true, Ordering::Release);
@@ -3967,6 +3987,12 @@ fn restore_main_window(app_handle: &tauri::AppHandle) {
let _ = window.set_focus(); let _ = window.set_focus();
} }
fn mark_main_window_startup_complete(app_handle: &tauri::AppHandle) {
if let Some(state) = app_handle.try_state::<MainWindowRestoreState>() {
state.startup_complete.store(true, Ordering::Release);
}
}
fn restore_pending_main_window(app_handle: &tauri::AppHandle) { fn restore_pending_main_window(app_handle: &tauri::AppHandle) {
if app_handle if app_handle
.try_state::<MainWindowRestoreState>() .try_state::<MainWindowRestoreState>()
@@ -18514,15 +18540,11 @@ pub fn run() {
.prevent_overflow(); .prevent_overflow();
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
// WebView2 can return E_INVALIDARG from MoveFocus while the // Wry installs its parent WM_SETFOCUS handler while WebView2
// newly-created host window is not focusable yet. Wry // is still being initialized. Keep the host unfocused and
// propagates that error from WebviewWindowBuilder::build, // hidden until RunEvent::Ready so Windows cannot re-enter that
// which destroys the native window and makes a release build // handler during native construction.
// look like it started headlessly before exiting. Leave the main_window_builder = main_window_builder.visible(false).focused(false);
// window unfocused until Windows has completed native
// activation; a synchronous post-build focus request can
// re-enter the WebView2 focus callback on affected systems.
main_window_builder = main_window_builder.focused(false);
} }
main_window_builder main_window_builder
.build() .build()
@@ -18535,13 +18557,6 @@ pub fn run() {
collect_opened_torrent_paths(std::env::args_os().skip(1)), collect_opened_torrent_paths(std::env::args_os().skip(1)),
); );
#[cfg(target_os = "windows")]
if let Some(window) = app.get_webview_window("main") {
window
.set_decorations(false)
.map_err(|error| format!("failed to disable Windows native frame: {error}"))?;
}
let deep_link_app = app.handle().clone(); let deep_link_app = app.handle().clone();
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
if let Err(error) = app.deep_link().register_all() { if let Err(error) = app.deep_link().register_all() {
@@ -19866,6 +19881,13 @@ pub fn run() {
.build(tauri::generate_context!()) .build(tauri::generate_context!())
.expect("error while building tauri application") .expect("error while building tauri application")
.run(|app_handle, event| match event { .run(|app_handle, event| match event {
tauri::RunEvent::Ready => {
mark_main_window_startup_complete(app_handle);
if let Some(state) = app_handle.try_state::<MainWindowRestoreState>() {
state.requested.swap(false, Ordering::AcqRel);
}
restore_main_window(app_handle);
}
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
tauri::RunEvent::Opened { urls } => { tauri::RunEvent::Opened { urls } => {
let paths = collect_opened_torrent_paths( let paths = collect_opened_torrent_paths(