compat: persist cookies so consent/login survive restarts, use native UA by default
- tab webviews now share a WebContext backed by the profile dir (~/.local/share/RustyWolf); WebKitGTK keeps cookies in memory unless a persistent store is set, which made youtube.com re-show the consent wall on every launch and made logins impossible to keep - default user agent is WebKit's native one instead of the malformed fake-Chrome + RustyWolf string that pushed sites into Chromium-only code paths; --ua-chrome adds a clean Chrome UA as fallback - --private keeps using an ephemeral in-memory session - README: document --ua-chrome and the persistent profile
This commit is contained in:
parent
fbd88c6281
commit
56f548bd1d
3 changed files with 201 additions and 91 deletions
|
|
@ -44,6 +44,9 @@ cargo run -- --title "RustyWolf"
|
||||||
cargo run -- --private
|
cargo run -- --private
|
||||||
cargo run -- --dry-run
|
cargo run -- --dry-run
|
||||||
|
|
||||||
|
# compatibility
|
||||||
|
cargo run -- --ua-chrome # Chrome UA for sites that block WebKit
|
||||||
|
|
||||||
# Linux backend controls
|
# Linux backend controls
|
||||||
cargo run -- --linux-backend x11
|
cargo run -- --linux-backend x11
|
||||||
cargo run -- --linux-backend wayland
|
cargo run -- --linux-backend wayland
|
||||||
|
|
@ -51,6 +54,12 @@ cargo run -- --linux-disable-dmabuf
|
||||||
cargo run -- --linux-backend x11 --linux-disable-dmabuf
|
cargo run -- --linux-backend x11 --linux-disable-dmabuf
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Sessions are persistent: cookies and site data live in `~/.local/share/RustyWolf`,
|
||||||
|
so logins and consent choices survive restarts. `--private` gives an ephemeral
|
||||||
|
session instead. The default user agent is WebKit's native one (best match for
|
||||||
|
the engine); `--ua-chrome` is a last-resort fallback for sites that refuse to
|
||||||
|
run on WebKit.
|
||||||
|
|
||||||
## Privacy Notes
|
## Privacy Notes
|
||||||
|
|
||||||
RustyWolf applies baseline hardening in page context:
|
RustyWolf applies baseline hardening in page context:
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,12 @@ pub struct Cli {
|
||||||
help = "Linux only: disable WebKit dmabuf renderer (helps some NVIDIA/Wayland setups)"
|
help = "Linux only: disable WebKit dmabuf renderer (helps some NVIDIA/Wayland setups)"
|
||||||
)]
|
)]
|
||||||
pub linux_disable_dmabuf: bool,
|
pub linux_disable_dmabuf: bool,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "Send a Chrome user agent instead of the native WebKit one (for sites that block WebKit)"
|
||||||
|
)]
|
||||||
|
pub ua_chrome: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cli {
|
impl Cli {
|
||||||
|
|
|
||||||
277
src/webview.rs
277
src/webview.rs
|
|
@ -19,12 +19,11 @@ use tao::platform::unix::WindowExtUnix;
|
||||||
use tao::window::{Icon, Window, WindowBuilder};
|
use tao::window::{Icon, Window, WindowBuilder};
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use wry::WebViewBuilderExtUnix;
|
use wry::WebViewBuilderExtUnix;
|
||||||
use wry::{PageLoadEvent, Rect, WebView, WebViewBuilder};
|
use wry::{PageLoadEvent, Rect, WebContext, WebView, WebViewBuilder};
|
||||||
|
|
||||||
const DEFAULT_HOME: &str = "https://duckduckgo.com";
|
const DEFAULT_HOME: &str = "https://duckduckgo.com";
|
||||||
const NEW_TAB_URL: &str = "https://duckduckgo.com";
|
const NEW_TAB_URL: &str = "https://duckduckgo.com";
|
||||||
const SEARCH_URL_PREFIX: &str = "https://duckduckgo.com/?q=";
|
const SEARCH_URL_PREFIX: &str = "https://duckduckgo.com/?q=";
|
||||||
const MODERN_BROWSER_UA_BASE: &str = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36";
|
|
||||||
const CHROME_HEIGHT_BASE: f64 = 88.0;
|
const CHROME_HEIGHT_BASE: f64 = 88.0;
|
||||||
const CHROME_HEIGHT_DOWNLOADS_EXTRA: f64 = 104.0;
|
const CHROME_HEIGHT_DOWNLOADS_EXTRA: f64 = 104.0;
|
||||||
const CHROME_HEIGHT_POPUP_PROMPT_EXTRA: f64 = 44.0;
|
const CHROME_HEIGHT_POPUP_PROMPT_EXTRA: f64 = 44.0;
|
||||||
|
|
@ -96,6 +95,8 @@ struct BrowserState {
|
||||||
active: usize,
|
active: usize,
|
||||||
next_id: u32,
|
next_id: u32,
|
||||||
private: bool,
|
private: bool,
|
||||||
|
user_agent: Option<String>,
|
||||||
|
web_context: Option<WebContext>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PendingPopup {
|
struct PendingPopup {
|
||||||
|
|
@ -1111,8 +1112,13 @@ pub fn launch_webkit(args: &Cli) -> Result<()> {
|
||||||
if args.dry_run {
|
if args.dry_run {
|
||||||
let effective_dmabuf =
|
let effective_dmabuf =
|
||||||
std::env::var("WEBKIT_DISABLE_DMABUF_RENDERER").unwrap_or_else(|_| "0".to_string());
|
std::env::var("WEBKIT_DISABLE_DMABUF_RENDERER").unwrap_or_else(|_| "0".to_string());
|
||||||
|
let ua = if args.ua_chrome {
|
||||||
|
chrome_user_agent()
|
||||||
|
} else {
|
||||||
|
"native".to_string()
|
||||||
|
};
|
||||||
println!(
|
println!(
|
||||||
"engine=webkit url={start_url} private={} linux_backend={:?} linux_disable_dmabuf={} effective_webkit_disable_dmabuf={effective_dmabuf}",
|
"engine=webkit url={start_url} private={} ua={ua} linux_backend={:?} linux_disable_dmabuf={} effective_webkit_disable_dmabuf={effective_dmabuf}",
|
||||||
args.private, args.linux_backend, args.linux_disable_dmabuf
|
args.private, args.linux_backend, args.linux_disable_dmabuf
|
||||||
);
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|
@ -1167,6 +1173,12 @@ pub fn launch_webkit(args: &Cli) -> Result<()> {
|
||||||
active: 0,
|
active: 0,
|
||||||
next_id: 1,
|
next_id: 1,
|
||||||
private: args.private,
|
private: args.private,
|
||||||
|
user_agent: if args.ua_chrome {
|
||||||
|
Some(chrome_user_agent())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
web_context: persistent_web_context(args.private),
|
||||||
};
|
};
|
||||||
|
|
||||||
open_tab(&mut state, &window, &proxy, start_url)?;
|
open_tab(&mut state, &window, &proxy, start_url)?;
|
||||||
|
|
@ -1820,71 +1832,20 @@ fn is_child_webview_unsupported_error(err_text: &str) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn build_tab_webview(
|
fn build_tab_webview(
|
||||||
window: &Window,
|
window: &Window,
|
||||||
proxy: &EventLoopProxy<UserEvent>,
|
proxy: &EventLoopProxy<UserEvent>,
|
||||||
host_mode: WebViewHostMode,
|
host_mode: WebViewHostMode,
|
||||||
#[cfg(target_os = "linux")] gtk_content_container: Option<>k::Box>,
|
#[cfg(target_os = "linux")] gtk_content_container: Option<>k::Box>,
|
||||||
private: bool,
|
private: bool,
|
||||||
|
user_agent: Option<&str>,
|
||||||
|
mut web_context: Option<&mut WebContext>,
|
||||||
tab_id: u32,
|
tab_id: u32,
|
||||||
url: &str,
|
url: &str,
|
||||||
bounds: Rect,
|
bounds: Rect,
|
||||||
) -> Result<(WebView, WebViewHostMode)> {
|
) -> Result<(WebView, WebViewHostMode)> {
|
||||||
let init_script = format!("{PRIVACY_JS}\n{ADBLOCK_JS}");
|
let init_script = format!("{PRIVACY_JS}\n{ADBLOCK_JS}");
|
||||||
let user_agent = browser_user_agent();
|
|
||||||
|
|
||||||
let builder = || {
|
|
||||||
let title_proxy = proxy.clone();
|
|
||||||
let page_proxy = proxy.clone();
|
|
||||||
let download_started_proxy = proxy.clone();
|
|
||||||
let download_completed_proxy = proxy.clone();
|
|
||||||
let download_total_proxy = proxy.clone();
|
|
||||||
let new_window_proxy = proxy.clone();
|
|
||||||
|
|
||||||
WebViewBuilder::new()
|
|
||||||
.with_url(url)
|
|
||||||
.with_user_agent(&user_agent)
|
|
||||||
.with_incognito(private)
|
|
||||||
.with_initialization_script(&init_script)
|
|
||||||
.with_bounds(bounds)
|
|
||||||
.with_new_window_req_handler(move |target| {
|
|
||||||
let _ = new_window_proxy.send_event(UserEvent::OpenInNewTab { url: target });
|
|
||||||
false
|
|
||||||
})
|
|
||||||
.with_document_title_changed_handler(move |title| {
|
|
||||||
let _ = title_proxy.send_event(UserEvent::TitleChanged { tab_id, title });
|
|
||||||
})
|
|
||||||
.with_on_page_load_handler(move |event, url| {
|
|
||||||
if matches!(event, PageLoadEvent::Finished) {
|
|
||||||
let _ = page_proxy.send_event(UserEvent::PageLoaded { tab_id, url });
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.with_download_started_handler(move |url, destination| {
|
|
||||||
let path = ensure_unique_download_path(default_download_path_for(&url));
|
|
||||||
if let Some(parent) = path.parent() {
|
|
||||||
let _ = std::fs::create_dir_all(parent);
|
|
||||||
}
|
|
||||||
*destination = path.clone();
|
|
||||||
let path_string = path_to_string(&path);
|
|
||||||
fetch_download_total(
|
|
||||||
url.clone(),
|
|
||||||
path_string.clone(),
|
|
||||||
download_total_proxy.clone(),
|
|
||||||
);
|
|
||||||
let _ = download_started_proxy.send_event(UserEvent::DownloadStarted {
|
|
||||||
url,
|
|
||||||
file_path: path_string,
|
|
||||||
});
|
|
||||||
true
|
|
||||||
})
|
|
||||||
.with_download_completed_handler(move |url, path, success| {
|
|
||||||
let _ = download_completed_proxy.send_event(UserEvent::DownloadCompleted {
|
|
||||||
url,
|
|
||||||
file_path: path.map(|p| path_to_string(&p)),
|
|
||||||
success,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
match host_mode {
|
match host_mode {
|
||||||
WebViewHostMode::Window => {
|
WebViewHostMode::Window => {
|
||||||
|
|
@ -1893,32 +1854,71 @@ fn build_tab_webview(
|
||||||
let container = gtk_content_container.ok_or_else(|| {
|
let container = gtk_content_container.ok_or_else(|| {
|
||||||
anyhow!("Failed to create tab webview: missing Linux GTK content container")
|
anyhow!("Failed to create tab webview: missing Linux GTK content container")
|
||||||
})?;
|
})?;
|
||||||
builder()
|
tab_webview_builder(
|
||||||
.build_gtk(container)
|
proxy,
|
||||||
.map(|webview| (webview, WebViewHostMode::Window))
|
web_context,
|
||||||
.map_err(|err| anyhow!("Failed to create tab webview with GTK hosting: {err}"))
|
private,
|
||||||
|
user_agent,
|
||||||
|
&init_script,
|
||||||
|
tab_id,
|
||||||
|
url,
|
||||||
|
bounds,
|
||||||
|
)
|
||||||
|
.build_gtk(container)
|
||||||
|
.map(|webview| (webview, WebViewHostMode::Window))
|
||||||
|
.map_err(|err| anyhow!("Failed to create tab webview with GTK hosting: {err}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
{
|
{
|
||||||
builder()
|
tab_webview_builder(
|
||||||
.build(window)
|
proxy,
|
||||||
.map(|webview| (webview, WebViewHostMode::Window))
|
web_context,
|
||||||
.map_err(|err| anyhow!("Failed to create tab webview: {err}"))
|
private,
|
||||||
|
user_agent,
|
||||||
|
&init_script,
|
||||||
|
tab_id,
|
||||||
|
url,
|
||||||
|
bounds,
|
||||||
|
)
|
||||||
|
.build(window)
|
||||||
|
.map(|webview| (webview, WebViewHostMode::Window))
|
||||||
|
.map_err(|err| anyhow!("Failed to create tab webview: {err}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WebViewHostMode::Child => match builder().build_as_child(window) {
|
WebViewHostMode::Child => {
|
||||||
Ok(webview) => Ok((webview, WebViewHostMode::Child)),
|
let attempt = tab_webview_builder(
|
||||||
Err(err) => {
|
proxy,
|
||||||
let err_text = err.to_string();
|
web_context.as_deref_mut(),
|
||||||
if is_child_webview_unsupported_error(&err_text) {
|
private,
|
||||||
eprintln!(
|
user_agent,
|
||||||
"child tab webview unsupported on this backend; falling back to window-hosted tabs"
|
&init_script,
|
||||||
);
|
tab_id,
|
||||||
#[cfg(target_os = "linux")]
|
url,
|
||||||
{
|
bounds,
|
||||||
if let Some(container) = gtk_content_container {
|
)
|
||||||
return builder()
|
.build_as_child(window);
|
||||||
|
match attempt {
|
||||||
|
Ok(webview) => Ok((webview, WebViewHostMode::Child)),
|
||||||
|
Err(err) => {
|
||||||
|
let err_text = err.to_string();
|
||||||
|
if is_child_webview_unsupported_error(&err_text) {
|
||||||
|
eprintln!(
|
||||||
|
"child tab webview unsupported on this backend; falling back to window-hosted tabs"
|
||||||
|
);
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
if let Some(container) = gtk_content_container {
|
||||||
|
return tab_webview_builder(
|
||||||
|
proxy,
|
||||||
|
web_context,
|
||||||
|
private,
|
||||||
|
user_agent,
|
||||||
|
&init_script,
|
||||||
|
tab_id,
|
||||||
|
url,
|
||||||
|
bounds,
|
||||||
|
)
|
||||||
.build_gtk(container)
|
.build_gtk(container)
|
||||||
.map(|webview| (webview, WebViewHostMode::Window))
|
.map(|webview| (webview, WebViewHostMode::Window))
|
||||||
.map_err(|gtk_err| {
|
.map_err(|gtk_err| {
|
||||||
|
|
@ -1926,15 +1926,24 @@ fn build_tab_webview(
|
||||||
"Failed to create tab webview: child failed ({err_text}); GTK fallback failed ({gtk_err})"
|
"Failed to create tab webview: child failed ({err_text}); GTK fallback failed ({gtk_err})"
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Failed to create tab webview: child failed ({err_text}); missing Linux GTK content container"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
return Err(anyhow!(
|
|
||||||
"Failed to create tab webview: child failed ({err_text}); missing Linux GTK content container"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
{
|
{
|
||||||
return builder()
|
return tab_webview_builder(
|
||||||
|
proxy,
|
||||||
|
web_context,
|
||||||
|
private,
|
||||||
|
user_agent,
|
||||||
|
&init_script,
|
||||||
|
tab_id,
|
||||||
|
url,
|
||||||
|
bounds,
|
||||||
|
)
|
||||||
.build(window)
|
.build(window)
|
||||||
.map(|webview| (webview, WebViewHostMode::Window))
|
.map(|webview| (webview, WebViewHostMode::Window))
|
||||||
.map_err(|fallback_err| {
|
.map_err(|fallback_err| {
|
||||||
|
|
@ -1942,27 +1951,109 @@ fn build_tab_webview(
|
||||||
"Failed to create tab webview: child failed ({err_text}); window fallback failed ({fallback_err})"
|
"Failed to create tab webview: child failed ({err_text}); window fallback failed ({fallback_err})"
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Err(anyhow!("Failed to create tab webview: {err_text}"))
|
||||||
}
|
}
|
||||||
Err(anyhow!("Failed to create tab webview: {err_text}"))
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn browser_user_agent() -> String {
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn tab_webview_builder<'a>(
|
||||||
|
proxy: &EventLoopProxy<UserEvent>,
|
||||||
|
web_context: Option<&'a mut WebContext>,
|
||||||
|
private: bool,
|
||||||
|
user_agent: Option<&str>,
|
||||||
|
init_script: &str,
|
||||||
|
tab_id: u32,
|
||||||
|
url: &str,
|
||||||
|
bounds: Rect,
|
||||||
|
) -> WebViewBuilder<'a> {
|
||||||
|
let title_proxy = proxy.clone();
|
||||||
|
let page_proxy = proxy.clone();
|
||||||
|
let download_started_proxy = proxy.clone();
|
||||||
|
let download_completed_proxy = proxy.clone();
|
||||||
|
let download_total_proxy = proxy.clone();
|
||||||
|
let new_window_proxy = proxy.clone();
|
||||||
|
|
||||||
|
let mut builder = match web_context {
|
||||||
|
Some(web_context) => WebViewBuilder::with_web_context(web_context),
|
||||||
|
None => WebViewBuilder::new(),
|
||||||
|
}
|
||||||
|
.with_url(url)
|
||||||
|
.with_incognito(private)
|
||||||
|
.with_initialization_script(init_script)
|
||||||
|
.with_bounds(bounds);
|
||||||
|
if let Some(user_agent) = user_agent {
|
||||||
|
builder = builder.with_user_agent(user_agent);
|
||||||
|
}
|
||||||
|
builder
|
||||||
|
.with_new_window_req_handler(move |target| {
|
||||||
|
let _ = new_window_proxy.send_event(UserEvent::OpenInNewTab { url: target });
|
||||||
|
false
|
||||||
|
})
|
||||||
|
.with_document_title_changed_handler(move |title| {
|
||||||
|
let _ = title_proxy.send_event(UserEvent::TitleChanged { tab_id, title });
|
||||||
|
})
|
||||||
|
.with_on_page_load_handler(move |event, url| {
|
||||||
|
if matches!(event, PageLoadEvent::Finished) {
|
||||||
|
let _ = page_proxy.send_event(UserEvent::PageLoaded { tab_id, url });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.with_download_started_handler(move |url, destination| {
|
||||||
|
let path = ensure_unique_download_path(default_download_path_for(&url));
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
let _ = std::fs::create_dir_all(parent);
|
||||||
|
}
|
||||||
|
*destination = path.clone();
|
||||||
|
let path_string = path_to_string(&path);
|
||||||
|
fetch_download_total(
|
||||||
|
url.clone(),
|
||||||
|
path_string.clone(),
|
||||||
|
download_total_proxy.clone(),
|
||||||
|
);
|
||||||
|
let _ = download_started_proxy.send_event(UserEvent::DownloadStarted {
|
||||||
|
url,
|
||||||
|
file_path: path_string,
|
||||||
|
});
|
||||||
|
true
|
||||||
|
})
|
||||||
|
.with_download_completed_handler(move |url, path, success| {
|
||||||
|
let _ = download_completed_proxy.send_event(UserEvent::DownloadCompleted {
|
||||||
|
url,
|
||||||
|
file_path: path.map(|p| path_to_string(&p)),
|
||||||
|
success,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn persistent_web_context(private: bool) -> Option<WebContext> {
|
||||||
|
if private {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// WebKitGTK keeps cookies in memory unless a persistent store is set,
|
||||||
|
// which would log users out and re-show consent walls on every launch.
|
||||||
|
let data_dir = dirs::data_dir()
|
||||||
|
.unwrap_or_else(std::env::temp_dir)
|
||||||
|
.join("RustyWolf");
|
||||||
|
let _ = fs::create_dir_all(&data_dir);
|
||||||
|
Some(WebContext::new(Some(data_dir)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chrome_user_agent() -> String {
|
||||||
let platform = if cfg!(target_os = "macos") {
|
let platform = if cfg!(target_os = "macos") {
|
||||||
"Macintosh; Intel Mac OS X 14_0"
|
"Macintosh; Intel Mac OS X 10_15_7"
|
||||||
} else if cfg!(target_os = "windows") {
|
} else if cfg!(target_os = "windows") {
|
||||||
"Windows NT 10.0; Win64; x64"
|
"Windows NT 10.0; Win64; x64"
|
||||||
} else {
|
} else {
|
||||||
"X11; Linux x86_64"
|
"X11; Linux x86_64"
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keep broad compatibility while explicitly identifying RustyWolf.
|
// A consistent, well-formed Chrome UA for sites that refuse to run on WebKit.
|
||||||
format!(
|
format!(
|
||||||
"Mozilla/5.0 ({platform}; rv:128.0) {MODERN_BROWSER_UA_BASE} RustyWolf/{}",
|
"Mozilla/5.0 ({platform}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36"
|
||||||
env!("CARGO_PKG_VERSION")
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1982,6 +2073,8 @@ fn open_tab(
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
state.gtk_content_container.as_ref(),
|
state.gtk_content_container.as_ref(),
|
||||||
state.private,
|
state.private,
|
||||||
|
state.user_agent.as_deref(),
|
||||||
|
state.web_context.as_mut(),
|
||||||
tab_id,
|
tab_id,
|
||||||
&url,
|
&url,
|
||||||
tab_bounds,
|
tab_bounds,
|
||||||
|
|
@ -2080,6 +2173,8 @@ fn ensure_tab_loaded(
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
state.gtk_content_container.as_ref(),
|
state.gtk_content_container.as_ref(),
|
||||||
state.private,
|
state.private,
|
||||||
|
state.user_agent.as_deref(),
|
||||||
|
state.web_context.as_mut(),
|
||||||
tab_id,
|
tab_id,
|
||||||
&tab_url,
|
&tab_url,
|
||||||
tab_bounds,
|
tab_bounds,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue