From 0c6b62ae3b3cddc2362f7d3e4eaa9868a86b8b52 Mon Sep 17 00:00:00 2001 From: dvorakmortonxd Date: Sun, 11 Oct 2026 19:35:51 +0200 Subject: [PATCH] ignore late page/title events from suspended tabs (white screen on tab restore) when a loading tab was suspended its webview was destroyed while events were still queued, and a finished load from the dying webview could overwrite the tab's stored URL with an intermediate redirect URL. on switch-back the tab then reloaded the wrong page and stayed blank. each webview instance now gets a generation (epoch): events carry it and are dropped when they don't match, and suspending a tab invalidates its epoch. verified tab cycling still restores pages normally. --- src/webview.rs | 55 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/src/webview.rs b/src/webview.rs index 8e3b383..e526b4a 100644 --- a/src/webview.rs +++ b/src/webview.rs @@ -39,10 +39,12 @@ enum UserEvent { ChromeIpc(String), TitleChanged { tab_id: u32, + epoch: u32, title: String, }, PageLoaded { tab_id: u32, + epoch: u32, url: String, }, DownloadStarted { @@ -73,6 +75,10 @@ struct Tab { url: String, kir_enabled: bool, webview: Option, + // Identifies the current webview instance for this tab. Events (page load, + // title) carry the epoch they were emitted from so that late events from a + // destroyed webview (suspended tab) cannot corrupt the tab state. + epoch: u32, back_stack: Vec, forward_stack: Vec, pending_nav_from_stack: bool, @@ -94,6 +100,7 @@ struct BrowserState { downloads_panel_open: bool, active: usize, next_id: u32, + next_epoch: u32, private: bool, user_agent: Option, web_context: Option, @@ -133,6 +140,13 @@ impl BrowserState { self.next_id += 1; id } + fn alloc_epoch(&mut self) -> u32 { + self.next_epoch = self.next_epoch.wrapping_add(1); + if self.next_epoch == 0 { + self.next_epoch = 1; + } + self.next_epoch + } fn find_index(&self, tab_id: u32) -> Option { self.tabs.iter().position(|t| t.id == tab_id) } @@ -1182,6 +1196,7 @@ pub fn launch_webkit(args: &Cli) -> Result<()> { downloads_panel_open: false, active: 0, next_id: 1, + next_epoch: 1, private: args.private, user_agent: if args.ua_chrome { Some(chrome_user_agent()) @@ -1385,16 +1400,23 @@ fn handle_event( ) -> Result<()> { match ev { UserEvent::ChromeIpc(msg) => handle_chrome_ipc(&msg, window, proxy, state)?, - UserEvent::TitleChanged { tab_id, title } => { + UserEvent::TitleChanged { + tab_id, + epoch, + title, + } => { if let Some(i) = state.find_index(tab_id) { - if !title.trim().is_empty() { + if state.tabs[i].epoch == epoch && !title.trim().is_empty() { state.tabs[i].title = title; push_tabs(state); } } } - UserEvent::PageLoaded { tab_id, url } => { + UserEvent::PageLoaded { tab_id, epoch, url } => { if let Some(i) = state.find_index(tab_id) { + if state.tabs[i].epoch != epoch { + return Ok(()); + } if !url.is_empty() && url != "about:blank" { let previous = state.tabs[i].url.clone(); if url != previous { @@ -1701,6 +1723,7 @@ fn handle_chrome_ipc( state.tabs[i].kir_enabled = !state.tabs[i].kir_enabled; if i != state.active && !state.tabs[i].kir_enabled { state.tabs[i].webview = None; + state.tabs[i].epoch = 0; } apply_visibility(state); push_tabs(state); @@ -1883,6 +1906,7 @@ fn build_tab_webview( user_agent: Option<&str>, mut web_context: Option<&mut WebContext>, tab_id: u32, + epoch: u32, url: &str, bounds: Rect, ) -> Result<(WebView, WebViewHostMode)> { @@ -1902,6 +1926,7 @@ fn build_tab_webview( user_agent, &init_script, tab_id, + epoch, url, bounds, ) @@ -1919,6 +1944,7 @@ fn build_tab_webview( user_agent, &init_script, tab_id, + epoch, url, bounds, ) @@ -1935,6 +1961,7 @@ fn build_tab_webview( user_agent, &init_script, tab_id, + epoch, url, bounds, ) @@ -1957,6 +1984,7 @@ fn build_tab_webview( user_agent, &init_script, tab_id, + epoch, url, bounds, ) @@ -1982,6 +2010,7 @@ fn build_tab_webview( user_agent, &init_script, tab_id, + epoch, url, bounds, ) @@ -2009,6 +2038,7 @@ fn tab_webview_builder<'a>( user_agent: Option<&str>, init_script: &str, tab_id: u32, + epoch: u32, url: &str, bounds: Rect, ) -> WebViewBuilder<'a> { @@ -2036,11 +2066,15 @@ fn tab_webview_builder<'a>( false }) .with_document_title_changed_handler(move |title| { - let _ = title_proxy.send_event(UserEvent::TitleChanged { tab_id, title }); + let _ = title_proxy.send_event(UserEvent::TitleChanged { + tab_id, + epoch, + title, + }); }) .with_on_page_load_handler(move |event, url| { if matches!(event, PageLoadEvent::Finished) { - let _ = page_proxy.send_event(UserEvent::PageLoaded { tab_id, url }); + let _ = page_proxy.send_event(UserEvent::PageLoaded { tab_id, epoch, url }); } }) .with_download_started_handler(move |url, destination| { @@ -2126,6 +2160,7 @@ fn open_tab( url: String, ) -> Result<()> { let tab_id = state.alloc_id(); + let epoch = state.alloc_epoch(); let chrome_h = chrome_height(state); let tab_bounds = content_bounds(window, chrome_h, state.host_mode); let (webview, effective_host_mode) = build_tab_webview( @@ -2138,6 +2173,7 @@ fn open_tab( state.user_agent.as_deref(), state.web_context.as_mut(), tab_id, + epoch, &url, tab_bounds, )?; @@ -2149,6 +2185,7 @@ fn open_tab( url, kir_enabled: false, webview: Some(webview), + epoch, back_stack: Vec::new(), forward_stack: Vec::new(), pending_nav_from_stack: false, @@ -2227,6 +2264,7 @@ fn ensure_tab_loaded( let chrome_h = chrome_height(state); let tab_bounds = content_bounds(window, chrome_h, state.host_mode); let tab_id = state.tabs[index].id; + let epoch = state.alloc_epoch(); let tab_url = state.tabs[index].url.clone(); let (webview, effective_host_mode) = build_tab_webview( window, @@ -2238,18 +2276,23 @@ fn ensure_tab_loaded( state.user_agent.as_deref(), state.web_context.as_mut(), tab_id, + epoch, &tab_url, tab_bounds, )?; state.host_mode = effective_host_mode; state.tabs[index].webview = Some(webview); + state.tabs[index].epoch = epoch; Ok(()) } fn suspend_background_tabs(state: &mut BrowserState) { for (i, tab) in state.tabs.iter_mut().enumerate() { - if i != state.active && !tab.kir_enabled { + if i != state.active && !tab.kir_enabled && tab.webview.is_some() { tab.webview = None; + // Invalidate pending events from the destroyed webview so they + // cannot overwrite this tab's url/title while it is suspended. + tab.epoch = 0; } } }