diff --git a/Cargo.toml b/Cargo.toml index bbf2d11..1fccfbf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,21 @@ [package] name = "tauri-runtime-cef" version = "0.1.0" -description = "CEF (Chromium Embedded Framework) runtime for Tauri: Chromium renders every webview while the app keeps Tauri's Builder, commands and plugins. Ported from tauri's feat/cef branch onto published tauri crates — no fork, no [patch.crates-io]. Adds a deny-by-default Chromium permission policy the app supplies." +description = "CEF runtime for Tauri, ported from tauri feat/cef branch onto published crates." authors = ["Tauri Programme within The Commons Conservancy", "byeongsu-hong"] -repository = "https://github.com/byeongsu-hong/tauri-runtime-cef" +homepage = "https://github.com/SableClient/tauri-runtime-cef" +repository = "https://github.com/SableClient/tauri-runtime-cef" +categories = ["gui"] license = "Apache-2.0 OR MIT" edition = "2024" rust-version = "1.88" [dependencies] base64 = "0.22" -cef = { version = "=148.0.0", features = ["build-util", "linux-x11"] } # Not actually used directly, just locking it. -cef-dll-sys = { version = "=148.0.0", default-features = false } +cef = { version = "=150.2.1", features = ["build-util", "linux-x11"] } +# Not actually used directly, just locking it. +cef-dll-sys = { version = "=150.2.1", default-features = false } dirs = "6" dioxus-debug-cell = "0.1" http = "1" @@ -24,7 +27,9 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" sha2 = "0.10" tauri-runtime = "2.11.2" -tauri-utils = { version = "2.9.2", features = ["html-manipulation"] } +tauri-utils = { version = "2.9.2", features = [ + "html-manipulation", +] } url = "2" winit = "0.31.0-beta.2" @@ -85,13 +90,14 @@ objc2-foundation = { version = "0.3", default-features = false, features = [ [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] dlopen2 = { version = "0.8", features = ["derive"] } gtk = { version = "0.18", features = ["v3_24"] } +libc = "0.2" x11-dl = "2.21" -[dev-dependencies] -tauri = { version = "2", default-features = false, features = ["test"] } - [features] default = ["sandbox"] devtools = [] macos-private-api = ["tauri-runtime/macos-private-api"] sandbox = ["cef/sandbox"] + +[dev-dependencies] +tauri = { version = "2", default-features = false, features = ["test"] } diff --git a/examples/stream_probe.rs b/examples/stream_probe.rs index 9e605fb..70601a7 100644 --- a/examples/stream_probe.rs +++ b/examples/stream_probe.rs @@ -51,86 +51,88 @@ const PAGE: &str = r#"stream probe"#; fn main() { - tauri_runtime_cef::configure(tauri_runtime_cef::CefConfig { - identifier: "cef-stream-probe".into(), - command_line_args: vec![ - ("--use-mock-keychain".into(), None), - ("password-store".into(), Some("basic".into())), - ], - custom_schemes: vec!["tauri".into(), "ipc".into(), "asset".into(), "probe".into()], - cookieable_schemes: vec!["probe".into()], - ..Default::default() - }); - - if std::env::args().any(|arg| arg.starts_with("--type=")) { - tauri_runtime_cef::run_cef_helper_process(); - return; - } + tauri_runtime_cef::configure(tauri_runtime_cef::CefConfig { + identifier: "cef-stream-probe".into(), + command_line_args: vec![ + ("--use-mock-keychain".into(), None), + ("password-store".into(), Some("basic".into())), + ], + custom_schemes: vec!["tauri".into(), "ipc".into(), "asset".into(), "probe".into()], + cookieable_schemes: vec!["probe".into()], + ..Default::default() + }); - std::thread::spawn(|| { - std::thread::sleep(Duration::from_secs(60)); - println!("PROBE-RESULT {{\"timeout\":true}}"); - std::process::exit(2); - }); + if std::env::args().any(|arg| arg.starts_with("--type=")) { + tauri_runtime_cef::run_cef_helper_process(); + return; + } - // The streaming handler owns every probe:// request (process_request consults - // the streaming registry before the buffered path). The buffered registration - // below still has to exist so the runtime installs the scheme factory + - // per-webview registry entry; its handler is never actually called. - tauri_runtime_cef::register_streaming_scheme_handler( - "probe", - Box::new(|_label, request, responder| match request.uri().path() { - "/" => { - let head = http::Response::builder() - .header("content-type", "text/html") - .body(()) - .unwrap(); - let mut writer = responder.respond(head); - let _ = writer.write(PAGE.as_bytes().to_vec()); - } - "/sse" => { - let head = http::Response::builder() - .header("content-type", "text/event-stream") - .header("cache-control", "no-cache") - .body(()) - .unwrap(); - let mut writer = responder.respond(head); - for i in 0..12 { - if writer.write(format!("data: {i}\n\n").into_bytes()).is_err() { - break; // renderer cancelled — the page got its ticks and closed. - } - std::thread::sleep(Duration::from_millis(150)); - } - } - "/report" => { - println!("PROBE-RESULT {}", String::from_utf8_lossy(request.body())); - std::process::exit(0); - } - _ => { - let head = http::Response::builder().status(404).body(()).unwrap(); - responder.respond(head); - } - }), - ); + std::thread::spawn(|| { + std::thread::sleep(Duration::from_secs(60)); + println!("PROBE-RESULT {{\"timeout\":true}}"); + std::process::exit(2); + }); - type Rt = tauri_runtime_cef::CefRuntime; - tauri::Builder::::new() - .register_uri_scheme_protocol("probe", |_ctx, _request| { - // Unreachable: the streaming handler owns every probe:// request. - tauri::http::Response::builder() - .status(500) - .body(Cow::Borrowed(&b"buffered probe handler should be unreachable"[..])) - .unwrap() - }) - .setup(|app| { - tauri::WebviewWindowBuilder::new( - app, + // The streaming handler owns every probe:// request (process_request consults + // the streaming registry before the buffered path). The buffered registration + // below still has to exist so the runtime installs the scheme factory + + // per-webview registry entry; its handler is never actually called. + tauri_runtime_cef::register_streaming_scheme_handler( "probe", - tauri::WebviewUrl::External("probe://app/".parse().unwrap()), - ) - .build()?; - Ok(()) - }) - .run(tauri::test::mock_context(tauri::test::noop_assets())) - .expect("probe app run"); + Box::new(|_label, request, responder| match request.uri().path() { + "/" => { + let head = http::Response::builder() + .header("content-type", "text/html") + .body(()) + .unwrap(); + let mut writer = responder.respond(head); + let _ = writer.write(PAGE.as_bytes().to_vec()); + } + "/sse" => { + let head = http::Response::builder() + .header("content-type", "text/event-stream") + .header("cache-control", "no-cache") + .body(()) + .unwrap(); + let mut writer = responder.respond(head); + for i in 0..12 { + if writer.write(format!("data: {i}\n\n").into_bytes()).is_err() { + break; // renderer cancelled — the page got its ticks and closed. + } + std::thread::sleep(Duration::from_millis(150)); + } + } + "/report" => { + println!("PROBE-RESULT {}", String::from_utf8_lossy(request.body())); + std::process::exit(0); + } + _ => { + let head = http::Response::builder().status(404).body(()).unwrap(); + responder.respond(head); + } + }), + ); + + type Rt = tauri_runtime_cef::CefRuntime; + tauri::Builder::::new() + .register_uri_scheme_protocol("probe", |_ctx, _request| { + // Unreachable: the streaming handler owns every probe:// request. + tauri::http::Response::builder() + .status(500) + .body(Cow::Borrowed( + &b"buffered probe handler should be unreachable"[..], + )) + .unwrap() + }) + .setup(|app| { + tauri::WebviewWindowBuilder::new( + app, + "probe", + tauri::WebviewUrl::External("probe://app/".parse().unwrap()), + ) + .build()?; + Ok(()) + }) + .run(tauri::test::mock_context(tauri::test::noop_assets())) + .expect("probe app run"); } diff --git a/examples/sw_probe.rs b/examples/sw_probe.rs index 782b90c..b8bcb01 100644 --- a/examples/sw_probe.rs +++ b/examples/sw_probe.rs @@ -36,71 +36,66 @@ self.addEventListener('activate', (e) => e.waitUntil(clients.claim())); self.addEventListener('fetch', () => {});"#; fn main() { - tauri_runtime_cef::configure(tauri_runtime_cef::CefConfig { - identifier: "cef-sw-probe".into(), - // On a headless box (Xvfb, no GPU, possibly no unprivileged-userns) the - // CEF sandbox zygote and/or GPU process can fail to launch. If this - // example traps at boot, build it `--no-default-features` (drops the - // `sandbox` feature so `no_sandbox` is honored) and add the switches - // `--no-sandbox --disable-gpu --in-process-gpu` here. That combination is - // box-dependent and flaky; a real desktop keeps the sandbox default and - // needs none of them. - command_line_args: vec![ - ("--use-mock-keychain".into(), None), - ("password-store".into(), Some("basic".into())), - ], - custom_schemes: vec![ - "tauri".into(), - "ipc".into(), - "asset".into(), - "probe".into(), - ], - cookieable_schemes: vec!["probe".into()], - ..Default::default() - }); + tauri_runtime_cef::configure(tauri_runtime_cef::CefConfig { + identifier: "cef-sw-probe".into(), + // On a headless box (Xvfb, no GPU, possibly no unprivileged-userns) the + // CEF sandbox zygote and/or GPU process can fail to launch. If this + // example traps at boot, build it `--no-default-features` (drops the + // `sandbox` feature so `no_sandbox` is honored) and add the switches + // `--no-sandbox --disable-gpu --in-process-gpu` here. That combination is + // box-dependent and flaky; a real desktop keeps the sandbox default and + // needs none of them. + command_line_args: vec![ + ("--use-mock-keychain".into(), None), + ("password-store".into(), Some("basic".into())), + ], + custom_schemes: vec!["tauri".into(), "ipc".into(), "asset".into(), "probe".into()], + cookieable_schemes: vec!["probe".into()], + ..Default::default() + }); - if std::env::args().any(|arg| arg.starts_with("--type=")) { - tauri_runtime_cef::run_cef_helper_process(); - return; - } + if std::env::args().any(|arg| arg.starts_with("--type=")) { + tauri_runtime_cef::run_cef_helper_process(); + return; + } - std::thread::spawn(|| { - std::thread::sleep(std::time::Duration::from_secs(60)); - println!("PROBE-RESULT {{\"timeout\":true}}"); - std::process::exit(2); - }); + std::thread::spawn(|| { + std::thread::sleep(std::time::Duration::from_secs(60)); + println!("PROBE-RESULT {{\"timeout\":true}}"); + std::process::exit(2); + }); - type Rt = tauri_runtime_cef::CefRuntime; - tauri::Builder::::new() - .register_uri_scheme_protocol("probe", |_ctx, request| { - let respond = |mime: &str, body: &'static str| { - tauri::http::Response::builder() - .header("content-type", mime) - .body(Cow::Borrowed(body.as_bytes())) - .unwrap() - }; - match request.uri().path() { - "/" => respond("text/html", PAGE), - "/sw.js" => respond("text/javascript", SW), - "/report" => { - println!("PROBE-RESULT {}", String::from_utf8_lossy(request.body())); - std::process::exit(0); - } - _ => tauri::http::Response::builder() - .status(404) - .body(Cow::Borrowed(&b""[..])) - .unwrap(), - } - }) - .setup(|app| { - tauri::WebviewWindowBuilder::new( - app, - "probe", - tauri::WebviewUrl::External("probe://app/".parse().unwrap()), - ) - .build()?; - Ok(()) - }) - .run(tauri::test::mock_context(tauri::test::noop_assets())) - .expect("probe app run"); + type Rt = tauri_runtime_cef::CefRuntime; + tauri::Builder::::new() + .register_uri_scheme_protocol("probe", |_ctx, request| { + let respond = |mime: &str, body: &'static str| { + tauri::http::Response::builder() + .header("content-type", mime) + .body(Cow::Borrowed(body.as_bytes())) + .unwrap() + }; + match request.uri().path() { + "/" => respond("text/html", PAGE), + "/sw.js" => respond("text/javascript", SW), + "/report" => { + println!("PROBE-RESULT {}", String::from_utf8_lossy(request.body())); + std::process::exit(0); + } + _ => tauri::http::Response::builder() + .status(404) + .body(Cow::Borrowed(&b""[..])) + .unwrap(), + } + }) + .setup(|app| { + tauri::WebviewWindowBuilder::new( + app, + "probe", + tauri::WebviewUrl::External("probe://app/".parse().unwrap()), + ) + .build()?; + Ok(()) + }) + .run(tauri::test::mock_context(tauri::test::noop_assets())) + .expect("probe app run"); } diff --git a/src/cef_impl/client/drag.rs b/src/cef_impl/client/drag.rs index fd493a2..af7713a 100644 --- a/src/cef_impl/client/drag.rs +++ b/src/cef_impl/client/drag.rs @@ -3,16 +3,16 @@ // SPDX-License-Identifier: MIT use std::{ - path::PathBuf, - sync::{Arc, Mutex}, + path::PathBuf, + sync::{Arc, Mutex}, }; use cef::*; use tauri_runtime::{ - UserEvent, - dpi::PhysicalPosition, - webview::InitializationScript, - window::{DragDropEvent, WindowId}, + UserEvent, + dpi::PhysicalPosition, + webview::InitializationScript, + window::{DragDropEvent, WindowId}, }; use url::Url; @@ -89,53 +89,53 @@ const DRAG_DROP_INIT_SCRIPT: &str = r#" "#; pub(crate) fn drag_drop_initialization_script() -> InitializationScript { - InitializationScript { - script: DRAG_DROP_INIT_SCRIPT.to_string(), - for_main_frame_only: false, - } + InitializationScript { + script: DRAG_DROP_INIT_SCRIPT.to_string(), + for_main_frame_only: false, + } } #[derive(Default)] pub(crate) struct DragDropState { - pub(crate) paths: Option>, - pub(crate) native_entered: bool, - pub(crate) entered: bool, + pub(crate) paths: Option>, + pub(crate) native_entered: bool, + pub(crate) entered: bool, } #[derive(Clone, Copy, PartialEq, Eq)] pub(crate) enum DragDropEventTarget { - Window, - Webview, + Window, + Webview, } #[derive(Clone, serde::Deserialize)] pub(crate) struct DragDropScriptEvent { - #[serde(rename = "type")] - pub(crate) kind: String, - pub(crate) x: f64, - pub(crate) y: f64, + #[serde(rename = "type")] + pub(crate) kind: String, + pub(crate) x: f64, + pub(crate) y: f64, } fn collect_drag_data_paths(drag_data: &mut DragData) -> Vec { - let mut paths = CefStringList::new(); - if drag_data.file_paths(Some(&mut paths)) != 0 { - let paths = paths - .into_iter() - .filter(|path| !path.is_empty()) - .map(PathBuf::from) - .collect::>(); - - if !paths.is_empty() { - return paths; + let mut paths = CefStringList::new(); + if drag_data.file_paths(Some(&mut paths)) != 0 { + let paths = paths + .into_iter() + .filter(|path| !path.is_empty()) + .map(PathBuf::from) + .collect::>(); + + if !paths.is_empty() { + return paths; + } } - } - let file_name = CefStringUtf16::from(&drag_data.file_name()).to_string(); - if file_name.is_empty() { - Vec::new() - } else { - vec![PathBuf::from(file_name)] - } + let file_name = CefStringUtf16::from(&drag_data.file_name()).to_string(); + if file_name.is_empty() { + Vec::new() + } else { + vec![PathBuf::from(file_name)] + } } wrap_drag_handler! { @@ -165,45 +165,45 @@ wrap_drag_handler! { } pub(crate) fn event_from_script_event( - drag_drop_state: &Arc>, - script_event: DragDropScriptEvent, + drag_drop_state: &Arc>, + script_event: DragDropScriptEvent, ) -> Option { - let position = PhysicalPosition::new(script_event.x, script_event.y); - let mut state = drag_drop_state.lock().unwrap(); - if !state.native_entered { - return None; - } - - match script_event.kind.as_str() { - "enter" => { - if state.entered { + let position = PhysicalPosition::new(script_event.x, script_event.y); + let mut state = drag_drop_state.lock().unwrap(); + if !state.native_entered { return None; - } - - let paths = state.paths.clone()?; - state.entered = true; - Some(DragDropEvent::Enter { paths, position }) } - "over" => state.entered.then_some(DragDropEvent::Over { position }), - "drop" => { - let paths = state.entered.then(|| state.paths.take()).flatten(); - state.entered = false; - state.native_entered = false; - paths.map(|paths| DragDropEvent::Drop { paths, position }) - } - "leave" => { - state.native_entered = false; - state.paths = None; - - if state.entered { - state.entered = false; - Some(DragDropEvent::Leave) - } else { - None - } + + match script_event.kind.as_str() { + "enter" => { + if state.entered { + return None; + } + + let paths = state.paths.clone()?; + state.entered = true; + Some(DragDropEvent::Enter { paths, position }) + } + "over" => state.entered.then_some(DragDropEvent::Over { position }), + "drop" => { + let paths = state.entered.then(|| state.paths.take()).flatten(); + state.entered = false; + state.native_entered = false; + paths.map(|paths| DragDropEvent::Drop { paths, position }) + } + "leave" => { + state.native_entered = false; + state.paths = None; + + if state.entered { + state.entered = false; + Some(DragDropEvent::Leave) + } else { + None + } + } + _ => None, } - _ => None, - } } wrap_resource_request_handler! { diff --git a/src/cef_impl/client/life_span.rs b/src/cef_impl/client/life_span.rs index 35de171..007e85d 100644 --- a/src/cef_impl/client/life_span.rs +++ b/src/cef_impl/client/life_span.rs @@ -15,31 +15,31 @@ use crate::runtime::{Message, RuntimeContext}; // "[85296:47750637:0127/131203.017395:ERROR:content/browser/network_service_instance_impl.cc:610] Network service crashed or was terminated, restarting service." // We check the app URL for a while until it actually loads the initial URL. fn check_and_reload_if_blank(browser: cef::Browser, initial_url: String) { - if initial_url == "about:blank" { - return; - } + if initial_url == "about:blank" { + return; + } - std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_secs(1)); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_secs(1)); - let start_time = std::time::Instant::now(); - let timeout = std::time::Duration::from_secs(5); - let check_interval = std::time::Duration::from_millis(100); + let start_time = std::time::Instant::now(); + let timeout = std::time::Duration::from_secs(5); + let check_interval = std::time::Duration::from_millis(100); - while start_time.elapsed() < timeout { - if let Some(frame) = browser.main_frame() { - let url = frame.url(); - let current_url = cef::CefString::from(&url).to_string(); - if current_url.is_empty() || current_url == "about:blank" { - frame.load_url(Some(&cef::CefString::from(initial_url.as_str()))); - // Continue checking in case it loads about:blank again. - } else { - return; + while start_time.elapsed() < timeout { + if let Some(frame) = browser.main_frame() { + let url = frame.url(); + let current_url = cef::CefString::from(&url).to_string(); + if current_url.is_empty() || current_url == "about:blank" { + frame.load_url(Some(&cef::CefString::from(initial_url.as_str()))); + // Continue checking in case it loads about:blank again. + } else { + return; + } + } + std::thread::sleep(check_interval); } - } - std::thread::sleep(check_interval); - } - }); + }); } wrap_life_span_handler! { diff --git a/src/cef_impl/client/mod.rs b/src/cef_impl/client/mod.rs index 675ba63..ecbb967 100644 --- a/src/cef_impl/client/mod.rs +++ b/src/cef_impl/client/mod.rs @@ -9,8 +9,8 @@ use tauri_runtime::{UserEvent, window::WindowId}; use winit::event_loop::EventLoopProxy as WinitEventLoopProxy; use crate::{ - cef_impl::{ipc, request_handler}, - runtime::{Message, RuntimeContext}, + cef_impl::{ipc, request_handler}, + runtime::{Message, RuntimeContext}, }; mod context_menu; @@ -28,8 +28,8 @@ use display::TauriCefDisplayHandler; use download::TauriCefDownloadHandler; use drag::TauriCefDragHandler; pub(crate) use drag::{ - DragDropEventTarget, DragDropScriptEvent, DragDropState, WebDragDropResourceRequestHandler, - drag_drop_initialization_script, event_from_script_event, + DragDropEventTarget, DragDropScriptEvent, DragDropState, WebDragDropResourceRequestHandler, + drag_drop_initialization_script, event_from_script_event, }; use keyboard::TauriCefKeyboardHandler; use life_span::TauriCefChildLifeSpanHandler; @@ -38,31 +38,32 @@ use permission::TauriCefPermissionHandler; pub(crate) use process::TauriCefBrowserProcessHandler; pub(crate) struct TauriCefBrowserClientHandlers { - pub(crate) ipc_handler: Option>>, - pub(crate) on_page_load_handler: Option>, - pub(crate) document_title_changed_handler: - Option>, - pub(crate) navigation_handler: Option>, - pub(crate) address_changed_handler: Option>, - pub(crate) new_window_handler: - Option>, - pub(crate) download_handler: Option>, - pub(crate) web_content_process_terminate_handler: Option>, + pub(crate) ipc_handler: Option>>, + pub(crate) on_page_load_handler: Option>, + pub(crate) document_title_changed_handler: + Option>, + pub(crate) navigation_handler: Option>, + pub(crate) address_changed_handler: Option>, + pub(crate) new_window_handler: Option>, + pub(crate) download_handler: Option>, + pub(crate) web_content_process_terminate_handler: Option>, } impl Clone for TauriCefBrowserClientHandlers { - fn clone(&self) -> Self { - Self { - ipc_handler: self.ipc_handler.clone(), - on_page_load_handler: self.on_page_load_handler.clone(), - document_title_changed_handler: self.document_title_changed_handler.clone(), - navigation_handler: self.navigation_handler.clone(), - address_changed_handler: self.address_changed_handler.clone(), - new_window_handler: self.new_window_handler.clone(), - download_handler: self.download_handler.clone(), - web_content_process_terminate_handler: self.web_content_process_terminate_handler.clone(), + fn clone(&self) -> Self { + Self { + ipc_handler: self.ipc_handler.clone(), + on_page_load_handler: self.on_page_load_handler.clone(), + document_title_changed_handler: self.document_title_changed_handler.clone(), + navigation_handler: self.navigation_handler.clone(), + address_changed_handler: self.address_changed_handler.clone(), + new_window_handler: self.new_window_handler.clone(), + download_handler: self.download_handler.clone(), + web_content_process_terminate_handler: self + .web_content_process_terminate_handler + .clone(), + } } - } } wrap_client! { diff --git a/src/cef_impl/client/process.rs b/src/cef_impl/client/process.rs index 23a6a7b..92a2f2c 100644 --- a/src/cef_impl/client/process.rs +++ b/src/cef_impl/client/process.rs @@ -3,8 +3,8 @@ // SPDX-License-Identifier: MIT use std::sync::{ - Arc, - atomic::{AtomicBool, Ordering}, + Arc, + atomic::{AtomicBool, Ordering}, }; use cef::*; @@ -26,7 +26,8 @@ wrap_browser_process_handler! { } fn on_schedule_message_pump_work(&self, delay_ms: i64) { - self.context.cef_pump.schedule_message_pump_work(delay_ms); + self.context.cef_pump.on_schedule_message_pump_work(delay_ms); + self.context.proxy.wake_up(); } fn on_already_running_app_relaunch( diff --git a/src/cef_impl/cookie.rs b/src/cef_impl/cookie.rs index 79bfcde..5509d07 100644 --- a/src/cef_impl/cookie.rs +++ b/src/cef_impl/cookie.rs @@ -14,49 +14,49 @@ type CookieResultSender = Sender>>>; type CollectedCookies = Arc>>>; fn cookie_from_cef(cookie: &cef::Cookie) -> Cookie<'static> { - let name = cookie.name.to_string(); - let value = cookie.value.to_string(); - let domain = cookie.domain.to_string(); - let path = cookie.path.to_string(); - - let mut builder = Cookie::build((name, value)); - if !domain.is_empty() { - builder = builder.domain(domain); - } - if !path.is_empty() { - builder = builder.path(path); - } - if cookie.secure == 1 { - builder = builder.secure(true); - } - if cookie.httponly == 1 { - builder = builder.http_only(true); - } + let name = cookie.name.to_string(); + let value = cookie.value.to_string(); + let domain = cookie.domain.to_string(); + let path = cookie.path.to_string(); + + let mut builder = Cookie::build((name, value)); + if !domain.is_empty() { + builder = builder.domain(domain); + } + if !path.is_empty() { + builder = builder.path(path); + } + if cookie.secure == 1 { + builder = builder.secure(true); + } + if cookie.httponly == 1 { + builder = builder.http_only(true); + } - builder.build().into_owned() + builder.build().into_owned() } fn cef_cookie_from_cookie(cookie: &Cookie<'_>) -> cef::Cookie { - let mut cef_cookie = cef::Cookie { - name: cef::CefString::from(cookie.name()), - value: cef::CefString::from(cookie.value()), - ..Default::default() - }; - - if let Some(domain) = cookie.domain() { - cef_cookie.domain = cef::CefString::from(domain); - } - if let Some(path) = cookie.path() { - cef_cookie.path = cef::CefString::from(path); - } - if cookie.secure().unwrap_or(false) { - cef_cookie.secure = 1; - } - if cookie.http_only().unwrap_or(false) { - cef_cookie.httponly = 1; - } + let mut cef_cookie = cef::Cookie { + name: cef::CefString::from(cookie.name()), + value: cef::CefString::from(cookie.value()), + ..Default::default() + }; + + if let Some(domain) = cookie.domain() { + cef_cookie.domain = cef::CefString::from(domain); + } + if let Some(path) = cookie.path() { + cef_cookie.path = cef::CefString::from(path); + } + if cookie.secure().unwrap_or(false) { + cef_cookie.secure = 1; + } + if cookie.http_only().unwrap_or(false) { + cef_cookie.httponly = 1; + } - cef_cookie + cef_cookie } cef::wrap_cookie_visitor! { @@ -111,59 +111,58 @@ cef::wrap_cookie_visitor! { // otherwise a query against a URL/store with no matching cookies would never // send and the dispatcher's blocking `recv` would hang forever. impl Drop for CollectUrlCookiesVisitor { - fn drop(&mut self) { - let _ = self.tx.send(Ok(self.collected.lock().unwrap().clone())); - } + fn drop(&mut self) { + let _ = self.tx.send(Ok(self.collected.lock().unwrap().clone())); + } } impl Drop for CollectAllCookiesVisitor { - fn drop(&mut self) { - let _ = self.tx.send(Ok(self.collected.lock().unwrap().clone())); - } + fn drop(&mut self) { + let _ = self.tx.send(Ok(self.collected.lock().unwrap().clone())); + } } pub(crate) fn visit_url_cookies(manager: CookieManager, url: Url, tx: CookieResultSender) { - let collected = Arc::new(Mutex::new(Vec::new())); - let mut visitor = CollectUrlCookiesVisitor::new(tx, collected); - let url = cef::CefString::from(url.as_str()); + let collected = Arc::new(Mutex::new(Vec::new())); + let mut visitor = CollectUrlCookiesVisitor::new(tx, collected); + let url = cef::CefString::from(url.as_str()); - // The result is sent from the visitor's `Drop`, including when this call fails - // to start the visit (the visitor is dropped here and sends the empty result). - manager.visit_url_cookies(Some(&url), 1, Some(&mut visitor)); + // The result is sent from the visitor's `Drop`, including when this call fails + // to start the visit (the visitor is dropped here and sends the empty result). + manager.visit_url_cookies(Some(&url), 1, Some(&mut visitor)); } pub(crate) fn visit_all_cookies(manager: CookieManager, tx: CookieResultSender) { - let collected = Arc::new(Mutex::new(Vec::new())); - let mut visitor = CollectAllCookiesVisitor::new(tx, collected); + let collected = Arc::new(Mutex::new(Vec::new())); + let mut visitor = CollectAllCookiesVisitor::new(tx, collected); - manager.visit_all_cookies(Some(&mut visitor)); + manager.visit_all_cookies(Some(&mut visitor)); } pub(crate) fn set_cookie(manager: CookieManager, url: Option, cookie: Cookie<'static>) { - let cef_cookie = cef_cookie_from_cookie(&cookie); - let url = url.as_ref().map(|u| cef::CefString::from(u.as_str())); - manager.set_cookie( - url.as_ref(), - Some(&cef_cookie), - Option::<&mut cef::SetCookieCallback>::None, - ); + let cef_cookie = cef_cookie_from_cookie(&cookie); + let url = url.as_ref().map(|u| cef::CefString::from(u.as_str())); + manager.set_cookie( + url.as_ref(), + Some(&cef_cookie), + Option::<&mut cef::SetCookieCallback>::None, + ); } pub(crate) fn delete_cookie(manager: CookieManager, url: Option, cookie: Cookie<'static>) { - let url = url.as_ref().map(|u| cef::CefString::from(u.as_str())); - let name = cef::CefString::from(cookie.name()); - manager.delete_cookies( - url.as_ref(), - Some(&name), - Option::<&mut cef::DeleteCookiesCallback>::None, - ); + let url = url.as_ref().map(|u| cef::CefString::from(u.as_str())); + let name = cef::CefString::from(cookie.name()); + manager.delete_cookies( + url.as_ref(), + Some(&name), + Option::<&mut cef::DeleteCookiesCallback>::None, + ); } impl AppWebview { - pub fn cookie_manager(&self) -> Option { - self - .host - .request_context() - .and_then(|rc| rc.cookie_manager(None)) - } + pub fn cookie_manager(&self) -> Option { + self.host + .request_context() + .and_then(|rc| rc.cookie_manager(None)) + } } diff --git a/src/cef_impl/ipc.rs b/src/cef_impl/ipc.rs index 9a57c6d..e166d19 100644 --- a/src/cef_impl/ipc.rs +++ b/src/cef_impl/ipc.rs @@ -8,14 +8,14 @@ use cef::*; use tauri_runtime::{UserEvent, webview::DetachedWebview}; use crate::{ - cef_impl::client::TauriCefBrowserClient, runtime::CefRuntime, webview::CefWebviewDispatcher, + cef_impl::client::TauriCefBrowserClient, runtime::CefRuntime, webview::CefWebviewDispatcher, }; const IPC_MESSAGE_NAME: &str = "tauri:ipc"; const IPC_POST_MESSAGE_FUNCTION: &str = "postMessage"; pub(crate) type IpcHandler = - dyn Fn(DetachedWebview>, http::Request) + Send; + dyn Fn(DetachedWebview>, http::Request) + Send; wrap_v8_handler! { struct IpcPostMessageV8Handler; @@ -75,35 +75,35 @@ wrap_v8_handler! { } fn install_ipc_post_message(context: Option<&mut V8Context>) { - let Some(window) = context.and_then(|context| context.global()) else { - return; - }; - let attributes = sys::cef_v8_propertyattribute_t( - [ - sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_READONLY, - sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_DONTENUM, - sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_DONTDELETE, - ] - .into_iter() - .fold(0, |acc, attr| acc | attr.0), - ) - .into(); - let Some(mut ipc) = v8_value_create_object(None, None) else { - return; - }; - let mut handler = IpcPostMessageV8Handler::new(); - let post_message_name = CefString::from(IPC_POST_MESSAGE_FUNCTION); - let Some(mut post_message) = - v8_value_create_function(Some(&post_message_name), Some(&mut handler)) - else { - return; - }; - ipc.set_value_bykey( - Some(&post_message_name), - Some(&mut post_message), - attributes, - ); - window.set_value_bykey(Some(&CefString::from("ipc")), Some(&mut ipc), attributes); + let Some(window) = context.and_then(|context| context.global()) else { + return; + }; + let attributes = sys::cef_v8_propertyattribute_t( + [ + sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_READONLY, + sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_DONTENUM, + sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_DONTDELETE, + ] + .into_iter() + .fold(0, |acc, attr| acc | attr.0), + ) + .into(); + let Some(mut ipc) = v8_value_create_object(None, None) else { + return; + }; + let mut handler = IpcPostMessageV8Handler::new(); + let post_message_name = CefString::from(IPC_POST_MESSAGE_FUNCTION); + let Some(mut post_message) = + v8_value_create_function(Some(&post_message_name), Some(&mut handler)) + else { + return; + }; + ipc.set_value_bykey( + Some(&post_message_name), + Some(&mut post_message), + attributes, + ); + window.set_value_bykey(Some(&CefString::from("ipc")), Some(&mut ipc), attributes); } wrap_render_process_handler! { @@ -122,68 +122,68 @@ wrap_render_process_handler! { } pub(crate) fn on_process_message_received( - client: &TauriCefBrowserClient, - frame: Option<&mut Frame>, - source_process: ProcessId, - message: Option<&mut ProcessMessage>, + client: &TauriCefBrowserClient, + frame: Option<&mut Frame>, + source_process: ProcessId, + message: Option<&mut ProcessMessage>, ) -> std::os::raw::c_int { - if source_process != ProcessId::RENDERER { - return 0; - } - let Some(message) = message else { - return 0; - }; - if CefString::from(&message.name()).to_string() != IPC_MESSAGE_NAME { - return 0; - } - let Some(handler) = client.handlers.ipc_handler.as_ref() else { - return 1; - }; - let Some(args) = message.argument_list() else { - return 1; - }; - - let mut url = CefString::from(&args.string(0)).to_string(); - if url.is_empty() - && let Some(frame) = frame - { - url = CefString::from(&frame.url()).to_string(); - } - let body = CefString::from(&args.string(1)).to_string(); - - if let Ok(request) = http::Request::builder().uri(url).body(body) { - let webview = DetachedWebview { - label: client.label.clone(), - dispatcher: CefWebviewDispatcher { - window_id: Arc::new(Mutex::new(client.window_id)), - webview_id: client.webview_id, - context: client.context.clone(), - }, + if source_process != ProcessId::RENDERER { + return 0; + } + let Some(message) = message else { + return 0; }; - // Run the handler through the event loop instead of inside this CEF - // callout. A sync tauri command that round-trips the loop (window - // creation, blocking getters) would otherwise self-deadlock whenever this - // callout runs on the main thread OUTSIDE a winit callback — no current - // dispatch is installed, so the round-trip queues a message the parked - // loop can never drain. That is the steady state on macOS, where CEF work - // is pumped from NSRunLoop timer callouts (huddle pop-out froze the whole - // browser process). Where a dispatch IS installed (Linux services CEF via - // glib inside winit callbacks), send_message degenerates to the same - // inline call as before. - // - // ThreadSafe: the handler Arc is not Sync, but it never actually crosses - // threads — this callout runs on the CEF UI thread (the runtime main - // thread), and Message::Task closures execute on that same thread. - let handler = crate::cef_impl::request_handler::ThreadSafe(handler.clone()); - if let Err(error) = client - .context - .send_message(crate::runtime::Message::Task(Box::new(move || { - (handler.into_owned())(webview, request); - }))) + if CefString::from(&message.name()).to_string() != IPC_MESSAGE_NAME { + return 0; + } + let Some(handler) = client.handlers.ipc_handler.as_ref() else { + return 1; + }; + let Some(args) = message.argument_list() else { + return 1; + }; + + let mut url = CefString::from(&args.string(0)).to_string(); + if url.is_empty() + && let Some(frame) = frame { - // Only fails when the loop is gone (shutdown) — the invoke is moot then. - log::debug!("dropped webview IPC message: {error}"); + url = CefString::from(&frame.url()).to_string(); } - } - 1 + let body = CefString::from(&args.string(1)).to_string(); + + if let Ok(request) = http::Request::builder().uri(url).body(body) { + let webview = DetachedWebview { + label: client.label.clone(), + dispatcher: CefWebviewDispatcher { + window_id: Arc::new(Mutex::new(client.window_id)), + webview_id: client.webview_id, + context: client.context.clone(), + }, + }; + // Run the handler through the event loop instead of inside this CEF + // callout. A sync tauri command that round-trips the loop (window + // creation, blocking getters) would otherwise self-deadlock whenever this + // callout runs on the main thread OUTSIDE a winit callback — no current + // dispatch is installed, so the round-trip queues a message the parked + // loop can never drain. That is the steady state on macOS, where CEF work + // is pumped from NSRunLoop timer callouts (huddle pop-out froze the whole + // browser process). Where a dispatch IS installed (Linux services CEF via + // glib inside winit callbacks), send_message degenerates to the same + // inline call as before. + // + // ThreadSafe: the handler Arc is not Sync, but it never actually crosses + // threads — this callout runs on the CEF UI thread (the runtime main + // thread), and Message::Task closures execute on that same thread. + let handler = crate::cef_impl::request_handler::ThreadSafe(handler.clone()); + if let Err(error) = client + .context + .send_message(crate::runtime::Message::Task(Box::new(move || { + (handler.into_owned())(webview, request); + }))) + { + // Only fails when the loop is gone (shutdown) — the invoke is moot then. + log::debug!("dropped webview IPC message: {error}"); + } + } + 1 } diff --git a/src/cef_impl/request_context.rs b/src/cef_impl/request_context.rs index af705ff..62c41e4 100644 --- a/src/cef_impl/request_context.rs +++ b/src/cef_impl/request_context.rs @@ -3,13 +3,13 @@ // SPDX-License-Identifier: MIT use std::{ - fs::create_dir_all, - path::{Component, Path, PathBuf}, - sync::{ - Arc, Mutex, - atomic::{AtomicBool, Ordering}, - }, - time::Duration, + fs::create_dir_all, + path::{Component, Path, PathBuf}, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, + }, + time::Duration, }; use base64::Engine; @@ -22,17 +22,17 @@ use crate::cef_impl::request_handler; #[inline] fn theme_to_color_variant(theme: Option) -> ColorVariant { - match theme { - Some(Theme::Dark) => ColorVariant::DARK, - Some(Theme::Light) => ColorVariant::LIGHT, - _ => ColorVariant::SYSTEM, - } + match theme { + Some(Theme::Dark) => ColorVariant::DARK, + Some(Theme::Light) => ColorVariant::LIGHT, + _ => ColorVariant::SYSTEM, + } } pub(crate) fn apply_theme_scheme(request_context: Option<&RequestContext>, theme: Option) { - if let Some(request_context) = request_context { - request_context.set_chrome_color_scheme(theme_to_color_variant(theme), 0); - } + if let Some(request_context) = request_context { + request_context.set_chrome_color_scheme(theme_to_color_variant(theme), 0); + } } /// Resolves a CEF-compatible cache path for a per-webview request context. @@ -57,35 +57,35 @@ pub(crate) fn apply_theme_scheme(request_context: Option<&RequestContext>, theme /// `data_directory` values produce distinct profiles, and the same value /// maps to the same on-disk profile across runs. fn resolve_request_context_cache_path(global_cache_path: &Path, data_directory: &Path) -> PathBuf { - if data_directory.is_absolute() { - if data_directory.starts_with(global_cache_path) { - return data_directory.to_path_buf(); + if data_directory.is_absolute() { + if data_directory.starts_with(global_cache_path) { + return data_directory.to_path_buf(); + } else { + log::warn!( + "data directory is not a child of the global cache path, we will derive a profile hash from it" + ); + } + } else if !data_directory + .components() + .any(|component| matches!(component, Component::ParentDir)) + { + return global_cache_path.join(data_directory); } else { - log::warn!( - "data directory is not a child of the global cache path, we will derive a profile hash from it" - ); + log::warn!( + "data directory is a relative path with parent components, we will derive a profile hash from it" + ); } - } else if !data_directory - .components() - .any(|component| matches!(component, Component::ParentDir)) - { - return global_cache_path.join(data_directory); - } else { - log::warn!( - "data directory is a relative path with parent components, we will derive a profile hash from it" - ); - } - let mut hasher = Sha256::new(); - hasher.update(data_directory.as_os_str().as_encoded_bytes()); - let hash = hasher.finalize(); - let suffix = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&hash[..16]); - let path = global_cache_path.join(format!("Profile-{suffix}")); - log::info!( - "derived profile hash from data directory: {suffix}, cache path: {}", - path.display() - ); - path + let mut hasher = Sha256::new(); + hasher.update(data_directory.as_os_str().as_encoded_bytes()); + let hash = hasher.finalize(); + let suffix = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&hash[..16]); + let path = global_cache_path.join(format!("Profile-{suffix}")); + log::info!( + "derived profile hash from data directory: {suffix}, cache path: {}", + path.display() + ); + path } /// Continuation invoked on the CEF UI thread once the request context's @@ -103,25 +103,25 @@ pub(crate) type RequestContextInitContinuation = Box( - work: F, + work: F, ) -> (Arc, RequestContextInitContinuation) where - F: FnOnce(Option) + 'static, + F: FnOnce(Option) + 'static, { - struct Guard(Arc); - impl Drop for Guard { - fn drop(&mut self) { - self.0.store(true, Ordering::SeqCst); + struct Guard(Arc); + impl Drop for Guard { + fn drop(&mut self) { + self.0.store(true, Ordering::SeqCst); + } } - } - let flag = Arc::new(AtomicBool::new(false)); - let guard = Guard(flag.clone()); - let wrapped: RequestContextInitContinuation = Box::new(move |request_context| { - let _guard = guard; - work(request_context); - }); - (flag, wrapped) + let flag = Arc::new(AtomicBool::new(false)); + let guard = Guard(flag.clone()); + let wrapped: RequestContextInitContinuation = Box::new(move |request_context| { + let _guard = guard; + work(request_context); + }); + (flag, wrapped) } /// Block the calling thread until `flag` is `true`. @@ -157,18 +157,18 @@ where /// `webview.open_devtools()`, `webview.on_dev_tools_protocol(...)`) can find /// the webview. pub(crate) fn wait_for_deferred_init(flag: &Arc) { - let on_ui_thread = cef::currently_on(cef::sys::cef_thread_id_t::TID_UI.into()) != 0; + let on_ui_thread = cef::currently_on(cef::sys::cef_thread_id_t::TID_UI.into()) != 0; - if on_ui_thread { - let _allow = AllowNestableTasks::enter(); - while !flag.load(Ordering::SeqCst) { - cef::do_message_loop_work(); - } - } else { - while !flag.load(Ordering::SeqCst) { - std::thread::sleep(Duration::from_millis(1)); + if on_ui_thread { + let _allow = AllowNestableTasks::enter(); + while !flag.load(Ordering::SeqCst) { + cef::do_message_loop_work(); + } + } else { + while !flag.load(Ordering::SeqCst) { + std::thread::sleep(Duration::from_millis(1)); + } } - } } /// RAII guard that scopes `CefSetNestableTasksAllowed(true)` for the current @@ -183,28 +183,28 @@ pub(crate) fn wait_for_deferred_init(flag: &Arc) { struct AllowNestableTasks; impl AllowNestableTasks { - fn enter() -> Self { - NESTABLE_TASKS_DEPTH.with(|depth| { - let current = depth.get(); - if current == 0 { - cef::set_nestable_tasks_allowed(1); - } - depth.set(current + 1); - }); - Self - } + fn enter() -> Self { + NESTABLE_TASKS_DEPTH.with(|depth| { + let current = depth.get(); + if current == 0 { + cef::set_nestable_tasks_allowed(1); + } + depth.set(current + 1); + }); + Self + } } impl Drop for AllowNestableTasks { - fn drop(&mut self) { - NESTABLE_TASKS_DEPTH.with(|depth| { - let current = depth.get(); - depth.set(current - 1); - if current == 1 { - cef::set_nestable_tasks_allowed(0); - } - }); - } + fn drop(&mut self) { + NESTABLE_TASKS_DEPTH.with(|depth| { + let current = depth.get(); + depth.set(current - 1); + if current == 1 { + cef::set_nestable_tasks_allowed(0); + } + }); + } } thread_local! { @@ -258,137 +258,138 @@ wrap_request_context_handler! { /// Applies a fixed-server proxy to a request context via the Chromium `proxy` /// preference. Must be called after the request context has initialized. fn apply_proxy(request_context: &RequestContext, proxy_url: &url::Url) { - use cef::{ImplDictionaryValue, ImplValue}; + use cef::{ImplDictionaryValue, ImplValue}; - let scheme = match proxy_url.scheme() { - "socks5" | "socks5h" => "socks5", - "socks4" | "socks4a" => "socks4", - "https" => "https", - _ => "http", - }; - let Some(host) = proxy_url.host_str() else { - log::warn!("ignoring proxy URL without a host: {proxy_url}"); - return; - }; - let server = match proxy_url.port_or_known_default() { - Some(port) => format!("{scheme}://{host}:{port}"), - None => format!("{scheme}://{host}"), - }; + let scheme = match proxy_url.scheme() { + "socks5" | "socks5h" => "socks5", + "socks4" | "socks4a" => "socks4", + "https" => "https", + _ => "http", + }; + let Some(host) = proxy_url.host_str() else { + log::warn!("ignoring proxy URL without a host: {proxy_url}"); + return; + }; + let server = match proxy_url.port_or_known_default() { + Some(port) => format!("{scheme}://{host}:{port}"), + None => format!("{scheme}://{host}"), + }; - let pref_name = "proxy"; - if request_context.can_set_preference(Some(&pref_name.into())) != 1 { - log::warn!("the CEF request context does not allow setting the proxy preference"); - return; - } + let pref_name = "proxy"; + if request_context.can_set_preference(Some(&pref_name.into())) != 1 { + log::warn!("the CEF request context does not allow setting the proxy preference"); + return; + } - // Build `{ "mode": "fixed_servers", "server": "://:" }`. - let Some(dict) = cef::dictionary_value_create() else { - return; - }; - dict.set_string(Some(&"mode".into()), Some(&"fixed_servers".into())); - dict.set_string(Some(&"server".into()), Some(&server.as_str().into())); + // Build `{ "mode": "fixed_servers", "server": "://:" }`. + let Some(dict) = cef::dictionary_value_create() else { + return; + }; + dict.set_string(Some(&"mode".into()), Some(&"fixed_servers".into())); + dict.set_string(Some(&"server".into()), Some(&server.as_str().into())); - let Some(value) = cef::value_create() else { - return; - }; - let mut dict = dict; - value.set_dictionary(Some(&mut dict)); + let Some(value) = cef::value_create() else { + return; + }; + let mut dict = dict; + value.set_dictionary(Some(&mut dict)); - let mut value = value; - if request_context.set_preference(Some(&pref_name.into()), Some(&mut value), None) != 1 { - log::error!("failed to apply the proxy preference to the CEF request context"); - } + let mut value = value; + if request_context.set_preference(Some(&pref_name.into()), Some(&mut value), None) != 1 { + log::error!("failed to apply the proxy preference to the CEF request context"); + } } pub(crate) fn request_context_from_webview_attributes<'a>( - global_cache_path: &Path, - webview_attributes: &WebviewAttributes, - custom_schemes: impl IntoIterator, - custom_protocol_scheme: &str, - scheme_registry: request_handler::SchemeRegistry, - on_initialized: RequestContextInitContinuation, + global_cache_path: &Path, + webview_attributes: &WebviewAttributes, + custom_schemes: impl IntoIterator, + custom_protocol_scheme: &str, + scheme_registry: request_handler::SchemeRegistry, + on_initialized: RequestContextInitContinuation, ) -> Option { - let cache_path = if webview_attributes.incognito { - CefStringUtf16::from("") - } else if let Some(data_directory) = &webview_attributes.data_directory { - let cache_path = resolve_request_context_cache_path(global_cache_path, data_directory); - if let Err(error) = create_dir_all(&cache_path) { - log::error!( - "failed to create request context cache directory {}: {error}", - cache_path.display() - ); - } - CefStringUtf16::from(cache_path.to_string_lossy().as_ref()) - } else { - let global_context = - request_context_get_global_context().expect("Failed to get global request context"); - // global_cache_path does not work here - global_context.cache_path() returns the proper profile path. - (&global_context.cache_path()).into() - }; + let cache_path = if webview_attributes.incognito { + CefStringUtf16::from("") + } else if let Some(data_directory) = &webview_attributes.data_directory { + let cache_path = resolve_request_context_cache_path(global_cache_path, data_directory); + if let Err(error) = create_dir_all(&cache_path) { + log::error!( + "failed to create request context cache directory {}: {error}", + cache_path.display() + ); + } + CefStringUtf16::from(cache_path.to_string_lossy().as_ref()) + } else { + let global_context = + request_context_get_global_context().expect("Failed to get global request context"); + // global_cache_path does not work here - global_context.cache_path() returns the proper profile path. + (&global_context.cache_path()).into() + }; - let settings = RequestContextSettings { - cache_path, - // Per-context settings do not inherit the global value, so an empty list - // here would silently drop custom-scheme cookie support configured - // through `CefConfig::cookieable_schemes`. - cookieable_schemes_list: crate::config::config() - .cookieable_schemes - .join(",") - .as_str() - .into(), - ..Default::default() - }; + let settings = RequestContextSettings { + cache_path, + // Per-context settings do not inherit the global value, so an empty list + // here would silently drop custom-scheme cookie support configured + // through `CefConfig::cookieable_schemes`. + cookieable_schemes_list: crate::config::config() + .cookieable_schemes + .join(",") + .as_str() + .into(), + ..Default::default() + }; - // Holds a strong reference to the `RequestContext` until the - // `on_request_context_initialized` callback fires. CEF keeps the underlying - // C++ `CefRequestContextImpl` alive during async profile creation through - // its own bound callbacks, but holding an explicit reference here guarantees - // we don't race with reference-count releases on shutdown paths. - let rc_holder: Arc>> = Arc::new(Mutex::new(None)); - let proxy_url = webview_attributes.proxy_url.clone(); - let wrapped_callback: RequestContextInitContinuation = Box::new({ - let rc_holder = rc_holder.clone(); - move |rc| { - // The proxy preference can only be set once the request context's - // underlying profile has finished initializing, which is exactly what - // this continuation signals. - if let (Some(rc), Some(proxy_url)) = (rc.as_ref(), proxy_url.as_ref()) { - apply_proxy(rc, proxy_url); - } - on_initialized(rc); - let _released = rc_holder.lock().unwrap().take(); - } - }); + // Holds a strong reference to the `RequestContext` until the + // `on_request_context_initialized` callback fires. CEF keeps the underlying + // C++ `CefRequestContextImpl` alive during async profile creation through + // its own bound callbacks, but holding an explicit reference here guarantees + // we don't race with reference-count releases on shutdown paths. + let rc_holder: Arc>> = Arc::new(Mutex::new(None)); + let proxy_url = webview_attributes.proxy_url.clone(); + let wrapped_callback: RequestContextInitContinuation = Box::new({ + let rc_holder = rc_holder.clone(); + move |rc| { + // The proxy preference can only be set once the request context's + // underlying profile has finished initializing, which is exactly what + // this continuation signals. + if let (Some(rc), Some(proxy_url)) = (rc.as_ref(), proxy_url.as_ref()) { + apply_proxy(rc, proxy_url); + } + on_initialized(rc); + let _released = rc_holder.lock().unwrap().take(); + } + }); - let mut handler = WebviewRequestContextHandler::new(Arc::new(Mutex::new(Some(wrapped_callback)))); - let request_context = request_context_create_context(Some(&settings), Some(&mut handler)); - *rc_holder.lock().unwrap() = request_context.clone(); + let mut handler = + WebviewRequestContextHandler::new(Arc::new(Mutex::new(Some(wrapped_callback)))); + let request_context = request_context_create_context(Some(&settings), Some(&mut handler)); + *rc_holder.lock().unwrap() = request_context.clone(); - if let Some(request_context) = request_context.as_ref() { - for scheme in custom_schemes { - // Windows/Android-style form: `http(s)://.localhost/…`. - request_context.register_scheme_handler_factory( - Some(&custom_protocol_scheme.into()), - Some(&format!("{scheme}.localhost").as_str().into()), - Some(&mut request_handler::UriSchemeHandlerFactory::new( - scheme_registry.clone(), - scheme.clone(), - )), - ); - // Native form published tauri emits on Linux/macOS: - // `://localhost/…`. The scheme itself is made known to - // Chromium in `on_register_custom_schemes` (crate config list); an - // empty domain filter matches every host on the scheme. - request_context.register_scheme_handler_factory( - Some(&scheme.as_str().into()), - None, - Some(&mut request_handler::UriSchemeHandlerFactory::new( - scheme_registry.clone(), - scheme.clone(), - )), - ); + if let Some(request_context) = request_context.as_ref() { + for scheme in custom_schemes { + // Windows/Android-style form: `http(s)://.localhost/…`. + request_context.register_scheme_handler_factory( + Some(&custom_protocol_scheme.into()), + Some(&format!("{scheme}.localhost").as_str().into()), + Some(&mut request_handler::UriSchemeHandlerFactory::new( + scheme_registry.clone(), + scheme.clone(), + )), + ); + // Native form published tauri emits on Linux/macOS: + // `://localhost/…`. The scheme itself is made known to + // Chromium in `on_register_custom_schemes` (crate config list); an + // empty domain filter matches every host on the scheme. + request_context.register_scheme_handler_factory( + Some(&scheme.as_str().into()), + None, + Some(&mut request_handler::UriSchemeHandlerFactory::new( + scheme_registry.clone(), + scheme.clone(), + )), + ); + } } - } - request_context + request_context } diff --git a/src/cef_impl/request_handler.rs b/src/cef_impl/request_handler.rs index 28e6ec1..9d26dc6 100644 --- a/src/cef_impl/request_handler.rs +++ b/src/cef_impl/request_handler.rs @@ -3,33 +3,33 @@ // SPDX-License-Identifier: MIT use std::{ - borrow::Cow, - io::{Cursor, Read}, - sync::{Arc, Mutex}, + borrow::Cow, + io::{Cursor, Read}, + sync::{Arc, Mutex}, }; use cef::{rc::*, *}; use dioxus_debug_cell::RefCell; use html5ever::{LocalName, interface::QualName, namespace_url, ns}; use http::{ - HeaderMap, HeaderName, HeaderValue, - header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE, ORIGIN}, + HeaderMap, HeaderName, HeaderValue, + header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE, ORIGIN}, }; use kuchiki::NodeRef; use tauri_runtime::{UserEvent, window::WindowId}; use crate::compat::{NavigationHandler, UriSchemeProtocolHandler}; use tauri_utils::{ - config::{Csp, CspDirectiveSources}, - html::{parse as parse_html, serialize_node}, + config::{Csp, CspDirectiveSources}, + html::{parse as parse_html, serialize_node}, }; use url::Url; use crate::{ - cef_impl::client::{DragDropEventTarget, DragDropState, WebDragDropResourceRequestHandler}, - runtime::RuntimeContext, - streaming::{self, InitiatorOrigin, ReadOutcome, StreamBody}, - webview::{CefInitScript, INITIAL_LOAD_URL}, + cef_impl::client::{DragDropEventTarget, DragDropState, WebDragDropResourceRequestHandler}, + runtime::RuntimeContext, + streaming::{self, InitiatorOrigin, ReadOutcome, StreamBody}, + webview::{CefInitScript, INITIAL_LOAD_URL}, }; type HttpResponse = Arc>>>>>; @@ -43,79 +43,79 @@ type HttpResponse = Arc>>>>>; type StreamCell = Arc>>; struct StreamState { - head: Arc>>>, - body: StreamBody, + head: Arc>>>, + body: StreamBody, } pub(crate) type SchemeRegistry = Arc< - Mutex< - std::collections::HashMap< - (i32, String), - ( - String, - Arc>, - Arc>, - ), + Mutex< + std::collections::HashMap< + (i32, String), + ( + String, + Arc>, + Arc>, + ), + >, >, - >, >; fn csp_inject_initialization_scripts_hashes( - existing_csp: String, - initialization_scripts: &[CefInitScript], + existing_csp: String, + initialization_scripts: &[CefInitScript], ) -> String { - if initialization_scripts.is_empty() { - return existing_csp; - } + if initialization_scripts.is_empty() { + return existing_csp; + } - let script_hashes: Vec = initialization_scripts - .iter() - .map(|s| s.hash.clone()) - .collect(); + let script_hashes: Vec = initialization_scripts + .iter() + .map(|s| s.hash.clone()) + .collect(); - if script_hashes.is_empty() { - return existing_csp; - } + if script_hashes.is_empty() { + return existing_csp; + } - let mut csp_map: std::collections::HashMap = - Csp::Policy(existing_csp.to_string()).into(); + let mut csp_map: std::collections::HashMap = + Csp::Policy(existing_csp.to_string()).into(); - let script_src = csp_map - .entry("script-src".to_string()) - .or_insert_with(|| CspDirectiveSources::List(vec!["'self'".to_string()])); + let script_src = csp_map + .entry("script-src".to_string()) + .or_insert_with(|| CspDirectiveSources::List(vec!["'self'".to_string()])); - script_src.extend(script_hashes); + script_src.extend(script_hashes); - Csp::DirectiveMap(csp_map).to_string() + Csp::DirectiveMap(csp_map).to_string() } fn inject_scripts_into_html_body( - body: &[u8], - initialization_scripts: &[CefInitScript], + body: &[u8], + initialization_scripts: &[CefInitScript], ) -> Option> { - let Ok(body_str) = std::str::from_utf8(body) else { - return None; - }; - - let document = parse_html(body_str.to_string()); - - let head = if let Ok(ref head_node) = document.select_first("head") { - head_node.as_node().clone() - } else { - let head_node = NodeRef::new_element( - QualName::new(None, ns!(html), LocalName::from("head")), - None, - ); - document.prepend(head_node.clone()); - head_node - }; - - for init_script in initialization_scripts.iter().rev() { - let script_el = NodeRef::new_element(QualName::new(None, ns!(html), "script".into()), None); - script_el.append(NodeRef::new_text(init_script.script.as_str())); - head.prepend(script_el); - } + let Ok(body_str) = std::str::from_utf8(body) else { + return None; + }; + + let document = parse_html(body_str.to_string()); + + let head = if let Ok(ref head_node) = document.select_first("head") { + head_node.as_node().clone() + } else { + let head_node = NodeRef::new_element( + QualName::new(None, ns!(html), LocalName::from("head")), + None, + ); + document.prepend(head_node.clone()); + head_node + }; - Some(serialize_node(&document)) + for init_script in initialization_scripts.iter().rev() { + let script_el = NodeRef::new_element(QualName::new(None, ns!(html), "script".into()), None); + script_el.append(NodeRef::new_text(init_script.script.as_str())); + head.prepend(script_el); + } + + Some(serialize_node(&document)) } wrap_request_handler! { @@ -217,48 +217,48 @@ wrap_request_handler! { /// (`-1`). Shared by the buffered and streaming `response_headers` paths, which /// differ only in the head's body type. fn write_response_headers( - cef_response: &mut Response, - head: &http::Response, - response_length: Option<&mut i64>, - redirect_url: Option<&mut CefString>, + cef_response: &mut Response, + head: &http::Response, + response_length: Option<&mut i64>, + redirect_url: Option<&mut CefString>, ) { - cef_response.set_status(head.status().as_u16() as i32); - let mut content_type = None; - - // Apply via a multimap so REPEATED header names survive — `Set-Cookie` is - // the common one, and a page that sets two cookies in a single response must - // keep both. `set_header_by_name(.., overwrite=0)` per value silently drops - // the second (it only sets when the name is absent), so build the whole map - // and set it once. `http::HeaderMap`'s iterator yields (name, value) for - // every value, so duplicates come through naturally. - let mut map = CefStringMultimap::new(); - for (name, value) in head.headers() { - let Ok(value) = value.to_str() else { - continue; - }; - map.append(name.as_str(), value); - if name == CONTENT_TYPE { - content_type.replace(value.to_string()); + cef_response.set_status(head.status().as_u16() as i32); + let mut content_type = None; + + // Apply via a multimap so REPEATED header names survive — `Set-Cookie` is + // the common one, and a page that sets two cookies in a single response must + // keep both. `set_header_by_name(.., overwrite=0)` per value silently drops + // the second (it only sets when the name is absent), so build the whole map + // and set it once. `http::HeaderMap`'s iterator yields (name, value) for + // every value, so duplicates come through naturally. + let mut map = CefStringMultimap::new(); + for (name, value) in head.headers() { + let Ok(value) = value.to_str() else { + continue; + }; + map.append(name.as_str(), value); + if name == CONTENT_TYPE { + content_type.replace(value.to_string()); + } } - } - cef_response.set_header_map(Some(&mut map)); + cef_response.set_header_map(Some(&mut map)); - cef_response.set_header_by_name(Some(&"Cache-Control".into()), Some(&"no-store".into()), 1); + cef_response.set_header_by_name(Some(&"Cache-Control".into()), Some(&"no-store".into()), 1); - let mime_type = content_type - .as_ref() - .and_then(|t| t.split(';').next()) - .map(str::trim) - .unwrap_or("text/plain"); - cef_response.set_mime_type(Some(&mime_type.into())); + let mime_type = content_type + .as_ref() + .and_then(|t| t.split(';').next()) + .map(str::trim) + .unwrap_or("text/plain"); + cef_response.set_mime_type(Some(&mime_type.into())); - if let Some(length) = response_length { - *length = -1; - } + if let Some(length) = response_length { + *length = -1; + } - if let Some(redirect_url) = redirect_url { - let _ = std::mem::take(redirect_url); - } + if let Some(redirect_url) = redirect_url { + let _ = std::mem::take(redirect_url); + } } wrap_resource_handler! { @@ -575,52 +575,52 @@ wrap_scheme_handler_factory! { pub(crate) struct ThreadSafe(pub(crate) T); impl ThreadSafe { - pub(crate) fn into_owned(self) -> T { - self.0 - } + pub(crate) fn into_owned(self) -> T { + self.0 + } } unsafe impl Send for ThreadSafe {} unsafe impl Sync for ThreadSafe {} fn read_request_body(request: &mut Request) -> Vec { - let mut body = Vec::new(); - - if let Some(post_data) = request.post_data() { - let mut elements = vec![None; post_data.element_count()]; - post_data.elements(Some(&mut elements)); - for element in elements.into_iter().flatten() { - match element.get_type().as_ref() { - sys::cef_postdataelement_type_t::PDE_TYPE_BYTES => { - let size = element.bytes_count(); - if size > 0 { - let mut buf = vec![0u8; size]; - // Copy bytes into our buffer - let copied = element.bytes(size, buf.as_mut_ptr()); - // Safety: CEF promises it wrote `copied` bytes into buf - unsafe { - buf.set_len(copied); + let mut body = Vec::new(); + + if let Some(post_data) = request.post_data() { + let mut elements = vec![None; post_data.element_count()]; + post_data.elements(Some(&mut elements)); + for element in elements.into_iter().flatten() { + match element.get_type().as_ref() { + sys::cef_postdataelement_type_t::PDE_TYPE_BYTES => { + let size = element.bytes_count(); + if size > 0 { + let mut buf = vec![0u8; size]; + // Copy bytes into our buffer + let copied = element.bytes(size, buf.as_mut_ptr()); + // Safety: CEF promises it wrote `copied` bytes into buf + unsafe { + buf.set_len(copied); + } + body.extend(buf); + } + } + sys::cef_postdataelement_type_t::PDE_TYPE_FILE => { + // Read file from disk + let file_path = CefString::from(&element.file()).to_string(); + if let Ok(mut file) = std::fs::File::open(&file_path) { + use std::io::Read; + let mut buf = Vec::new(); + if file.read_to_end(&mut buf).is_ok() { + body.extend(buf); + } + } + } + _ => {} } - body.extend(buf); - } - } - sys::cef_postdataelement_type_t::PDE_TYPE_FILE => { - // Read file from disk - let file_path = CefString::from(&element.file()).to_string(); - if let Ok(mut file) = std::fs::File::open(&file_path) { - use std::io::Read; - let mut buf = Vec::new(); - if file.read_to_end(&mut buf).is_ok() { - body.extend(buf); - } - } } - _ => {} - } } - } - body + body } /// The tuple origin of `url`, as **Chromium** serializes it. @@ -637,58 +637,58 @@ fn read_request_body(request: &mut Request) -> Vec { /// always `None` there, silently disabling both the `Origin: null` repair in /// `process_request` and any same-origin check a handler builds on it. fn tuple_origin(url: &Url) -> Option { - let spec_origin = url.origin().ascii_serialization(); - if spec_origin != "null" { - return Some(spec_origin); - } - let host = url.host_str()?; - Some(match url.port() { - Some(port) => format!("{}://{}:{}", url.scheme(), host, port), - None => format!("{}://{}", url.scheme(), host), - }) + let spec_origin = url.origin().ascii_serialization(); + if spec_origin != "null" { + return Some(spec_origin); + } + let host = url.host_str()?; + Some(match url.port() { + Some(port) => format!("{}://{}:{}", url.scheme(), host, port), + None => format!("{}://{}", url.scheme(), host), + }) } #[cfg(test)] mod tests { - use super::tuple_origin; - use url::Url; - - #[test] - fn tuple_origin_covers_special_and_custom_schemes() { - let special = Url::parse("https://example.com:8443/x?y").unwrap(); - assert_eq!( - tuple_origin(&special).as_deref(), - Some("https://example.com:8443") - ); - // A standard-registered custom scheme: the URL spec calls this opaque, but - // Chromium gives it a tuple origin, so we must too. - let custom = Url::parse("duck://site.alice.duck/index.html").unwrap(); - assert_eq!( - tuple_origin(&custom).as_deref(), - Some("duck://site.alice.duck") - ); - // Genuinely origin-less: nothing to compare against, so no origin. - let opaque = Url::parse("data:text/html,hi").unwrap(); - assert_eq!(tuple_origin(&opaque), None); - } + use super::tuple_origin; + use url::Url; + + #[test] + fn tuple_origin_covers_special_and_custom_schemes() { + let special = Url::parse("https://example.com:8443/x?y").unwrap(); + assert_eq!( + tuple_origin(&special).as_deref(), + Some("https://example.com:8443") + ); + // A standard-registered custom scheme: the URL spec calls this opaque, but + // Chromium gives it a tuple origin, so we must too. + let custom = Url::parse("duck://site.alice.duck/index.html").unwrap(); + assert_eq!( + tuple_origin(&custom).as_deref(), + Some("duck://site.alice.duck") + ); + // Genuinely origin-less: nothing to compare against, so no origin. + let opaque = Url::parse("data:text/html,hi").unwrap(); + assert_eq!(tuple_origin(&opaque), None); + } } fn get_request_headers(request: &mut Request) -> HeaderMap { - let mut headers = HeaderMap::new(); + let mut headers = HeaderMap::new(); - let mut map = CefStringMultimap::new(); + let mut map = CefStringMultimap::new(); - request.header_map(Some(&mut map)); + request.header_map(Some(&mut map)); - // Iterate through all entries - for (name, value) in map { - for v in value { - headers.append( - HeaderName::from_bytes(name.as_bytes()).unwrap(), - HeaderValue::from_str(&v).unwrap(), - ); + // Iterate through all entries + for (name, value) in map { + for v in value { + headers.append( + HeaderName::from_bytes(name.as_bytes()).unwrap(), + HeaderValue::from_str(&v).unwrap(), + ); + } } - } - headers + headers } diff --git a/src/compat.rs b/src/compat.rs index aafb5c4..f54871d 100644 --- a/src/compat.rs +++ b/src/compat.rs @@ -10,24 +10,21 @@ use std::borrow::Cow; +use tauri_runtime::Icon; #[cfg(target_os = "macos")] use tauri_runtime::dpi::LogicalRect; #[cfg(not(target_os = "macos"))] use tauri_runtime::dpi::PhysicalRect; use tauri_runtime::dpi::{Pixel, Rect}; use tauri_runtime::webview::{DownloadEvent, PageLoadEvent}; -use tauri_runtime::Icon; use url::Url; // Published tauri-runtime keeps these handler aliases private; the types // themselves (boxed fields on PendingWebview) are identical. -pub(crate) type UriSchemeProtocolHandler = dyn Fn( - &str, - http::Request>, - Box>) + Send>, - ) + Send - + Sync - + 'static; +pub(crate) type UriSchemeProtocolHandler = dyn Fn(&str, http::Request>, Box>) + Send>) + + Send + + Sync + + 'static; pub(crate) type NavigationHandler = dyn Fn(&Url) -> bool + Send; @@ -41,11 +38,8 @@ pub(crate) type DownloadHandler = dyn Fn(DownloadEvent) -> bool + Send + Sync; // never invoked from CEF: constructing the published `NewWindowFeatures` // requires a platform webview handle (`webkit2gtk::WebView` on Linux) that a // CEF browser cannot provide. See `life_span.rs` for how presence is used. -pub(crate) type NewWindowHandler = dyn Fn( - Url, - tauri_runtime::webview::NewWindowFeatures, - ) -> tauri_runtime::webview::NewWindowResponse - + Send; +pub(crate) type NewWindowHandler = dyn Fn(Url, tauri_runtime::webview::NewWindowFeatures) -> tauri_runtime::webview::NewWindowResponse + + Send; // feat/cef-only: published PendingWebview has no address-changed channel, so // this stays crate-internal (currently never populated) until upstream ships. @@ -53,32 +47,26 @@ pub(crate) type AddressChangedHandler = dyn Fn(&Url) + Send + Sync + 'static; // feat/cef adds these conversions on tauri_runtime::dpi::Rect directly. #[cfg(not(target_os = "macos"))] -pub(crate) fn rect_to_physical( - rect: Rect, - scale: f64, -) -> PhysicalRect { - PhysicalRect { - position: rect.position.to_physical(scale), - size: rect.size.to_physical(scale), - } +pub(crate) fn rect_to_physical(rect: Rect, scale: f64) -> PhysicalRect { + PhysicalRect { + position: rect.position.to_physical(scale), + size: rect.size.to_physical(scale), + } } #[cfg(target_os = "macos")] -pub(crate) fn rect_to_logical( - rect: Rect, - scale: f64, -) -> LogicalRect { - LogicalRect { - position: rect.position.to_logical(scale), - size: rect.size.to_logical(scale), - } +pub(crate) fn rect_to_logical(rect: Rect, scale: f64) -> LogicalRect { + LogicalRect { + position: rect.position.to_logical(scale), + size: rect.size.to_logical(scale), + } } // feat/cef adds Icon::into_owned. pub(crate) fn icon_into_owned(icon: Icon<'_>) -> Icon<'static> { - Icon { - rgba: Cow::Owned(icon.rgba.into_owned()), - width: icon.width, - height: icon.height, - } + Icon { + rgba: Cow::Owned(icon.rgba.into_owned()), + width: icon.width, + height: icon.height, + } } diff --git a/src/config.rs b/src/config.rs index 9d2d56d..94aee13 100644 --- a/src/config.rs +++ b/src/config.rs @@ -27,41 +27,41 @@ use std::sync::OnceLock; /// process) and at custom-scheme registration (all processes). #[derive(Debug, Clone)] pub struct CefConfig { - /// Application identifier; the CEF disk cache defaults to - /// `{user cache dir}/{identifier}/cef`. Falls back to the executable name. - pub identifier: String, - /// Extra Chromium command line switches, `(name, optional value)`. - pub command_line_args: Vec<(String, Option)>, - /// Overrides the CEF disk cache directory (`Settings::cache_path`). - pub cache_path: Option, - /// Deep link schemes to register as protocol handlers. - pub deep_link_schemes: Vec, - /// Tauri custom protocol schemes to register with Chromium as standard, - /// secure, CORS- and fetch-enabled schemes so published tauri's native URL - /// forms (`tauri://localhost`, `fetch("ipc://localhost/…")`) work under - /// CEF. Must cover every scheme the app or its plugins register. - pub custom_schemes: Vec, - /// Custom schemes whose origins get a cookie jar - /// (`cef::Settings::cookieable_schemes_list`). CEF cookies only http/https - /// by default, so `document.cookie` and `Set-Cookie` are inert on custom - /// schemes not listed here. The http/https defaults are preserved. - pub cookieable_schemes: Vec, + /// Application identifier; the CEF disk cache defaults to + /// `{user cache dir}/{identifier}/cef`. Falls back to the executable name. + pub identifier: String, + /// Extra Chromium command line switches, `(name, optional value)`. + pub command_line_args: Vec<(String, Option)>, + /// Overrides the CEF disk cache directory (`Settings::cache_path`). + pub cache_path: Option, + /// Deep link schemes to register as protocol handlers. + pub deep_link_schemes: Vec, + /// Tauri custom protocol schemes to register with Chromium as standard, + /// secure, CORS- and fetch-enabled schemes so published tauri's native URL + /// forms (`tauri://localhost`, `fetch("ipc://localhost/…")`) work under + /// CEF. Must cover every scheme the app or its plugins register. + pub custom_schemes: Vec, + /// Custom schemes whose origins get a cookie jar + /// (`cef::Settings::cookieable_schemes_list`). CEF cookies only http/https + /// by default, so `document.cookie` and `Set-Cookie` are inert on custom + /// schemes not listed here. The http/https defaults are preserved. + pub cookieable_schemes: Vec, } impl Default for CefConfig { - fn default() -> Self { - Self { - identifier: std::env::current_exe() - .ok() - .and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned())) - .unwrap_or_else(|| "tauri-app".into()), - command_line_args: Vec::new(), - cache_path: None, - deep_link_schemes: Vec::new(), - custom_schemes: vec!["tauri".into(), "ipc".into(), "asset".into()], - cookieable_schemes: Vec::new(), + fn default() -> Self { + Self { + identifier: std::env::current_exe() + .ok() + .and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned())) + .unwrap_or_else(|| "tauri-app".into()), + command_line_args: Vec::new(), + cache_path: None, + deep_link_schemes: Vec::new(), + custom_schemes: vec!["tauri".into(), "ipc".into(), "asset".into()], + cookieable_schemes: Vec::new(), + } } - } } static CONFIG: OnceLock = OnceLock::new(); @@ -70,9 +70,9 @@ static CONFIG: OnceLock = OnceLock::new(); /// `main`, before [`crate::run_cef_helper_process`] and before building the /// tauri app. Later calls are ignored. pub fn configure(config: CefConfig) { - let _ = CONFIG.set(config); + let _ = CONFIG.set(config); } pub(crate) fn config() -> &'static CefConfig { - CONFIG.get_or_init(CefConfig::default) + CONFIG.get_or_init(CefConfig::default) } diff --git a/src/external_message_pump/linux.rs b/src/external_message_pump/linux.rs index dd4e2df..09d57fa 100644 --- a/src/external_message_pump/linux.rs +++ b/src/external_message_pump/linux.rs @@ -4,93 +4,295 @@ //! Linux/BSD backend for the CEF external message pump. //! -//! cefclient owns the GLib loop and drives a GLib timeout from it -//! (`main_message_loop_external_pump_linux.cc`). Here winit owns an X11 loop -//! instead, so the timeout is attached to the default GLib main context and the -//! winit loop services that context every iteration (see the runtime's -//! `service_glib`), waking at [`PlatformPump::deadline`] via -//! `ControlFlow::WaitUntil`. The timeout still fires inside nested GLib loops -//! (e.g. GTK menus/dialogs) that winit cannot observe, keeping CEF pumping -//! there — the same property the Windows/macOS backends get from `WM_TIMER` / -//! `NSTimer`. +//! Upstream cefclient: +//! //! -//! Reference: -//! +//! Mirrors cefclient's `main_message_loop_external_pump_linux.cc`: a custom +//! low-priority `GSource` is attached to the default GLib context, delayed work +//! is tracked as an absolute deadline, and cross-thread scheduling wakes the +//! GLib poll through a pipe. -use std::sync::Weak; -use std::time::{Duration, Instant}; +use std::{ + mem, + os::raw::{c_int, c_uint}, + sync::Weak, + time::{Duration, Instant}, +}; -use gtk::glib; -use winit::event_loop::EventLoopProxy; +use gtk::glib::{self, ffi, translate::ToGlibPtr}; use super::PumpState; +#[repr(C)] +struct WorkSource { + source: ffi::GSource, + source_state: *mut SourceState, +} + +struct SourceState { + state: Weak, + + // The time when we need to do delayed work. + delayed_work_time: Option, + + // We use a wakeup pipe to make sure we'll get out of the glib polling phase + // when another thread has scheduled us to do some work. There is a glib + // mechanism g_main_context_wakeup, but this won't guarantee that our event's + // Dispatch() will be called. + wakeup_pipe_read: c_int, + wakeup_pipe_write: c_int, + + // Boxed to keep the GPollFD address stable while it is registered with the + // work source. + wakeup_gpollfd: Box, +} + pub(super) struct PlatformPump { - state: Weak, - proxy: EventLoopProxy, - timer: Option, - deadline: Option, + // The work source. It is destroyed when the message pump is destroyed. + work_source: *mut ffi::GSource, + source_state: Box, } +// SAFETY: `on_schedule_message_pump_work` is the only method called from other +// threads, and it only writes to the wakeup pipe. GLib source/timer state is +// mutated on the owner thread when GLib dispatches the attached source. +unsafe impl Send for PlatformPump {} + impl PlatformPump { - pub(super) fn new(state: Weak, proxy: EventLoopProxy) -> Self { - Self { - state, - proxy, - timer: None, - deadline: None, + pub(super) fn new(state: Weak) -> Self { + // The runtime services callbacks from GLib's default MainContext. + let context = glib::MainContext::default(); + + // Create our wakeup pipe, which is used to flag when work was scheduled. + let mut fds = [0; 2]; + let ret = unsafe { libc::pipe(fds.as_mut_ptr()) }; + assert_eq!(ret, 0, "failed to create CEF message pump wakeup pipe"); + + let mut source_state = Box::new(SourceState { + state, + delayed_work_time: None, + wakeup_pipe_read: fds[0], + wakeup_pipe_write: fds[1], + wakeup_gpollfd: Box::new(ffi::GPollFD { + fd: fds[0], + events: ffi::G_IO_IN as _, + revents: 0, + }), + }); + + let work_source = unsafe { + let source = ffi::g_source_new( + &raw mut WORK_SOURCE_FUNCS, + mem::size_of::() as c_uint, + ); + assert!( + !source.is_null(), + "failed to create CEF message pump GSource" + ); + + (*(source as *mut WorkSource)).source_state = &mut *source_state; + ffi::g_source_add_poll(source, &mut *source_state.wakeup_gpollfd); + // Use a low priority so that we let other events in the queue go first. + ffi::g_source_set_priority(source, ffi::G_PRIORITY_DEFAULT_IDLE); + // This is needed to allow Run calls inside Dispatch. + ffi::g_source_set_can_recurse(source, ffi::GTRUE); + ffi::g_source_attach(source, context.to_glib_none().0); + source + }; + + Self { + work_source, + source_state, + } + } + + pub(super) fn on_schedule_message_pump_work(&mut self, delay_ms: i64) { + // This can be called on any thread, so we don't want to touch any state + // variables as we would then need locks all over. This ensures that if we + // are sleeping in a poll that we will wake up. + let written = retry_eintr(|| unsafe { + libc::write( + self.source_state.wakeup_pipe_write, + (&delay_ms as *const i64).cast(), + mem::size_of::(), + ) + }); + if written != mem::size_of::() as isize { + log::error!("could not write to the CEF message pump wakeup pipe"); + } + } + + pub(super) fn set_timer(&mut self, delay_ms: i64) { + debug_assert!(delay_ms > 0); + + let now = Instant::now(); + self.source_state.delayed_work_time = Some(now + Duration::from_millis(delay_ms as u64)); + } + + pub(super) fn kill_timer(&mut self) { + self.source_state.delayed_work_time = None; + } + + pub(super) fn is_timer_pending(&self) -> bool { + get_time_interval_milliseconds(self.source_state.delayed_work_time) > 0 } - } - - pub(super) fn post_schedule_work(&mut self, delay_ms: i64) { - // May be called on any thread. Marshal the request onto the default GLib - // main context, which the winit loop services on the main thread; wake winit - // so it does so promptly. - let state = self.state.clone(); - glib::idle_add_once(move || { - if let Some(state) = state.upgrade() { - state.on_schedule_work(delay_ms); - } - }); - self.proxy.wake_up(); - } - - pub(super) fn set_timer(&mut self, delay_ms: i64) { - debug_assert!(self.timer.is_none()); - debug_assert!(delay_ms > 0); - - let delay = Duration::from_millis(delay_ms as u64); - let state = self.state.clone(); - let source = glib::timeout_add_once(delay, move || { - let Some(state) = state.upgrade() else { - return; - }; - // This one-shot source removes itself after firing, so forget it before - // `on_timer_timeout` runs `kill_timer` — `SourceId::remove` would panic on - // an already-removed source. - if let Ok(mut platform) = state.platform.lock() { - platform.timer = None; - platform.deadline = None; - } - state.on_timer_timeout(); - }); - - self.timer = Some(source); - self.deadline = Some(Instant::now() + delay); - } - - pub(super) fn kill_timer(&mut self) { - if let Some(source) = self.timer.take() { - source.remove(); + + pub(super) fn deadline(&self) -> Option { + self.source_state.delayed_work_time } - self.deadline = None; - } +} + +impl Drop for PlatformPump { + fn drop(&mut self) { + unsafe { + ffi::g_source_destroy(self.work_source); + ffi::g_source_unref(self.work_source); + libc::close(self.source_state.wakeup_pipe_read); + libc::close(self.source_state.wakeup_pipe_write); + } + } +} + +// Return a timeout suitable for the glib loop, -1 to block forever, +// 0 to return right away, or a timeout in milliseconds from now. +fn get_time_interval_milliseconds(from: Option) -> c_int { + let Some(from) = from else { + return -1; + }; - pub(super) fn is_timer_pending(&self) -> bool { - self.timer.is_some() - } + // Be careful here. Instant has finer precision than milliseconds, but GLib + // wants a value in milliseconds. If there are 5.5ms left, should the delay be + // 5 or 6? It should be 6 to avoid executing delayed work too early. + let now = Instant::now(); + let delay = from + .checked_duration_since(now) + .map(|duration| (duration.as_secs_f64() * 1000.0).ceil() as c_int) + .unwrap_or(-1); - pub(super) fn deadline(&self) -> Option { - self.deadline - } + // If this value is negative, then we need to run delayed work soon. + if delay < 0 { 0 } else { delay } } + +// From base/posix/eintr_wrapper.h. +// This provides a wrapper around system calls which may be interrupted by a +// signal and return EINTR. See man 7 signal. +fn retry_eintr(mut f: F) -> isize +where + F: FnMut() -> isize, +{ + loop { + let result = f(); + if result != -1 || std::io::Error::last_os_error().raw_os_error() != Some(libc::EINTR) { + return result; + } + } +} + +unsafe fn source_state(source: *mut ffi::GSource) -> *mut SourceState { + unsafe { (*(source as *mut WorkSource)).source_state } +} + +// Return the timeout we want passed to poll. +unsafe fn handle_prepare(source_state: *mut SourceState) -> c_int { + // We don't think we have work to do, but make sure not to block longer than + // the next time we need to run delayed work. + let delayed_work_time = unsafe { (*source_state).delayed_work_time }; + get_time_interval_milliseconds(delayed_work_time) +} + +unsafe fn handle_check(source_state: *mut SourceState) -> bool { + // We usually have a single message on the wakeup pipe, since we are only + // signaled when the queue went from empty to non-empty, but there can be + // two messages if a task posted a task, hence we read at most two bytes. + // The glib poll will tell us whether there was data, so this read shouldn't + // block. + let have_wakeup = { + let wakeup_gpollfd = unsafe { &*(*source_state).wakeup_gpollfd }; + (wakeup_gpollfd.revents & ffi::G_IO_IN as u16) != 0 + }; + if have_wakeup { + let mut delay_ms = [0_i64; 2]; + let num_bytes = retry_eintr(|| unsafe { + libc::read( + (*source_state).wakeup_pipe_read, + delay_ms.as_mut_ptr().cast(), + mem::size_of::() * 2, + ) + }); + + if num_bytes < mem::size_of::() as isize { + log::error!("error reading from the CEF message pump wakeup pipe"); + } + if num_bytes == mem::size_of::() as isize { + if let Some(state) = unsafe { (*source_state).state.upgrade() } { + state.on_schedule_work(delay_ms[0]); + } + } + if num_bytes == (mem::size_of::() * 2) as isize { + if let Some(state) = unsafe { (*source_state).state.upgrade() } { + state.on_schedule_work(delay_ms[1]); + } + } + } + + let delayed_work_time = unsafe { (*source_state).delayed_work_time }; + if get_time_interval_milliseconds(delayed_work_time) == 0 { + // The timer has expired. That condition will stay true until we process + // that delayed work, so we don't need to record this differently. + return true; + } + + false +} + +unsafe fn handle_dispatch(source_state: *mut SourceState) { + if let Some(state) = unsafe { (*source_state).state.upgrade() } { + state.on_timer_timeout(); + } +} + +unsafe extern "C" fn work_source_prepare( + source: *mut ffi::GSource, + timeout_ms: *mut c_int, +) -> ffi::gboolean { + if !timeout_ms.is_null() { + let source_state = unsafe { source_state(source) }; + unsafe { *timeout_ms = handle_prepare(source_state) }; + } + + // We always return FALSE, so that our timeout is honored. If we were + // to return TRUE, the timeout would be considered to be 0 and the poll + // would never block. Once the poll is finished, Check will be called. + ffi::GFALSE +} + +unsafe extern "C" fn work_source_check(source: *mut ffi::GSource) -> ffi::gboolean { + let source_state = unsafe { source_state(source) }; + // Only return TRUE if Dispatch should be called. + if unsafe { handle_check(source_state) } { + ffi::GTRUE + } else { + ffi::GFALSE + } +} + +unsafe extern "C" fn work_source_dispatch( + source: *mut ffi::GSource, + _callback: ffi::GSourceFunc, + _user_data: ffi::gpointer, +) -> ffi::gboolean { + let source_state = unsafe { source_state(source) }; + unsafe { handle_dispatch(source_state) }; + // Always return TRUE so our source stays registered. + ffi::GTRUE +} + +// I wish these could be const, but g_source_new wants non-const. +static mut WORK_SOURCE_FUNCS: ffi::GSourceFuncs = ffi::GSourceFuncs { + prepare: Some(work_source_prepare), + check: Some(work_source_check), + dispatch: Some(work_source_dispatch), + finalize: None, + closure_callback: None, + closure_marshal: None, +}; diff --git a/src/external_message_pump/macos.rs b/src/external_message_pump/macos.rs index fdc7376..7c168d1 100644 --- a/src/external_message_pump/macos.rs +++ b/src/external_message_pump/macos.rs @@ -4,27 +4,27 @@ //! macOS backend for the CEF external message pump. //! +//! Upstream cefclient: +//! +//! //! Mirrors cefclient's `main_message_loop_external_pump_mac.mm`: scheduling //! requests are posted back onto the owning AppKit thread with //! `performSelector:onThread:`, and delayed work is driven by an `NSTimer` //! installed in the common and event-tracking run-loop modes so it keeps firing //! while AppKit spins a nested menu/tracking loop (e.g. a webview context menu) //! that winit's callbacks never observe. -//! -//! Reference: -//! - use std::sync::Weak; use objc2::{AnyThread, DefinedClass, define_class, msg_send, rc::Retained, sel}; use objc2_app_kit::NSEventTrackingRunLoopMode; use objc2_foundation::{ - NSNumber, NSObject, NSObjectNSThreadPerformAdditions, NSObjectProtocol, NSRunLoop, - NSRunLoopCommonModes, NSThread, NSTimer, + NSNumber, NSObject, NSObjectNSThreadPerformAdditions, NSObjectProtocol, NSRunLoop, + NSRunLoopCommonModes, NSThread, NSTimer, }; use super::PumpState; +// Object that handles event callbacks on the owner thread. define_class! { #[unsafe(super(NSObject))] #[ivars = Weak] @@ -32,7 +32,7 @@ define_class! { impl EventHandler { #[unsafe(method(scheduleWork:))] - fn schedule_work(&self, delay_ms: &NSNumber) { + fn handle_schedule_work(&self, delay_ms: &NSNumber) { let Some(state) = self.ivars().upgrade() else { return; }; @@ -40,7 +40,7 @@ define_class! { } #[unsafe(method(timerTimeout:))] - fn timer_timeout(&self, _: &NSTimer) { + fn handle_timer_timeout(&self, _: &NSTimer) { let Some(state) = self.ivars().upgrade() else { return; }; @@ -52,76 +52,82 @@ define_class! { } impl EventHandler { - fn new(state: Weak) -> Retained { - let this = Self::alloc().set_ivars(state); - unsafe { msg_send![super(this), init] } - } + fn new(state: Weak) -> Retained { + let this = Self::alloc().set_ivars(state); + unsafe { msg_send![super(this), init] } + } } pub(super) struct PlatformPump { - owner_thread: Retained, - event_handler: Retained, - timer: Option>, + // Owner thread that will run events. + owner_thread: Retained, + + // Used to handle event callbacks on the owner thread. + event_handler: Retained, + + // Pending work timer. + timer: Option>, } // SAFETY: the owner thread and timer are only touched on the AppKit thread that -// constructed the pump; `post_schedule_work` marshals back to it before use. +// constructed the pump; `on_schedule_message_pump_work` marshals back to it +// before use. unsafe impl Send for PlatformPump {} impl PlatformPump { - pub(super) fn new(state: Weak) -> Self { - Self { - owner_thread: NSThread::currentThread(), - event_handler: EventHandler::new(state), - timer: None, + pub(super) fn new(state: Weak) -> Self { + Self { + owner_thread: NSThread::currentThread(), + event_handler: EventHandler::new(state), + timer: None, + } } - } - pub(super) fn post_schedule_work(&mut self, delay_ms: i64) { - let delay_ms = isize::try_from(delay_ms).unwrap_or(isize::MAX); - let delay_ms = NSNumber::new_isize(delay_ms); - unsafe { - self - .event_handler - .performSelector_onThread_withObject_waitUntilDone( - sel!(scheduleWork:), - &self.owner_thread, - Some(&delay_ms), - false, - ); + pub(super) fn on_schedule_message_pump_work(&mut self, delay_ms: i64) { + // This method may be called on any thread. + let delay_ms = NSNumber::new_i32(delay_ms as i32); + unsafe { + self.event_handler + .performSelector_onThread_withObject_waitUntilDone( + sel!(scheduleWork:), + &self.owner_thread, + Some(&delay_ms), + false, + ); + } } - } - pub(super) fn set_timer(&mut self, delay_ms: i64) { - debug_assert!(delay_ms > 0); - debug_assert!(self.timer.is_none()); - - let timer = unsafe { - NSTimer::timerWithTimeInterval_target_selector_userInfo_repeats( - delay_ms as f64 / 1000.0, - &self.event_handler, - sel!(timerTimeout:), - None, - false, - ) - }; - - let run_loop = NSRunLoop::currentRunLoop(); - unsafe { - run_loop.addTimer_forMode(&timer, NSRunLoopCommonModes); - run_loop.addTimer_forMode(&timer, NSEventTrackingRunLoopMode); + pub(super) fn set_timer(&mut self, delay_ms: i64) { + debug_assert!(delay_ms > 0); + debug_assert!(self.timer.is_none()); + + let timer = unsafe { + NSTimer::timerWithTimeInterval_target_selector_userInfo_repeats( + delay_ms as f64 / 1000.0, + &self.event_handler, + sel!(timerTimeout:), + None, + false, + ) + }; + + // Add the timer to default and tracking runloop modes. + let run_loop = NSRunLoop::currentRunLoop(); + unsafe { + run_loop.addTimer_forMode(&timer, NSRunLoopCommonModes); + run_loop.addTimer_forMode(&timer, NSEventTrackingRunLoopMode); + } + + self.timer = Some(timer); } - self.timer = Some(timer); - } - - pub(super) fn kill_timer(&mut self) { - if let Some(timer) = self.timer.take() { - timer.invalidate(); + pub(super) fn kill_timer(&mut self) { + if let Some(timer) = self.timer.take() { + timer.invalidate(); + } } - } - pub(super) fn is_timer_pending(&self) -> bool { - self.timer.is_some() - } + pub(super) fn is_timer_pending(&self) -> bool { + self.timer.is_some() + } } diff --git a/src/external_message_pump/mod.rs b/src/external_message_pump/mod.rs index 5140d2e..870ca40 100644 --- a/src/external_message_pump/mod.rs +++ b/src/external_message_pump/mod.rs @@ -9,6 +9,10 @@ //! [`cef::do_message_loop_work`] by invoking `OnScheduleMessagePumpWork(delay)` //! whenever it has work pending. //! +//! Upstream cefclient: +//! - +//! - +//! //! This is a port of upstream cefclient's external pump — same semantics and //! logic, adapted to Rust. The platform-independent scheduling/reentrancy logic //! lives here; each platform supplies a [`PlatformPump`] backend that drives a @@ -16,29 +20,25 @@ //! //! - Windows: a `WM_TIMER` on a message-only window. //! - macOS: an `NSTimer` in the common and event-tracking run-loop modes. -//! - Linux/BSD: a GLib timeout serviced by the winit loop. +//! - Linux/BSD: a custom low-priority GLib source with a wakeup pipe. //! //! On Windows and macOS the timer lives on the same native loop winit already //! runs, so CEF keeps painting and processing IPC even while the OS spins a //! nested modal loop winit cannot observe (window move/resize on Windows, menu -//! and event tracking on macOS). On Linux/BSD the GLib timeout still fires +//! and event tracking on macOS). On Linux/BSD the GLib source still dispatches //! inside nested GLib loops (e.g. GTK menus/dialogs) for the same reason. -//! -//! Reference implementation (cefclient base class): -//! - -//! - use std::sync::{ - Arc, Mutex, - atomic::{AtomicBool, Ordering}, + Arc, Mutex, + atomic::{AtomicBool, Ordering}, }; #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" ))] mod linux; #[cfg(target_os = "macos")] @@ -47,11 +47,11 @@ mod macos; mod windows; #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" ))] use linux::PlatformPump; #[cfg(target_os = "macos")] @@ -59,173 +59,153 @@ use macos::PlatformPump; #[cfg(windows)] use windows::PlatformPump; -#[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" -))] -use winit::event_loop::EventLoopProxy; +// Special timer delay placeholder value. Intentionally 32-bit for Windows and +// OS X platform API compatibility. +const K_TIMER_DELAY_PLACEHOLDER: i64 = i32::MAX as i64; -/// Sentinel delay used to (re)arm the fallback "max delay" tick. Kept within 32 -/// bits for Win32/AppKit timer API compatibility, matching cefclient's -/// `kTimerDelayPlaceholder`. -const TIMER_DELAY_PLACEHOLDER: i64 = i32::MAX as i64; - -/// Upper bound on how long we wait between [`cef::do_message_loop_work`] calls -/// (~30fps), matching cefclient's `kMaxTimerDelay`. -const MAX_TIMER_DELAY_MS: i64 = 1000 / 30; +// The maximum number of milliseconds we're willing to wait between calls to +// DoWork(). +const K_MAX_TIMER_DELAY: i64 = 1000 / 30; // 30fps /// Handle to the external message pump. Cloning shares the same underlying /// state; the backing platform resources are released when the last clone drops. #[derive(Clone)] pub(crate) struct CefExternalPump { - state: Arc, + state: Arc, } impl CefExternalPump { - pub(crate) fn new( + pub(crate) fn new() -> Self { + let state = Arc::new_cyclic(|weak| PumpState { + is_active: AtomicBool::new(false), + reentrancy_detected: AtomicBool::new(false), + platform: Mutex::new(PlatformPump::new(weak.clone())), + }); + + Self { state } + } + + /// Called from CEF's `OnScheduleMessagePumpWork`. May run on any thread. + pub(crate) fn on_schedule_message_pump_work(&self, delay_ms: i64) { + self.state.on_schedule_message_pump_work(delay_ms); + } + + /// Explicit tick, used to drive CEF before winit's loop is running (startup) + /// and after winit processes a batch of events. Must run on the owner thread. + pub(crate) fn do_work(&self) { + self.state.do_work(); + } + + /// When the platform timer is next due. This is only needed by event loops + /// that do not block in the GLib main context themselves. #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" ))] - proxy: EventLoopProxy, - ) -> Self { - let state = Arc::new_cyclic(|weak| PumpState { - is_active: AtomicBool::new(false), - reentrancy_detected: AtomicBool::new(false), - platform: Mutex::new(PlatformPump::new( - weak.clone(), - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - proxy, - )), - }); - - Self { state } - } - - /// Called from CEF's `OnScheduleMessagePumpWork`. May run on any thread. - pub(crate) fn schedule_message_pump_work(&self, delay_ms: i64) { - self.state.schedule_message_pump_work(delay_ms); - } - - /// Explicit tick, used to drive CEF before winit's loop is running (startup) - /// and after winit processes a batch of events. Must run on the owner thread. - pub(crate) fn do_message_loop_work(&self) { - self.state.do_work(); - } - - /// When the GLib timer is next due, so the winit loop can wake to service - /// GLib (see [`linux`]). - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - pub(crate) fn next_deadline(&self) -> Option { - self.state.platform.lock().ok().and_then(|p| p.deadline()) - } + pub(crate) fn next_deadline(&self) -> Option { + self.state.platform.lock().ok().and_then(|p| p.deadline()) + } } /// Platform-independent pump state, shared with the [`PlatformPump`] backend. struct PumpState { - is_active: AtomicBool, - reentrancy_detected: AtomicBool, - platform: Mutex, + is_active: AtomicBool, + reentrancy_detected: AtomicBool, + platform: Mutex, } impl PumpState { - /// Post a scheduling request onto the owner thread. The platform backend is - /// responsible for delivering it there, where it lands back in - /// [`Self::on_schedule_work`]. Mirrors the platform `OnScheduleMessagePumpWork`. - fn schedule_message_pump_work(&self, delay_ms: i64) { - if let Ok(mut platform) = self.platform.lock() { - platform.post_schedule_work(delay_ms); - } - } - - /// Runs on the owner thread once a scheduling request is delivered. Mirrors - /// cefclient's `OnScheduleWork`. - fn on_schedule_work(&self, delay_ms: i64) { - { - let Ok(mut platform) = self.platform.lock() else { - return; - }; - - // An already-pending timer covers the fallback tick; don't reset it. - if delay_ms == TIMER_DELAY_PLACEHOLDER && platform.is_timer_pending() { - return; - } - - platform.kill_timer(); + /// Post a scheduling request onto the owner thread. The platform backend is + /// responsible for delivering it there, where it lands back in + /// [`Self::on_schedule_work`]. Mirrors the platform `OnScheduleMessagePumpWork`. + fn on_schedule_message_pump_work(&self, delay_ms: i64) { + if let Ok(mut platform) = self.platform.lock() { + platform.on_schedule_message_pump_work(delay_ms); + } } - if delay_ms <= 0 { - self.do_work(); - return; + /// Runs on the owner thread once a scheduling request is delivered. Mirrors + /// cefclient's `OnScheduleWork`. + fn on_schedule_work(&self, mut delay_ms: i64) { + { + let Ok(mut platform) = self.platform.lock() else { + return; + }; + + if delay_ms == K_TIMER_DELAY_PLACEHOLDER && platform.is_timer_pending() { + // Don't set the maximum timer requested from DoWork() if a timer event is + // currently pending. + return; + } + + platform.kill_timer(); + } + + if delay_ms <= 0 { + // Execute the work immediately. + self.do_work(); + } else if let Ok(mut platform) = self.platform.lock() { + // Never wait longer than the maximum allowed time. + if delay_ms > K_MAX_TIMER_DELAY { + delay_ms = K_MAX_TIMER_DELAY; + } + + // Results in call to OnTimerTimeout() after the specified delay. + platform.set_timer(delay_ms); + } } - if let Ok(mut platform) = self.platform.lock() { - platform.set_timer(delay_ms.min(MAX_TIMER_DELAY_MS)); + /// Runs on the owner thread when the platform timer fires. Mirrors cefclient's + /// `OnTimerTimeout`. + fn on_timer_timeout(&self) { + if let Ok(mut platform) = self.platform.lock() { + platform.kill_timer(); + } + self.do_work(); } - } - /// Runs on the owner thread when the platform timer fires. Mirrors cefclient's - /// `OnTimerTimeout`. - fn on_timer_timeout(&self) { - if let Ok(mut platform) = self.platform.lock() { - platform.kill_timer(); - } - self.do_work(); - } - - /// Mirrors cefclient's `DoWork`. - fn do_work(&self) { - let was_reentrant = self.perform_message_loop_work(); - if was_reentrant { - // The work was discarded because we were already inside - // do_message_loop_work; repost so it runs on the next clean turn. - self.schedule_message_pump_work(0); - return; + /// Mirrors cefclient's `DoWork`. + fn do_work(&self) { + let was_reentrant = self.perform_message_loop_work(); + if was_reentrant { + // Execute the remaining work as soon as possible. + self.on_schedule_message_pump_work(0); + } else if !self.is_timer_pending() { + // Schedule a timer event at the maximum allowed time. This may be dropped + // in OnScheduleWork() if another timer event is already in-flight. + self.on_schedule_message_pump_work(K_TIMER_DELAY_PLACEHOLDER); + } } - // Arm the fallback tick so CEF work it didn't explicitly announce (e.g. via - // its own internal timers) still runs within the max delay. - let timer_pending = self - .platform - .lock() - .map(|platform| platform.is_timer_pending()) - .unwrap_or(true); - if !timer_pending { - self.schedule_message_pump_work(TIMER_DELAY_PLACEHOLDER); - } - } - - /// Mirrors cefclient's `PerformMessageLoopWork`. - fn perform_message_loop_work(&self) -> bool { - if self.is_active.swap(true, Ordering::SeqCst) { - // do_message_loop_work can trigger CEF callbacks (paint, IPC) that - // re-enter this method. Record it so the caller reschedules the work. - self.reentrancy_detected.store(true, Ordering::SeqCst); - return false; + fn is_timer_pending(&self) -> bool { + self.platform + .lock() + .map(|platform| platform.is_timer_pending()) + .unwrap_or(true) } - self.reentrancy_detected.store(false, Ordering::SeqCst); - cef::do_message_loop_work(); - self.is_active.store(false, Ordering::SeqCst); - - self.reentrancy_detected.load(Ordering::SeqCst) - } + /// Mirrors cefclient's `PerformMessageLoopWork`. + fn perform_message_loop_work(&self) -> bool { + if self.is_active.load(Ordering::SeqCst) { + // When CefDoMessageLoopWork() is called there may be various callbacks + // (such as paint and IPC messages) that result in additional calls to this + // method. If re-entrancy is detected we must repost a request again to the + // owner thread to ensure that the discarded call is executed in the future. + self.reentrancy_detected.store(true, Ordering::SeqCst); + return false; + } + + self.reentrancy_detected.store(false, Ordering::SeqCst); + + self.is_active.store(true, Ordering::SeqCst); + cef::do_message_loop_work(); + self.is_active.store(false, Ordering::SeqCst); + + // |reentrancy_detected_| may have changed due to re-entrant calls to this + // method. + self.reentrancy_detected.load(Ordering::SeqCst) + } } diff --git a/src/external_message_pump/windows.rs b/src/external_message_pump/windows.rs index 69bf451..c52a8eb 100644 --- a/src/external_message_pump/windows.rs +++ b/src/external_message_pump/windows.rs @@ -4,6 +4,9 @@ //! Windows backend for the CEF external message pump. //! +//! Upstream cefclient: +//! +//! //! Mirrors cefclient's `main_message_loop_external_pump_win.cc`: a message-only //! window owns a `WM_TIMER`, and `OnScheduleMessagePumpWork` posts a private //! `WM_HAVE_WORK` message to it. winit runs the thread's own @@ -11,153 +14,164 @@ //! window procedure — so CEF is pumped from the same loop, including while //! Windows runs a modal move/resize loop that winit's `ApplicationHandler` //! callbacks never observe. -//! -//! Reference: -//! - use std::sync::Weak; use windows::{ - Win32::{ - Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, WPARAM}, - System::LibraryLoader::GetModuleHandleW, - UI::WindowsAndMessaging::{ - CreateWindowExW, DefWindowProcW, DestroyWindow, GWLP_USERDATA, GetWindowLongPtrW, - HWND_MESSAGE, KillTimer, PostMessageW, RegisterClassExW, SetTimer, SetWindowLongPtrW, - WINDOW_EX_STYLE, WM_TIMER, WM_USER, WNDCLASSEXW, WS_OVERLAPPEDWINDOW, + Win32::{ + Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, WPARAM}, + System::LibraryLoader::GetModuleHandleW, + UI::WindowsAndMessaging::{ + CreateWindowExW, DefWindowProcW, DestroyWindow, GWLP_USERDATA, GetWindowLongPtrW, + HWND_MESSAGE, KillTimer, PostMessageW, RegisterClassExW, SetTimer, SetWindowLongPtrW, + WINDOW_EX_STYLE, WM_TIMER, WM_USER, WNDCLASSEXW, WS_OVERLAPPEDWINDOW, + }, }, - }, - core::{PCWSTR, w}, + core::{PCWSTR, w}, }; use super::PumpState; -/// Window class for the pump's message-only window. -const WINDOW_CLASS: PCWSTR = w!("TauriCefExternalMessagePump"); -/// Private message posted by `OnScheduleMessagePumpWork`; matches cefclient's -/// `kMsgHaveWork` (`WM_USER + 1`). The `LPARAM` carries the delay in ms. -const WM_HAVE_WORK: u32 = WM_USER + 1; -/// Timer id used with `SetTimer`/`KillTimer` on the pump window. +const K_CLASS_NAME: PCWSTR = w!("TauriCEFMainTargetHWND"); + +// Message sent to get an additional time slice for pumping (processing) another +// task (a series of such messages creates a continuous task pump). +const K_MSG_HAVE_WORK: u32 = WM_USER + 1; + const TIMER_ID: usize = 1; pub(super) struct PlatformPump { - hwnd: HWND, - timer_pending: bool, + // HWND owned by the thread that CefDoMessageLoopWork should be invoked on. + main_thread_target: HWND, + + // True if a timer event is currently pending. + timer_pending: bool, } -// SAFETY: `hwnd` is created on, and its timer is only armed/disarmed from, the -// main (winit) thread. The sole cross-thread use is `PostMessageW` in -// `post_schedule_work`, which Win32 explicitly permits from any thread. +// SAFETY: `main_thread_target` is created on, and its timer is only +// armed/disarmed from, the main (winit) thread. The sole cross-thread use is +// `PostMessageW` in `on_schedule_message_pump_work`, which Win32 explicitly +// permits from any thread. unsafe impl Send for PlatformPump {} impl PlatformPump { - pub(super) fn new(state: Weak) -> Self { - let hinstance: HINSTANCE = unsafe { GetModuleHandleW(None) } - .map(|module| HINSTANCE(module.0)) - .unwrap_or_default(); - - let class = WNDCLASSEXW { - cbSize: std::mem::size_of::() as u32, - lpfnWndProc: Some(wndproc), - hInstance: hinstance, - lpszClassName: WINDOW_CLASS, - ..Default::default() - }; - // Registering an already-registered class fails harmlessly; this also lets a - // second runtime in the same process reuse the class. - unsafe { RegisterClassExW(&class) }; - - let hwnd = unsafe { - CreateWindowExW( - WINDOW_EX_STYLE::default(), - WINDOW_CLASS, - PCWSTR::null(), - WS_OVERLAPPEDWINDOW, - 0, - 0, - 0, - 0, - Some(HWND_MESSAGE), - None, - Some(hinstance), - None, - ) + pub(super) fn new(state: Weak) -> Self { + let hinstance: HINSTANCE = unsafe { GetModuleHandleW(None) } + .map(|module| HINSTANCE(module.0)) + .unwrap_or_default(); + + let class = WNDCLASSEXW { + cbSize: std::mem::size_of::() as u32, + lpfnWndProc: Some(wnd_proc), + hInstance: hinstance, + lpszClassName: K_CLASS_NAME, + ..Default::default() + }; + unsafe { RegisterClassExW(&class) }; + + // Create the message handling window. + let hwnd = unsafe { + CreateWindowExW( + WINDOW_EX_STYLE::default(), + K_CLASS_NAME, + PCWSTR::null(), + WS_OVERLAPPEDWINDOW, + 0, + 0, + 0, + 0, + Some(HWND_MESSAGE), + None, + Some(hinstance), + None, + ) + } + .expect("failed to create CEF external message pump window"); + + let state = Box::into_raw(Box::new(state)); + unsafe { SetWindowLongPtrW(hwnd, GWLP_USERDATA, state as isize) }; + + Self { + main_thread_target: hwnd, + timer_pending: false, + } } - .expect("failed to create CEF external message pump window"); - // Store a Weak back-reference for the window procedure; freed in `Drop`. No - // timer is armed and no message is posted yet, so the window cannot be - // dispatched anything that reads this slot before it is set. - let state = Box::into_raw(Box::new(state)); - unsafe { SetWindowLongPtrW(hwnd, GWLP_USERDATA, state as isize) }; + pub(super) fn on_schedule_message_pump_work(&mut self, delay_ms: i64) { + // This method may be called on any thread. + let _ = unsafe { + PostMessageW( + Some(self.main_thread_target), + K_MSG_HAVE_WORK, + WPARAM(0), + LPARAM(delay_ms as isize), + ) + }; + } - Self { - hwnd, - timer_pending: false, + pub(super) fn set_timer(&mut self, delay_ms: i64) { + debug_assert!(!self.timer_pending); + debug_assert!(delay_ms > 0); + self.timer_pending = true; + unsafe { + SetTimer( + Some(self.main_thread_target), + TIMER_ID, + delay_ms as u32, + None, + ) + }; } - } - - pub(super) fn post_schedule_work(&mut self, delay_ms: i64) { - // Thread-safe; lands in `wndproc` (WM_HAVE_WORK) on the owner thread. - let _ = unsafe { - PostMessageW( - Some(self.hwnd), - WM_HAVE_WORK, - WPARAM(0), - LPARAM(delay_ms as isize), - ) - }; - } - - pub(super) fn set_timer(&mut self, delay_ms: i64) { - debug_assert!(!self.timer_pending); - debug_assert!(delay_ms > 0); - self.timer_pending = true; - unsafe { SetTimer(Some(self.hwnd), TIMER_ID, delay_ms as u32, None) }; - } - - pub(super) fn kill_timer(&mut self) { - if self.timer_pending { - let _ = unsafe { KillTimer(Some(self.hwnd), TIMER_ID) }; - self.timer_pending = false; + + pub(super) fn kill_timer(&mut self) { + if self.timer_pending { + let _ = unsafe { KillTimer(Some(self.main_thread_target), TIMER_ID) }; + self.timer_pending = false; + } } - } - pub(super) fn is_timer_pending(&self) -> bool { - self.timer_pending - } + pub(super) fn is_timer_pending(&self) -> bool { + self.timer_pending + } } impl Drop for PlatformPump { - fn drop(&mut self) { - unsafe { - if self.timer_pending { - let _ = KillTimer(Some(self.hwnd), TIMER_ID); - } - - let state = SetWindowLongPtrW(self.hwnd, GWLP_USERDATA, 0) as *mut Weak; - if !state.is_null() { - drop(Box::from_raw(state)); - } - - let _ = DestroyWindow(self.hwnd); + fn drop(&mut self) { + unsafe { + if self.timer_pending { + let _ = KillTimer(Some(self.main_thread_target), TIMER_ID); + } + + let state = SetWindowLongPtrW(self.main_thread_target, GWLP_USERDATA, 0) + as *mut Weak; + if !state.is_null() { + drop(Box::from_raw(state)); + } + + let _ = DestroyWindow(self.main_thread_target); + } } - } } -unsafe extern "system" fn wndproc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { - if msg == WM_TIMER || msg == WM_HAVE_WORK { - let state = unsafe { GetWindowLongPtrW(hwnd, GWLP_USERDATA) } as *const Weak; - if !state.is_null() - && let Some(state) = unsafe { &*state }.upgrade() - { - if msg == WM_HAVE_WORK { - state.on_schedule_work(lparam.0 as i64); - } else { - state.on_timer_timeout(); - } +unsafe extern "system" fn wnd_proc( + hwnd: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, +) -> LRESULT { + if msg == WM_TIMER || msg == K_MSG_HAVE_WORK { + let state = unsafe { GetWindowLongPtrW(hwnd, GWLP_USERDATA) } as *const Weak; + if !state.is_null() + && let Some(state) = unsafe { &*state }.upgrade() + { + if msg == K_MSG_HAVE_WORK { + // OnScheduleMessagePumpWork() request. + state.on_schedule_work(lparam.0 as i64); + } else { + // Timer timed out. + state.on_timer_timeout(); + } + } } - } - unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } + unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } } diff --git a/src/lib.rs b/src/lib.rs index f1196ee..4cd51f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,16 +18,16 @@ mod window; mod window_builder; mod window_handle; -pub use config::{configure, CefConfig}; -pub use streaming::{ - register_streaming_scheme_handler, InitiatorOrigin, StreamClosed, StreamResponder, StreamWriter, -}; +pub use config::{CefConfig, configure}; pub use policy::{ - DEFAULT_PROMPT_TIMEOUT, DeferredResponder, DenyReason, NormalizedOrigin, PermissionAudit, - PermissionKind, PermissionRequest, PermissionResponder, PopupRequest, RequestSource, Verdict, - set_permission_audit, set_permission_policy, set_popup_policy, + DEFAULT_PROMPT_TIMEOUT, DeferredResponder, DenyReason, NormalizedOrigin, PermissionAudit, + PermissionKind, PermissionRequest, PermissionResponder, PopupRequest, RequestSource, Verdict, + set_permission_audit, set_permission_policy, set_popup_policy, }; pub use runtime::*; +pub use streaming::{ + InitiatorOrigin, StreamClosed, StreamResponder, StreamWriter, register_streaming_scheme_handler, +}; pub use webview::*; pub use window::CefWindowDispatcher; pub use window_builder::WindowBuilderWrapper; diff --git a/src/platform/linux/event_loop.rs b/src/platform/linux/event_loop.rs index 2664b04..eeb03e3 100644 --- a/src/platform/linux/event_loop.rs +++ b/src/platform/linux/event_loop.rs @@ -11,39 +11,39 @@ use crate::platform::EventLoopExt; use super::{taskbar, utils::with_x11}; impl EventLoopExt for dyn ActiveEventLoop + '_ { - fn set_badge_count(&self, count: Option, desktop_filename: Option) { - taskbar::set_badge_count(count, desktop_filename); - } - - fn set_badge_label(&self, _label: Option) { - // Unsupported on Linux/BSD - } - - fn cursor_position(&self) -> Result> { - with_x11(None, |xlib, display| unsafe { - let root = (xlib.XDefaultRootWindow)(display); - let mut root_return: c_ulong = 0; - let mut child_return: c_ulong = 0; - let mut root_x = 0; - let mut root_y = 0; - let mut win_x = 0; - let mut win_y = 0; - let mut mask: c_uint = 0; - - let ok = (xlib.XQueryPointer)( - display, - root, - &mut root_return, - &mut child_return, - &mut root_x, - &mut root_y, - &mut win_x, - &mut win_y, - &mut mask, - ); - - (ok != 0).then_some(PhysicalPosition::new(root_x as f64, root_y as f64)) - }) - .ok_or(Error::FailedToGetCursorPosition) - } + fn set_badge_count(&self, count: Option, desktop_filename: Option) { + taskbar::set_badge_count(count, desktop_filename); + } + + fn set_badge_label(&self, _label: Option) { + // Unsupported on Linux/BSD + } + + fn cursor_position(&self) -> Result> { + with_x11(None, |xlib, display| unsafe { + let root = (xlib.XDefaultRootWindow)(display); + let mut root_return: c_ulong = 0; + let mut child_return: c_ulong = 0; + let mut root_x = 0; + let mut root_y = 0; + let mut win_x = 0; + let mut win_y = 0; + let mut mask: c_uint = 0; + + let ok = (xlib.XQueryPointer)( + display, + root, + &mut root_return, + &mut child_return, + &mut root_x, + &mut root_y, + &mut win_x, + &mut win_y, + &mut mask, + ); + + (ok != 0).then_some(PhysicalPosition::new(root_x as f64, root_y as f64)) + }) + .ok_or(Error::FailedToGetCursorPosition) + } } diff --git a/src/platform/linux/monitor.rs b/src/platform/linux/monitor.rs index c7f1c31..9c92b4d 100644 --- a/src/platform/linux/monitor.rs +++ b/src/platform/linux/monitor.rs @@ -12,122 +12,120 @@ use crate::platform::{MonitorExt, monitor_bounds}; use super::utils::{atom, with_x11}; impl MonitorExt for MonitorHandle { - fn work_area(&self) -> PhysicalRect { - let bounds = monitor_bounds(self); - x11_work_area(bounds).unwrap_or(bounds) - } + fn work_area(&self) -> PhysicalRect { + let bounds = monitor_bounds(self); + x11_work_area(bounds).unwrap_or(bounds) + } } fn x11_work_area(monitor_bounds: PhysicalRect) -> Option> { - with_x11(None, |xlib, display| unsafe { - let root = (xlib.XDefaultRootWindow)(display); - let workareas = get_cardinal_property(xlib, display, root, "_NET_WORKAREA")?; - - let desktop = get_cardinal_property(xlib, display, root, "_NET_CURRENT_DESKTOP") - .and_then(|desktops| desktops.first().copied()) - .and_then(|desktop| usize::try_from(desktop).ok()) - .unwrap_or(0); - - let offset = desktop.checked_mul(4)?; - let workarea = workareas - .get(offset..offset + 4) - .or_else(|| workareas.get(0..4))?; - - let workarea = PhysicalRect { - position: PhysicalPosition::new( - i32::try_from(workarea[0]).ok()?, - i32::try_from(workarea[1]).ok()?, - ), - size: PhysicalSize::new( - u32::try_from(workarea[2]).ok()?, - u32::try_from(workarea[3]).ok()?, - ), - }; - - intersect_rects(monitor_bounds, workarea) - }) + with_x11(None, |xlib, display| unsafe { + let root = (xlib.XDefaultRootWindow)(display); + let workareas = get_cardinal_property(xlib, display, root, "_NET_WORKAREA")?; + + let desktop = get_cardinal_property(xlib, display, root, "_NET_CURRENT_DESKTOP") + .and_then(|desktops| desktops.first().copied()) + .and_then(|desktop| usize::try_from(desktop).ok()) + .unwrap_or(0); + + let offset = desktop.checked_mul(4)?; + let workarea = workareas + .get(offset..offset + 4) + .or_else(|| workareas.get(0..4))?; + + let workarea = PhysicalRect { + position: PhysicalPosition::new( + i32::try_from(workarea[0]).ok()?, + i32::try_from(workarea[1]).ok()?, + ), + size: PhysicalSize::new( + u32::try_from(workarea[2]).ok()?, + u32::try_from(workarea[3]).ok()?, + ), + }; + + intersect_rects(monitor_bounds, workarea) + }) } fn get_cardinal_property( - xlib: &xlib::Xlib, - display: *mut xlib::Display, - window: xlib::Window, - name: &str, + xlib: &xlib::Xlib, + display: *mut xlib::Display, + window: xlib::Window, + name: &str, ) -> Option> { - let property = atom(xlib, display, name); - if property == 0 { - return None; - } - - let mut actual_type: c_ulong = 0; - let mut actual_format: c_int = 0; - let mut nitems: c_ulong = 0; - let mut bytes_after: c_ulong = 0; - let mut data: *mut c_uchar = std::ptr::null_mut(); - - let status = unsafe { - (xlib.XGetWindowProperty)( - display, - window, - property, - 0, - 256, - 0, - xlib::XA_CARDINAL, - &mut actual_type, - &mut actual_format, - &mut nitems, - &mut bytes_after, - &mut data, - ) - }; - - if status != xlib::Success as c_int || data.is_null() { - return None; - } - - let value = if actual_type == xlib::XA_CARDINAL && actual_format == 32 && bytes_after == 0 { - let values = unsafe { std::slice::from_raw_parts(data.cast::(), nitems as usize) }; - Some(values.to_vec()) - } else { - None - }; - - unsafe { - (xlib.XFree)(data.cast()); - } - value + let property = atom(xlib, display, name); + if property == 0 { + return None; + } + + let mut actual_type: c_ulong = 0; + let mut actual_format: c_int = 0; + let mut nitems: c_ulong = 0; + let mut bytes_after: c_ulong = 0; + let mut data: *mut c_uchar = std::ptr::null_mut(); + + let status = unsafe { + (xlib.XGetWindowProperty)( + display, + window, + property, + 0, + 256, + 0, + xlib::XA_CARDINAL, + &mut actual_type, + &mut actual_format, + &mut nitems, + &mut bytes_after, + &mut data, + ) + }; + + if status != xlib::Success as c_int || data.is_null() { + return None; + } + + let value = if actual_type == xlib::XA_CARDINAL && actual_format == 32 && bytes_after == 0 { + let values = unsafe { std::slice::from_raw_parts(data.cast::(), nitems as usize) }; + Some(values.to_vec()) + } else { + None + }; + + unsafe { + (xlib.XFree)(data.cast()); + } + value } fn intersect_rects( - a: PhysicalRect, - b: PhysicalRect, + a: PhysicalRect, + b: PhysicalRect, ) -> Option> { - let left = a.position.x.max(b.position.x); - let top = a.position.y.max(b.position.y); - let right = rect_right(a).min(rect_right(b)); - let bottom = rect_bottom(a).min(rect_bottom(b)); - - if right <= left || bottom <= top { - return None; - } - - Some(PhysicalRect { - position: PhysicalPosition::new(left, top), - size: PhysicalSize::new((right - left) as u32, (bottom - top) as u32), - }) + let left = a.position.x.max(b.position.x); + let top = a.position.y.max(b.position.y); + let right = rect_right(a).min(rect_right(b)); + let bottom = rect_bottom(a).min(rect_bottom(b)); + + if right <= left || bottom <= top { + return None; + } + + Some(PhysicalRect { + position: PhysicalPosition::new(left, top), + size: PhysicalSize::new((right - left) as u32, (bottom - top) as u32), + }) } fn rect_right(rect: PhysicalRect) -> i32 { - rect - .position - .x - .saturating_add(i32::try_from(rect.size.width).unwrap_or(i32::MAX)) + rect.position + .x + .saturating_add(i32::try_from(rect.size.width).unwrap_or(i32::MAX)) } fn rect_bottom(rect: PhysicalRect) -> i32 { - rect - .position - .y - .saturating_add(i32::try_from(rect.size.height).unwrap_or(i32::MAX)) + rect.position + .y + .saturating_add(i32::try_from(rect.size.height).unwrap_or(i32::MAX)) } diff --git a/src/platform/linux/taskbar.rs b/src/platform/linux/taskbar.rs index f5ff835..17c3d34 100644 --- a/src/platform/linux/taskbar.rs +++ b/src/platform/linux/taskbar.rs @@ -7,35 +7,36 @@ use std::{cell::RefCell, ffi::CString, os::raw::c_char}; use tauri_runtime::{ProgressBarState, ProgressBarStatus}; pub(super) fn set_progress_bar(state: ProgressBarState) { - TASKBAR_INDICATOR.with_borrow_mut(|taskbar| taskbar.update(state)); + TASKBAR_INDICATOR.with_borrow_mut(|taskbar| taskbar.update(state)); } pub(super) fn set_badge_count(count: Option, desktop_filename: Option) { - TASKBAR_INDICATOR.with_borrow_mut(|taskbar| taskbar.update_count(count, desktop_filename)); + TASKBAR_INDICATOR.with_borrow_mut(|taskbar| taskbar.update_count(count, desktop_filename)); } #[derive(WrapperApi)] struct UnityLib { - unity_launcher_entry_get_for_desktop_id: unsafe extern "C" fn(id: *const c_char) -> *const isize, - unity_inspector_get_default: unsafe extern "C" fn() -> *const isize, - unity_inspector_get_unity_running: unsafe extern "C" fn(inspector: *const isize) -> i32, - unity_launcher_entry_set_progress: unsafe extern "C" fn(entry: *const isize, value: f64) -> i32, - unity_launcher_entry_set_progress_visible: - unsafe extern "C" fn(entry: *const isize, value: i32) -> i32, - unity_launcher_entry_set_count: unsafe extern "C" fn(entry: *const isize, value: i64) -> i32, - unity_launcher_entry_set_count_visible: - unsafe extern "C" fn(entry: *const isize, value: bool) -> bool, + unity_launcher_entry_get_for_desktop_id: + unsafe extern "C" fn(id: *const c_char) -> *const isize, + unity_inspector_get_default: unsafe extern "C" fn() -> *const isize, + unity_inspector_get_unity_running: unsafe extern "C" fn(inspector: *const isize) -> i32, + unity_launcher_entry_set_progress: unsafe extern "C" fn(entry: *const isize, value: f64) -> i32, + unity_launcher_entry_set_progress_visible: + unsafe extern "C" fn(entry: *const isize, value: i32) -> i32, + unity_launcher_entry_set_count: unsafe extern "C" fn(entry: *const isize, value: i64) -> i32, + unity_launcher_entry_set_count_visible: + unsafe extern "C" fn(entry: *const isize, value: bool) -> bool, } struct TaskbarIndicator { - desktop_filename: Option, - desktop_filename_c_str: Option, + desktop_filename: Option, + desktop_filename_c_str: Option, - unity_lib: Option>, - attempted_load: bool, + unity_lib: Option>, + attempted_load: bool, - unity_inspector: Option<*const isize>, - unity_entry: Option<*const isize>, + unity_inspector: Option<*const isize>, + unity_entry: Option<*const isize>, } thread_local! { @@ -43,136 +44,138 @@ thread_local! { } impl TaskbarIndicator { - fn new() -> Self { - Self { - desktop_filename: None, - desktop_filename_c_str: None, + fn new() -> Self { + Self { + desktop_filename: None, + desktop_filename_c_str: None, - unity_lib: None, - attempted_load: false, + unity_lib: None, + attempted_load: false, - unity_inspector: None, - unity_entry: None, + unity_inspector: None, + unity_entry: None, + } } - } - fn ensure_lib_load(&mut self) { - if self.attempted_load { - return; - } - - self.attempted_load = true; - - self.unity_lib = unsafe { - Container::load("libunity.so.4") - .or_else(|_| Container::load("libunity.so.6")) - .or_else(|_| Container::load("libunity.so.9")) - .ok() - }; - - if let Some(unity_lib) = &self.unity_lib { - let handle = unsafe { unity_lib.unity_inspector_get_default() }; - if !handle.is_null() { - self.unity_inspector = Some(handle); - } - } - } - - fn ensure_entry_load(&mut self) { - if let Some(unity_lib) = &self.unity_lib - && let Some(id) = &self.desktop_filename_c_str - { - let handle = unsafe { unity_lib.unity_launcher_entry_get_for_desktop_id(id.as_ptr()) }; - if !handle.is_null() { - self.unity_entry = Some(handle); - } - } - } - - fn is_unity_running(&self) -> bool { - if let Some(inspector) = self.unity_inspector - && let Some(unity_lib) = &self.unity_lib - { - return unsafe { unity_lib.unity_inspector_get_unity_running(inspector) } == 1; - } - - false - } - - fn update(&mut self, progress: ProgressBarState) { - if let Some(uri) = progress.desktop_filename { - self.desktop_filename = Some(uri); - self.desktop_filename_c_str = None; - self.unity_entry = None; - } - - self.ensure_lib_load(); - - if !self.is_unity_running() { - return; - } - - if self.desktop_filename_c_str.is_none() - && let Some(uri) = &self.desktop_filename - { - self.desktop_filename_c_str = Some(CString::new(uri.as_str()).unwrap_or_default()); - } + fn ensure_lib_load(&mut self) { + if self.attempted_load { + return; + } - if self.unity_entry.is_none() { - self.ensure_entry_load(); - } + self.attempted_load = true; - if let Some(unity_lib) = &self.unity_lib - && let Some(unity_entry) = self.unity_entry - { - if let Some(progress) = progress.progress { - let progress = progress.min(100) as f64 / 100.0; - unsafe { unity_lib.unity_launcher_entry_set_progress(unity_entry, progress) }; - } - - if let Some(status) = progress.status { - let is_visible = !matches!(status, ProgressBarStatus::None); - unsafe { - unity_lib - .unity_launcher_entry_set_progress_visible(unity_entry, if is_visible { 1 } else { 0 }) + self.unity_lib = unsafe { + Container::load("libunity.so.4") + .or_else(|_| Container::load("libunity.so.6")) + .or_else(|_| Container::load("libunity.so.9")) + .ok() }; - } - } - } - fn update_count(&mut self, count: Option, desktop_filename: Option) { - if let Some(uri) = desktop_filename { - self.desktop_filename = Some(uri); - self.desktop_filename_c_str = None; - self.unity_entry = None; + if let Some(unity_lib) = &self.unity_lib { + let handle = unsafe { unity_lib.unity_inspector_get_default() }; + if !handle.is_null() { + self.unity_inspector = Some(handle); + } + } } - self.ensure_lib_load(); - - if !self.is_unity_running() { - return; + fn ensure_entry_load(&mut self) { + if let Some(unity_lib) = &self.unity_lib + && let Some(id) = &self.desktop_filename_c_str + { + let handle = unsafe { unity_lib.unity_launcher_entry_get_for_desktop_id(id.as_ptr()) }; + if !handle.is_null() { + self.unity_entry = Some(handle); + } + } } - if self.desktop_filename_c_str.is_none() - && let Some(uri) = &self.desktop_filename - { - self.desktop_filename_c_str = Some(CString::new(uri.as_str()).unwrap_or_default()); + fn is_unity_running(&self) -> bool { + if let Some(inspector) = self.unity_inspector + && let Some(unity_lib) = &self.unity_lib + { + return unsafe { unity_lib.unity_inspector_get_unity_running(inspector) } == 1; + } + + false } - if self.unity_entry.is_none() { - self.ensure_entry_load(); + fn update(&mut self, progress: ProgressBarState) { + if let Some(uri) = progress.desktop_filename { + self.desktop_filename = Some(uri); + self.desktop_filename_c_str = None; + self.unity_entry = None; + } + + self.ensure_lib_load(); + + if !self.is_unity_running() { + return; + } + + if self.desktop_filename_c_str.is_none() + && let Some(uri) = &self.desktop_filename + { + self.desktop_filename_c_str = Some(CString::new(uri.as_str()).unwrap_or_default()); + } + + if self.unity_entry.is_none() { + self.ensure_entry_load(); + } + + if let Some(unity_lib) = &self.unity_lib + && let Some(unity_entry) = self.unity_entry + { + if let Some(progress) = progress.progress { + let progress = progress.min(100) as f64 / 100.0; + unsafe { unity_lib.unity_launcher_entry_set_progress(unity_entry, progress) }; + } + + if let Some(status) = progress.status { + let is_visible = !matches!(status, ProgressBarStatus::None); + unsafe { + unity_lib.unity_launcher_entry_set_progress_visible( + unity_entry, + if is_visible { 1 } else { 0 }, + ) + }; + } + } } - if let Some(unity_lib) = &self.unity_lib - && let Some(unity_entry) = self.unity_entry - { - if let Some(count) = count { - unsafe { unity_lib.unity_launcher_entry_set_count(unity_entry, count) }; - unsafe { unity_lib.unity_launcher_entry_set_count_visible(unity_entry, true) }; - } else { - unsafe { unity_lib.unity_launcher_entry_set_count(unity_entry, 0) }; - unsafe { unity_lib.unity_launcher_entry_set_count_visible(unity_entry, false) }; - } + fn update_count(&mut self, count: Option, desktop_filename: Option) { + if let Some(uri) = desktop_filename { + self.desktop_filename = Some(uri); + self.desktop_filename_c_str = None; + self.unity_entry = None; + } + + self.ensure_lib_load(); + + if !self.is_unity_running() { + return; + } + + if self.desktop_filename_c_str.is_none() + && let Some(uri) = &self.desktop_filename + { + self.desktop_filename_c_str = Some(CString::new(uri.as_str()).unwrap_or_default()); + } + + if self.unity_entry.is_none() { + self.ensure_entry_load(); + } + + if let Some(unity_lib) = &self.unity_lib + && let Some(unity_entry) = self.unity_entry + { + if let Some(count) = count { + unsafe { unity_lib.unity_launcher_entry_set_count(unity_entry, count) }; + unsafe { unity_lib.unity_launcher_entry_set_count_visible(unity_entry, true) }; + } else { + unsafe { unity_lib.unity_launcher_entry_set_count(unity_entry, 0) }; + unsafe { unity_lib.unity_launcher_entry_set_count_visible(unity_entry, false) }; + } + } } - } } diff --git a/src/platform/linux/utils.rs b/src/platform/linux/utils.rs index fe9b4e6..88e6ab5 100644 --- a/src/platform/linux/utils.rs +++ b/src/platform/linux/utils.rs @@ -3,10 +3,10 @@ // SPDX-License-Identifier: MIT use std::{ - cell::RefCell, - ffi::CString, - os::raw::{c_long, c_ulong}, - sync::LazyLock, + cell::RefCell, + ffi::CString, + os::raw::{c_long, c_ulong}, + sync::LazyLock, }; use x11_dl::xlib; @@ -25,84 +25,84 @@ thread_local! { } pub(super) fn with_cef_display( - default: R, - f: impl FnOnce(&xlib::Xlib, *mut xlib::Display) -> R, + default: R, + f: impl FnOnce(&xlib::Xlib, *mut xlib::Display) -> R, ) -> R { - let Some(xlib) = XLIB.as_ref() else { - return default; - }; - let display = cef::get_xdisplay() as *mut xlib::Display; - if display.is_null() { - return default; - } - - let result = f(xlib, display); - unsafe { - (xlib.XFlush)(display); - } - result -} - -pub(super) fn with_x11(default: R, f: impl FnOnce(&xlib::Xlib, *mut xlib::Display) -> R) -> R { - let Some(xlib) = XLIB.as_ref() else { - return default; - }; - - DISPLAY.with(|cell| { - let mut guard = cell.borrow_mut(); - if guard.is_none() { - let display = unsafe { (xlib.XOpenDisplay)(std::ptr::null()) }; - if display.is_null() { + let Some(xlib) = XLIB.as_ref() else { + return default; + }; + let display = cef::get_xdisplay() as *mut xlib::Display; + if display.is_null() { return default; - } - *guard = Some(Display(display)); } - let display = guard.as_ref().unwrap().0; let result = f(xlib, display); unsafe { - (xlib.XFlush)(display); + (xlib.XFlush)(display); } result - }) +} + +pub(super) fn with_x11(default: R, f: impl FnOnce(&xlib::Xlib, *mut xlib::Display) -> R) -> R { + let Some(xlib) = XLIB.as_ref() else { + return default; + }; + + DISPLAY.with(|cell| { + let mut guard = cell.borrow_mut(); + if guard.is_none() { + let display = unsafe { (xlib.XOpenDisplay)(std::ptr::null()) }; + if display.is_null() { + return default; + } + *guard = Some(Display(display)); + } + + let display = guard.as_ref().unwrap().0; + let result = f(xlib, display); + unsafe { + (xlib.XFlush)(display); + } + result + }) } pub(super) fn atom(xlib: &xlib::Xlib, display: *mut xlib::Display, name: &str) -> c_ulong { - let cname = CString::new(name).unwrap(); - unsafe { (xlib.XInternAtom)(display, cname.as_ptr(), 0) } + let cname = CString::new(name).unwrap(); + unsafe { (xlib.XInternAtom)(display, cname.as_ptr(), 0) } } pub(super) fn set_wm_state(xid: c_ulong, add: bool, atom1: &str, atom2: Option<&str>) { - with_x11((), |xlib, display| { - let wm_state = atom(xlib, display, "_NET_WM_STATE"); - let a1 = atom(xlib, display, atom1); - let a2 = atom2.map(|name| atom(xlib, display, name)).unwrap_or(0); - let action = if add { - NET_WM_STATE_ADD - } else { - NET_WM_STATE_REMOVE - }; + with_x11((), |xlib, display| { + let wm_state = atom(xlib, display, "_NET_WM_STATE"); + let a1 = atom(xlib, display, atom1); + let a2 = atom2.map(|name| atom(xlib, display, name)).unwrap_or(0); + let action = if add { + NET_WM_STATE_ADD + } else { + NET_WM_STATE_REMOVE + }; - unsafe { - let root = (xlib.XDefaultRootWindow)(display); - let mut event: xlib::XEvent = std::mem::zeroed(); - event.client_message = xlib::XClientMessageEvent { - type_: CLIENT_MESSAGE, - serial: 0, - send_event: 1, - display, - window: xid, - message_type: wm_state, - format: 32, - data: xlib::ClientMessageData::from([action, a1 as c_long, a2 as c_long, 1, 0]), - }; - (xlib.XSendEvent)( - display, - root, - 0, - SUBSTRUCTURE_REDIRECT_MASK | SUBSTRUCTURE_NOTIFY_MASK, - &mut event, - ); - } - }); + unsafe { + let root = (xlib.XDefaultRootWindow)(display); + let mut event: xlib::XEvent = std::mem::zeroed(); + event.client_message = xlib::XClientMessageEvent { + type_: CLIENT_MESSAGE, + serial: 0, + send_event: 1, + display, + window: xid, + message_type: wm_state, + format: 32, + data: xlib::ClientMessageData::from([action, a1 as c_long, a2 as c_long, 1, 0]), + }; + (xlib.XSendEvent)( + display, + root, + 0, + SUBSTRUCTURE_REDIRECT_MASK | SUBSTRUCTURE_NOTIFY_MASK, + &mut event, + ); + } + }); } diff --git a/src/platform/linux/webview.rs b/src/platform/linux/webview.rs index b32e629..a5992be 100644 --- a/src/platform/linux/webview.rs +++ b/src/platform/linux/webview.rs @@ -13,120 +13,127 @@ use crate::{webview::AppWebview, window::AppWindow}; use super::utils::{atom, with_cef_display}; impl AppWebview { - fn xid(&self) -> xlib::Window { - let xid = self.host.window_handle(); - assert_ne!(xid, 0, "failed to get XID"); - xid as xlib::Window - } - - pub(crate) fn set_background_color(&self, color: Option) { - let _ = (self, color); - // Native child-window background is not equivalent to Chromium's rendered - // background. Creation still applies BrowserSettings. - } - - pub(crate) fn bounds(&self) -> Option { - let xid = self.xid(); - - with_cef_display(None, |xlib, display| unsafe { - let mut root: xlib::Window = 0; - let mut x: i32 = 0; - let mut y: i32 = 0; - let mut width: u32 = 0; - let mut height: u32 = 0; - let mut border_width: u32 = 0; - let mut depth: u32 = 0; - - if (xlib.XGetGeometry)( - display, - xid, - &mut root, - &mut x, - &mut y, - &mut width, - &mut height, - &mut border_width, - &mut depth, - ) == 0 - { - return None; - } - - Some(Rect { - position: PhysicalPosition::new(x, y).into(), - size: PhysicalSize::new(width, height).into(), - }) - }) - } - - pub(crate) fn reparent(&self, parent: &AppWindow) { - let xid = self.xid(); - let parent_xid = parent.xid(); - - with_cef_display((), |xlib, display| unsafe { - (xlib.XReparentWindow)(display, xid, parent_xid as xlib::Window, 0, 0); - (xlib.XMapRaised)(display, xid); - }); - } - - pub(crate) fn apply_visible(&self, visible: bool) { - let xid = self.xid(); - - with_cef_display((), |xlib, display| unsafe { - let net_wm_state = atom(xlib, display, "_NET_WM_STATE"); - const PROP_MODE_REPLACE: i32 = 0; - - if visible { - (xlib.XChangeProperty)( - display, - xid, - net_wm_state, - xlib::XA_ATOM, - 32, - PROP_MODE_REPLACE, - std::ptr::null(), - 0, - ); - (xlib.XMapWindow)(display, xid); - } else { - let hidden: [c_ulong; 1] = [atom(xlib, display, "_NET_WM_STATE_HIDDEN")]; - (xlib.XChangeProperty)( - display, - xid, - net_wm_state, - xlib::XA_ATOM, - 32, - PROP_MODE_REPLACE, - hidden.as_ptr() as *const u8, - 1, - ); - (xlib.XUnmapWindow)(display, xid); - } - }); - } - - pub(crate) fn destroy_native(&self) { - let xid = self.xid(); - with_cef_display((), |xlib, display| unsafe { - (xlib.XDestroyWindow)(display, xid); - (xlib.XFlush)(display); - }); - } - - pub(crate) fn apply_physical_bounds(&self, _scale: f64, x: i32, y: i32, width: i32, height: i32) { - let xid = self.xid(); - - with_cef_display((), |xlib, display| unsafe { - (xlib.XMoveResizeWindow)( - display, - xid, - x, - y, - width.max(1) as u32, - height.max(1) as u32, - ); - // `with_cef_display` issues an `XFlush` once the closure returns, so a - // blocking `XSync` round-trip here just stalls every resize frame. - }); - } + fn xid(&self) -> xlib::Window { + let xid = self.host.window_handle(); + assert_ne!(xid, 0, "failed to get XID"); + xid as xlib::Window + } + + pub(crate) fn set_background_color(&self, color: Option) { + let _ = (self, color); + // Native child-window background is not equivalent to Chromium's rendered + // background. Creation still applies BrowserSettings. + } + + pub(crate) fn bounds(&self) -> Option { + let xid = self.xid(); + + with_cef_display(None, |xlib, display| unsafe { + let mut root: xlib::Window = 0; + let mut x: i32 = 0; + let mut y: i32 = 0; + let mut width: u32 = 0; + let mut height: u32 = 0; + let mut border_width: u32 = 0; + let mut depth: u32 = 0; + + if (xlib.XGetGeometry)( + display, + xid, + &mut root, + &mut x, + &mut y, + &mut width, + &mut height, + &mut border_width, + &mut depth, + ) == 0 + { + return None; + } + + Some(Rect { + position: PhysicalPosition::new(x, y).into(), + size: PhysicalSize::new(width, height).into(), + }) + }) + } + + pub(crate) fn reparent(&self, parent: &AppWindow) { + let xid = self.xid(); + let parent_xid = parent.xid(); + + with_cef_display((), |xlib, display| unsafe { + (xlib.XReparentWindow)(display, xid, parent_xid as xlib::Window, 0, 0); + (xlib.XMapRaised)(display, xid); + }); + } + + pub(crate) fn apply_visible(&self, visible: bool) { + let xid = self.xid(); + + with_cef_display((), |xlib, display| unsafe { + let net_wm_state = atom(xlib, display, "_NET_WM_STATE"); + const PROP_MODE_REPLACE: i32 = 0; + + if visible { + (xlib.XChangeProperty)( + display, + xid, + net_wm_state, + xlib::XA_ATOM, + 32, + PROP_MODE_REPLACE, + std::ptr::null(), + 0, + ); + (xlib.XMapWindow)(display, xid); + } else { + let hidden: [c_ulong; 1] = [atom(xlib, display, "_NET_WM_STATE_HIDDEN")]; + (xlib.XChangeProperty)( + display, + xid, + net_wm_state, + xlib::XA_ATOM, + 32, + PROP_MODE_REPLACE, + hidden.as_ptr() as *const u8, + 1, + ); + (xlib.XUnmapWindow)(display, xid); + } + }); + } + + pub(crate) fn destroy_native(&self) { + let xid = self.xid(); + with_cef_display((), |xlib, display| unsafe { + (xlib.XDestroyWindow)(display, xid); + (xlib.XFlush)(display); + }); + } + + pub(crate) fn apply_physical_bounds( + &self, + _scale: f64, + x: i32, + y: i32, + width: i32, + height: i32, + ) { + let xid = self.xid(); + + with_cef_display((), |xlib, display| unsafe { + (xlib.XMoveResizeWindow)( + display, + xid, + x, + y, + width.max(1) as u32, + height.max(1) as u32, + ); + // `with_cef_display` issues an `XFlush` once the closure returns, so a + // blocking `XSync` round-trip here just stalls every resize frame. + }); + } } diff --git a/src/platform/linux/window.rs b/src/platform/linux/window.rs index 1f39b25..2e26cf4 100644 --- a/src/platform/linux/window.rs +++ b/src/platform/linux/window.rs @@ -12,67 +12,67 @@ use crate::window::AppWindow; use super::{taskbar, utils::set_wm_state}; impl AppWindow { - pub(crate) fn raw_cef_handle(&self) -> cef::sys::cef_window_handle_t { - self.xid() as cef::sys::cef_window_handle_t - } + pub(crate) fn raw_cef_handle(&self) -> cef::sys::cef_window_handle_t { + self.xid() as cef::sys::cef_window_handle_t + } - pub(crate) fn xid(&self) -> c_ulong { - let handle = self - .window - .window_handle() - .expect("failed to get window handle"); - match handle.as_raw() { - RawWindowHandle::Xlib(handle) => handle.window as c_ulong, - RawWindowHandle::Xcb(handle) => handle.window.get() as c_ulong, - other => panic!("expected X11 window handle, got {other:?}"), + pub(crate) fn xid(&self) -> c_ulong { + let handle = self + .window + .window_handle() + .expect("failed to get window handle"); + match handle.as_raw() { + RawWindowHandle::Xlib(handle) => handle.window as c_ulong, + RawWindowHandle::Xcb(handle) => handle.window.get() as c_ulong, + other => panic!("expected X11 window handle, got {other:?}"), + } } - } - pub(crate) fn set_enabled(&self, enabled: bool) { - let _ = (self, enabled); - // TODO: implement native window enabled state on Linux/BSD. - } + pub(crate) fn set_enabled(&self, enabled: bool) { + let _ = (self, enabled); + // TODO: implement native window enabled state on Linux/BSD. + } - pub(crate) fn is_enabled(&self) -> bool { - let _ = self; - // TODO: query native window enabled state on Linux/BSD. - true - } + pub(crate) fn is_enabled(&self) -> bool { + let _ = self; + // TODO: query native window enabled state on Linux/BSD. + true + } - pub(crate) fn set_background_color(&self, color: Option) { - let xid = self.xid(); - let Some(color) = color else { - return; - }; + pub(crate) fn set_background_color(&self, color: Option) { + let xid = self.xid(); + let Some(color) = color else { + return; + }; - super::utils::with_x11((), |xlib, display| unsafe { - let screen = (xlib.XDefaultScreen)(display); - let colormap = (xlib.XDefaultColormap)(display, screen); - let mut xcolor = x11_dl::xlib::XColor { - pixel: 0, - red: u16::from(color.0) * 257, - green: u16::from(color.1) * 257, - blue: u16::from(color.2) * 257, - flags: x11_dl::xlib::DoRed | x11_dl::xlib::DoGreen | x11_dl::xlib::DoBlue, - pad: 0, - }; + super::utils::with_x11((), |xlib, display| unsafe { + let screen = (xlib.XDefaultScreen)(display); + let colormap = (xlib.XDefaultColormap)(display, screen); + let mut xcolor = x11_dl::xlib::XColor { + pixel: 0, + red: u16::from(color.0) * 257, + green: u16::from(color.1) * 257, + blue: u16::from(color.2) * 257, + flags: x11_dl::xlib::DoRed | x11_dl::xlib::DoGreen | x11_dl::xlib::DoBlue, + pad: 0, + }; - if (xlib.XAllocColor)(display, colormap, &mut xcolor) != 0 { - (xlib.XSetWindowBackground)(display, xid, xcolor.pixel); - (xlib.XClearWindow)(display, xid); - } - }); - } + if (xlib.XAllocColor)(display, colormap, &mut xcolor) != 0 { + (xlib.XSetWindowBackground)(display, xid, xcolor.pixel); + (xlib.XClearWindow)(display, xid); + } + }); + } - pub(crate) fn set_skip_taskbar(&self, skip: bool) { - set_wm_state(self.xid(), skip, "_NET_WM_STATE_SKIP_TASKBAR", None); - } + pub(crate) fn set_skip_taskbar(&self, skip: bool) { + set_wm_state(self.xid(), skip, "_NET_WM_STATE_SKIP_TASKBAR", None); + } - pub(crate) fn set_visible_on_all_workspaces(&self, visible: bool) { - set_wm_state(self.xid(), visible, "_NET_WM_STATE_STICKY", None); - } + pub(crate) fn set_visible_on_all_workspaces(&self, visible: bool) { + set_wm_state(self.xid(), visible, "_NET_WM_STATE_STICKY", None); + } - pub(crate) fn set_progress_bar(&self, state: ProgressBarState) { - taskbar::set_progress_bar(state); - } + pub(crate) fn set_progress_bar(&self, state: ProgressBarState) { + taskbar::set_progress_bar(state); + } } diff --git a/src/platform/macos/appkit_state.rs b/src/platform/macos/appkit_state.rs index e285b05..42f714c 100644 --- a/src/platform/macos/appkit_state.rs +++ b/src/platform/macos/appkit_state.rs @@ -3,8 +3,8 @@ // SPDX-License-Identifier: MIT use std::{ - ffi::c_void, - sync::{Arc, RwLock}, + ffi::c_void, + sync::{Arc, RwLock}, }; use objc2_app_kit::NSWindow; @@ -14,20 +14,20 @@ use super::utils; #[derive(Debug, Default)] pub(crate) struct AppkitState { - pub(crate) traffic_light_position: Option, + pub(crate) traffic_light_position: Option, } impl AppkitState { - pub(super) fn associate(state: &Arc>, nswindow: &NSWindow) { - utils::set_associated_data(nswindow, Self::key(), Arc::as_ptr(state)); - } + pub(super) fn associate(state: &Arc>, nswindow: &NSWindow) { + utils::set_associated_data(nswindow, Self::key(), Arc::as_ptr(state)); + } - pub(super) fn from_window(nswindow: &NSWindow) -> Option<&RwLock> { - unsafe { utils::associated_data(nswindow, Self::key()) } - } + pub(super) fn from_window(nswindow: &NSWindow) -> Option<&RwLock> { + unsafe { utils::associated_data(nswindow, Self::key()) } + } - fn key() -> *const c_void { - static APPKIT_STATE_KEY: u8 = 0; - &APPKIT_STATE_KEY as *const u8 as *const c_void - } + fn key() -> *const c_void { + static APPKIT_STATE_KEY: u8 = 0; + &APPKIT_STATE_KEY as *const u8 as *const c_void + } } diff --git a/src/platform/macos/application.rs b/src/platform/macos/application.rs index f40c094..6eaf04a 100644 --- a/src/platform/macos/application.rs +++ b/src/platform/macos/application.rs @@ -3,20 +3,20 @@ // SPDX-License-Identifier: MIT use std::{ - cell::Cell, - time::{Duration, Instant}, + cell::Cell, + time::{Duration, Instant}, }; use cef::application_mac::{CefAppProtocol, CrAppControlProtocol, CrAppProtocol}; use objc2::{ - ClassType, DefinedClass, MainThreadMarker, MainThreadOnly, define_class, extern_methods, - msg_send, - rc::Retained, - runtime::{AnyObject, Bool, ProtocolObject}, + ClassType, DefinedClass, MainThreadMarker, MainThreadOnly, define_class, extern_methods, + msg_send, + rc::Retained, + runtime::{AnyObject, Bool, ProtocolObject}, }; use objc2_app_kit::{ - NSApp, NSApplication, NSApplicationActivationOptions, NSApplicationDelegate, - NSApplicationTerminateReply, NSEvent, NSRunningApplication, + NSApp, NSApplication, NSApplicationActivationOptions, NSApplicationDelegate, + NSApplicationTerminateReply, NSEvent, NSRunningApplication, }; use objc2_application_services::kProcessTransformToForegroundApplication; use objc2_foundation::{NSArray, NSObject, NSObjectProtocol, NSString, NSURL}; @@ -25,20 +25,20 @@ use super::utils; #[derive(Default)] pub(crate) struct CefWinitApplicationIvars { - handling_send_event: Cell, - last_dock_show_ms: Cell, - delegate: Cell<*const AppDelegate>, + handling_send_event: Cell, + last_dock_show_ms: Cell, + delegate: Cell<*const AppDelegate>, } pub(crate) enum AppDelegateEvent { - TryTerminate, - Reopen { has_visible_windows: bool }, - AccessibilityChanged { enabled: bool }, - OpenURLs { urls: Vec }, + TryTerminate, + Reopen { has_visible_windows: bool }, + AccessibilityChanged { enabled: bool }, + OpenURLs { urls: Vec }, } pub(crate) struct CefAppDelegateIvars { - on_event: Box, + on_event: Box, } define_class!( @@ -178,66 +178,65 @@ define_class!( ); impl AppDelegate { - fn new(mtm: MainThreadMarker, on_event: Box) -> Retained { - let this = Self::alloc(mtm).set_ivars(CefAppDelegateIvars { on_event }); - unsafe { msg_send![super(this), init] } - } + fn new(mtm: MainThreadMarker, on_event: Box) -> Retained { + let this = Self::alloc(mtm).set_ivars(CefAppDelegateIvars { on_event }); + unsafe { msg_send![super(this), init] } + } - fn emit(&self, event: AppDelegateEvent) { - (self.ivars().on_event)(event); - } + fn emit(&self, event: AppDelegateEvent) { + (self.ivars().on_event)(event); + } } impl CefWinitApplication { - extern_methods! { - #[unsafe(method(sharedApplication))] - pub fn shared_application() -> Retained; - } + extern_methods! { + #[unsafe(method(sharedApplication))] + pub fn shared_application() -> Retained; + } - pub fn last_dock_show(&self) -> Option { - match self.ivars().last_dock_show_ms.get() { - 0 => None, - // Store elapsed milliseconds + 1 so the zero-initialized ivar can mean - // "not set" for AppKit-created NSApplication instances. - milliseconds => Some(utils::instant_epoch() + Duration::from_millis(milliseconds - 1)), + pub fn last_dock_show(&self) -> Option { + match self.ivars().last_dock_show_ms.get() { + 0 => None, + // Store elapsed milliseconds + 1 so the zero-initialized ivar can mean + // "not set" for AppKit-created NSApplication instances. + milliseconds => Some(utils::instant_epoch() + Duration::from_millis(milliseconds - 1)), + } } - } - pub fn set_last_dock_show(&self, instant: Instant) { - let milliseconds = instant - .saturating_duration_since(utils::instant_epoch()) - .as_millis() - .try_into() - .unwrap_or(u64::MAX); - self - .ivars() - .last_dock_show_ms - // Offset by one so `0` remains the zero-initialized "not set" sentinel. - .set(milliseconds.saturating_add(1)); - } + pub fn set_last_dock_show(&self, instant: Instant) { + let milliseconds = instant + .saturating_duration_since(utils::instant_epoch()) + .as_millis() + .try_into() + .unwrap_or(u64::MAX); + self.ivars() + .last_dock_show_ms + // Offset by one so `0` remains the zero-initialized "not set" sentinel. + .set(milliseconds.saturating_add(1)); + } - fn delegate(&self) -> Option<&AppDelegate> { - unsafe { self.ivars().delegate.get().as_ref() } - } + fn delegate(&self) -> Option<&AppDelegate> { + unsafe { self.ivars().delegate.get().as_ref() } + } } pub fn setup_application() { - let _ = CefWinitApplication::shared_application(); - let mtm = MainThreadMarker::new().expect("macOS application must start on the main thread"); - assert!(NSApp(mtm).isKindOfClass(CefWinitApplication::class())); + let _ = CefWinitApplication::shared_application(); + let mtm = MainThreadMarker::new().expect("macOS application must start on the main thread"); + assert!(NSApp(mtm).isKindOfClass(CefWinitApplication::class())); } pub(crate) fn set_application_event_handler( - on_event: Box, + on_event: Box, ) -> Retained { - let mtm = MainThreadMarker::new().expect("macOS application must start on the main thread"); - let delegate = AppDelegate::new(mtm, on_event); - let app = CefWinitApplication::shared_application(); - - // `NSApplication.delegate` is weak. The runtime owns the retained delegate; - // the app stores only a non-owning pointer because AppKit creates the - // NSApplication singleton and its ivars must stay zero-initialized/no-drop. - app.ivars().delegate.set(&*delegate as *const AppDelegate); - app.setDelegate(Some(ProtocolObject::from_ref(&*delegate))); - delegate + let mtm = MainThreadMarker::new().expect("macOS application must start on the main thread"); + let delegate = AppDelegate::new(mtm, on_event); + let app = CefWinitApplication::shared_application(); + + // `NSApplication.delegate` is weak. The runtime owns the retained delegate; + // the app stores only a non-owning pointer because AppKit creates the + // NSApplication singleton and its ivars must stay zero-initialized/no-drop. + app.ivars().delegate.set(&*delegate as *const AppDelegate); + app.setDelegate(Some(ProtocolObject::from_ref(&*delegate))); + delegate } diff --git a/src/platform/macos/dock.rs b/src/platform/macos/dock.rs index bb07709..b4876b9 100644 --- a/src/platform/macos/dock.rs +++ b/src/platform/macos/dock.rs @@ -7,7 +7,7 @@ use std::time::{Duration, Instant}; use objc2::{msg_send, runtime::AnyObject, sel}; use objc2_app_kit::{NSApplicationActivationOptions, NSRunningApplication}; use objc2_application_services::{ - kProcessTransformToForegroundApplication, kProcessTransformToUIElementApplication, + kProcessTransformToForegroundApplication, kProcessTransformToUIElementApplication, }; use objc2_foundation::NSString; @@ -17,77 +17,78 @@ const DOCK_SHOW_TIMEOUT: Duration = Duration::from_secs(1); const DOCK_BUNDLE_IDENTIFIER: &str = "com.apple.dock"; impl CefWinitApplication { - pub fn set_dock_visibility(&self, visible: bool) { - if visible { - self.set_dock_show(); - } else { - self.set_dock_hide(); + pub fn set_dock_visibility(&self, visible: bool) { + if visible { + self.set_dock_show(); + } else { + self.set_dock_hide(); + } } - } - fn set_dock_hide(&self) { - let now = Instant::now(); - if let Some(last_dock_show_time) = self.last_dock_show() { - // TransformProcessType from UIElement back to foreground is asynchronous - // and does not expose a completion signal. Electron found that rapid - // hide/show cycles can race the macOS Dock and leave duplicate app icons - // behind, so it ignores hide requests immediately after showing. - // https://github.com/electron/electron/blob/88cd4b418618424fbcd11917fffee489f534ad72/shell/browser/browser_mac.mm#L2376-L2408 - if now.duration_since(last_dock_show_time) < DOCK_SHOW_TIMEOUT { - return; - } - } + fn set_dock_hide(&self) { + let now = Instant::now(); + if let Some(last_dock_show_time) = self.last_dock_show() { + // TransformProcessType from UIElement back to foreground is asynchronous + // and does not expose a completion signal. Electron found that rapid + // hide/show cycles can race the macOS Dock and leave duplicate app icons + // behind, so it ignores hide requests immediately after showing. + // https://github.com/electron/electron/blob/88cd4b418618424fbcd11917fffee489f534ad72/shell/browser/browser_mac.mm#L2376-L2408 + if now.duration_since(last_dock_show_time) < DOCK_SHOW_TIMEOUT { + return; + } + } - self.set_windows_can_hide(false); - utils::transform_process_type(kProcessTransformToUIElementApplication); - } + self.set_windows_can_hide(false); + utils::transform_process_type(kProcessTransformToUIElementApplication); + } - fn set_dock_show(&self) { - self.set_last_dock_show(Instant::now()); - self.set_windows_can_hide(true); + fn set_dock_show(&self) { + self.set_last_dock_show(Instant::now()); + self.set_windows_can_hide(true); - if NSRunningApplication::currentApplication().isActive() { - // TransformProcessType is buggy when bringing an active UIElement app - // back to foreground. Electron works around it by activating Dock first, - // then delaying the foreground transform and app reactivation: - // https://github.com/electron/electron/blob/88cd4b418618424fbcd11917fffee489f534ad72/shell/browser/browser_mac.mm#L2424-L2475 - activate_dock(); - self.perform_delayed_dock_show(); - } else { - utils::transform_process_type(kProcessTransformToForegroundApplication); + if NSRunningApplication::currentApplication().isActive() { + // TransformProcessType is buggy when bringing an active UIElement app + // back to foreground. Electron works around it by activating Dock first, + // then delaying the foreground transform and app reactivation: + // https://github.com/electron/electron/blob/88cd4b418618424fbcd11917fffee489f534ad72/shell/browser/browser_mac.mm#L2424-L2475 + activate_dock(); + self.perform_delayed_dock_show(); + } else { + utils::transform_process_type(kProcessTransformToForegroundApplication); + } } - } - fn set_windows_can_hide(&self, can_hide: bool) { - let windows = self.windows(); - for idx in 0..windows.count() { - windows.objectAtIndex(idx).setCanHide(can_hide); + fn set_windows_can_hide(&self, can_hide: bool) { + let windows = self.windows(); + for idx in 0..windows.count() { + windows.objectAtIndex(idx).setCanHide(can_hide); + } } - } - fn perform_delayed_dock_show(&self) { - unsafe { - let _: () = msg_send![ - self, - performSelector: sel!(tauriTransformProcessToForeground), - withObject: None::<&AnyObject>, - afterDelay: 1.0f64, - ]; - let _: () = msg_send![ - self, - performSelector: sel!(tauriActivateCurrentApplication), - withObject: None::<&AnyObject>, - afterDelay: 2.0f64, - ]; + fn perform_delayed_dock_show(&self) { + unsafe { + let _: () = msg_send![ + self, + performSelector: sel!(tauriTransformProcessToForeground), + withObject: None::<&AnyObject>, + afterDelay: 1.0f64, + ]; + let _: () = msg_send![ + self, + performSelector: sel!(tauriActivateCurrentApplication), + withObject: None::<&AnyObject>, + afterDelay: 2.0f64, + ]; + } } - } } fn activate_dock() { - let dock_id = NSString::from_str(DOCK_BUNDLE_IDENTIFIER); - let dock_apps = NSRunningApplication::runningApplicationsWithBundleIdentifier(&dock_id); - if dock_apps.count() > 0 { - let app = dock_apps.objectAtIndex(0); - app.activateWithOptions(NSApplicationActivationOptions::ActivateIgnoringOtherApps); - } + let dock_id = NSString::from_str(DOCK_BUNDLE_IDENTIFIER); + let dock_apps = NSRunningApplication::runningApplicationsWithBundleIdentifier(&dock_id); + if dock_apps.count() > 0 { + let app = dock_apps.objectAtIndex(0); + #[allow(deprecated)] + app.activateWithOptions(NSApplicationActivationOptions::ActivateIgnoringOtherApps); + } } diff --git a/src/platform/macos/event_loop.rs b/src/platform/macos/event_loop.rs index 7b6c844..86adf3b 100644 --- a/src/platform/macos/event_loop.rs +++ b/src/platform/macos/event_loop.rs @@ -6,8 +6,8 @@ use objc2::MainThreadMarker; use objc2_app_kit::{NSApp, NSApplication, NSApplicationActivationPolicy, NSEvent, NSScreen}; use objc2_foundation::{NSPoint, NSString}; use tauri_runtime::{ - Error, ProgressBarState, Result, - dpi::{LogicalPosition, PhysicalPosition}, + Error, ProgressBarState, Result, + dpi::{LogicalPosition, PhysicalPosition}, }; use winit::event_loop::ActiveEventLoop; @@ -16,83 +16,86 @@ use crate::platform::EventLoopExt; use super::{application::CefWinitApplication, progress}; impl EventLoopExt for dyn ActiveEventLoop + '_ { - fn set_activation_policy(&self, policy: tauri_runtime::ActivationPolicy) { - let Some(mtm) = MainThreadMarker::new() else { - return; - }; - - let app = NSApplication::sharedApplication(mtm); - let policy = match policy { - tauri_runtime::ActivationPolicy::Regular => NSApplicationActivationPolicy::Regular, - tauri_runtime::ActivationPolicy::Accessory => NSApplicationActivationPolicy::Accessory, - tauri_runtime::ActivationPolicy::Prohibited => NSApplicationActivationPolicy::Prohibited, - _ => NSApplicationActivationPolicy::Regular, - }; - app.setActivationPolicy(policy); - } - - fn set_dock_visibility(&self, visible: bool) { - let Some(_mtm) = MainThreadMarker::new() else { - return; - }; - - let app = CefWinitApplication::shared_application(); - app.set_dock_visibility(visible); - } - - fn show_application(&self) { - let Some(mtm) = MainThreadMarker::new() else { - return; - }; - - NSApp(mtm).unhide(None); - } - - fn hide_application(&self) { - let Some(mtm) = MainThreadMarker::new() else { - return; - }; - - NSApp(mtm).hide(None); - } - - fn set_progress_bar(&self, state: ProgressBarState) { - progress::set_dock_progress_bar(state); - } - - fn set_badge_count(&self, count: Option, _desktop_filename: Option) { - self.set_badge_label(count.map(|count| count.to_string())); - } - - fn set_badge_label(&self, label: Option) { - let Some(mtm) = MainThreadMarker::new() else { - return; - }; - - let app = NSApplication::sharedApplication(mtm); - let dock_tile = app.dockTile(); - let ns_label = label.map(|label| NSString::from_str(&label)); - dock_tile.setBadgeLabel(ns_label.as_deref()); - } - - fn cursor_position(&self) -> Result> { - let Some(mtm) = MainThreadMarker::new() else { - return Err(Error::FailedToGetCursorPosition); - }; - - // `NSEvent::mouseLocation` is in global coordinates with a bottom-left - // origin, in logical points. The global origin is the bottom-left of the - // primary screen, so flip Y against the primary screen height, then scale to - // physical pixels to satisfy the trait contract (the Windows/Linux backends - // and wry/tao all return physical pixels — returning logical here is off by - // the scale factor on HiDPI/Retina displays). - let location: NSPoint = NSEvent::mouseLocation(); - let primary = - unsafe { NSScreen::screens(mtm).firstObject() }.ok_or(Error::FailedToGetCursorPosition)?; - let screen_height = primary.frame().size.height; - let scale = primary.backingScaleFactor(); - - let logical = LogicalPosition::new(location.x, screen_height - location.y); - Ok(logical.to_physical(scale)) - } + fn set_activation_policy(&self, policy: tauri_runtime::ActivationPolicy) { + let Some(mtm) = MainThreadMarker::new() else { + return; + }; + + let app = NSApplication::sharedApplication(mtm); + let policy = match policy { + tauri_runtime::ActivationPolicy::Regular => NSApplicationActivationPolicy::Regular, + tauri_runtime::ActivationPolicy::Accessory => NSApplicationActivationPolicy::Accessory, + tauri_runtime::ActivationPolicy::Prohibited => { + NSApplicationActivationPolicy::Prohibited + } + _ => NSApplicationActivationPolicy::Regular, + }; + app.setActivationPolicy(policy); + } + + fn set_dock_visibility(&self, visible: bool) { + let Some(_mtm) = MainThreadMarker::new() else { + return; + }; + + let app = CefWinitApplication::shared_application(); + app.set_dock_visibility(visible); + } + + fn show_application(&self) { + let Some(mtm) = MainThreadMarker::new() else { + return; + }; + + NSApp(mtm).unhide(None); + } + + fn hide_application(&self) { + let Some(mtm) = MainThreadMarker::new() else { + return; + }; + + NSApp(mtm).hide(None); + } + + fn set_progress_bar(&self, state: ProgressBarState) { + progress::set_dock_progress_bar(state); + } + + fn set_badge_count(&self, count: Option, _desktop_filename: Option) { + self.set_badge_label(count.map(|count| count.to_string())); + } + + fn set_badge_label(&self, label: Option) { + let Some(mtm) = MainThreadMarker::new() else { + return; + }; + + let app = NSApplication::sharedApplication(mtm); + let dock_tile = app.dockTile(); + let ns_label = label.map(|label| NSString::from_str(&label)); + dock_tile.setBadgeLabel(ns_label.as_deref()); + } + + fn cursor_position(&self) -> Result> { + let Some(mtm) = MainThreadMarker::new() else { + return Err(Error::FailedToGetCursorPosition); + }; + + // `NSEvent::mouseLocation` is in global coordinates with a bottom-left + // origin, in logical points. The global origin is the bottom-left of the + // primary screen, so flip Y against the primary screen height, then scale to + // physical pixels to satisfy the trait contract (the Windows/Linux backends + // and wry/tao all return physical pixels — returning logical here is off by + // the scale factor on HiDPI/Retina displays). + let location: NSPoint = NSEvent::mouseLocation(); + let primary = NSScreen::screens(mtm) + .firstObject() + .ok_or(Error::FailedToGetCursorPosition)?; + let screen_height = primary.frame().size.height; + let scale = primary.backingScaleFactor(); + + let logical = LogicalPosition::new(location.x, screen_height - location.y); + Ok(logical.to_physical(scale)) + } } diff --git a/src/platform/macos/monitor.rs b/src/platform/macos/monitor.rs index 4e42472..fdaa741 100644 --- a/src/platform/macos/monitor.rs +++ b/src/platform/macos/monitor.rs @@ -9,27 +9,27 @@ use winit::{monitor::MonitorHandle, platform::macos::MonitorHandleExtMacOS}; use crate::platform::{MonitorExt, monitor_bounds}; impl MonitorExt for MonitorHandle { - fn work_area(&self) -> PhysicalRect { - let Some(ns_screen) = self.ns_screen() else { - return monitor_bounds(self); - }; + fn work_area(&self) -> PhysicalRect { + let Some(ns_screen) = self.ns_screen() else { + return monitor_bounds(self); + }; - let ns_screen: &NSScreen = unsafe { &*ns_screen.cast() }; - let screen_frame = ns_screen.frame(); - let visible_frame = ns_screen.visibleFrame(); - let scale_factor = self.scale_factor(); + let ns_screen: &NSScreen = unsafe { &*ns_screen.cast() }; + let screen_frame = ns_screen.frame(); + let visible_frame = ns_screen.visibleFrame(); + let scale_factor = self.scale_factor(); - let position = self.position().unwrap_or_default(); - let mut position = position.to_logical::(scale_factor); - position.x += visible_frame.origin.x - screen_frame.origin.x; - position.y += (screen_frame.origin.y + screen_frame.size.height) - - (visible_frame.origin.y + visible_frame.size.height); + let position = self.position().unwrap_or_default(); + let mut position = position.to_logical::(scale_factor); + position.x += visible_frame.origin.x - screen_frame.origin.x; + position.y += (screen_frame.origin.y + screen_frame.size.height) + - (visible_frame.origin.y + visible_frame.size.height); - let size = LogicalSize::new(visible_frame.size.width, visible_frame.size.height); + let size = LogicalSize::new(visible_frame.size.width, visible_frame.size.height); - PhysicalRect { - position: position.to_physical(scale_factor), - size: size.to_physical(scale_factor), + PhysicalRect { + position: position.to_physical(scale_factor), + size: size.to_physical(scale_factor), + } } - } } diff --git a/src/platform/macos/progress.rs b/src/platform/macos/progress.rs index a67058c..d9a6a02 100644 --- a/src/platform/macos/progress.rs +++ b/src/platform/macos/progress.rs @@ -6,21 +6,21 @@ use std::cell::Cell; use objc2::{DefinedClass, MainThreadMarker, MainThreadOnly, define_class, msg_send, rc::Retained}; use objc2_app_kit::{ - NSApplication, NSBezierPath, NSColor, NSDockTile, NSImageView, NSProgressIndicator, NSView, + NSApplication, NSBezierPath, NSColor, NSDockTile, NSImageView, NSProgressIndicator, NSView, }; use objc2_foundation::{NSInsetRect, NSPoint, NSRect, NSSize}; use tauri_runtime::{ProgressBarState, ProgressBarStatus}; struct DockProgressIndicatorIvars { - state: Cell, + state: Cell, } impl Default for DockProgressIndicatorIvars { - fn default() -> Self { - Self { - state: Cell::new(ProgressBarStatus::None), + fn default() -> Self { + Self { + state: Cell::new(ProgressBarStatus::None), + } } - } } define_class!( @@ -57,82 +57,82 @@ define_class!( ); impl DockProgressIndicator { - fn new(mtm: MainThreadMarker, frame: NSRect) -> Retained { - let this = Self::alloc(mtm).set_ivars(DockProgressIndicatorIvars::default()); - unsafe { msg_send![super(this), initWithFrame: frame] } - } + fn new(mtm: MainThreadMarker, frame: NSRect) -> Retained { + let this = Self::alloc(mtm).set_ivars(DockProgressIndicatorIvars::default()); + unsafe { msg_send![super(this), initWithFrame: frame] } + } - fn set_state(&self, status: ProgressBarStatus) { - self.ivars().state.set(status); - } + fn set_state(&self, status: ProgressBarStatus) { + self.ivars().state.set(status); + } } fn draw_rounded_rect(rect: NSRect) { - let radius = rect.size.height / 2.0; - NSBezierPath::bezierPathWithRoundedRect_xRadius_yRadius(rect, radius, radius).fill(); + let radius = rect.size.height / 2.0; + NSBezierPath::bezierPathWithRoundedRect_xRadius_yRadius(rect, radius, radius).fill(); } pub fn set_dock_progress_bar(state: ProgressBarState) { - let Some(mtm) = MainThreadMarker::new() else { - return; - }; - let app = NSApplication::sharedApplication(mtm); - let dock_tile = app.dockTile(); - let Some(progress_indicator) = dock_progress_indicator(&app, &dock_tile, mtm) else { - return; - }; - - if let Some(progress) = state.progress { - progress_indicator.setDoubleValue(progress.min(100) as f64); - progress_indicator.setHidden(false); - } + let Some(mtm) = MainThreadMarker::new() else { + return; + }; + let app = NSApplication::sharedApplication(mtm); + let dock_tile = app.dockTile(); + let Some(progress_indicator) = dock_progress_indicator(&app, &dock_tile, mtm) else { + return; + }; + + if let Some(progress) = state.progress { + progress_indicator.setDoubleValue(progress.min(100) as f64); + progress_indicator.setHidden(false); + } - if let Some(status) = state.status { - progress_indicator.set_state(status); - progress_indicator.setHidden(matches!(status, ProgressBarStatus::None)); - } + if let Some(status) = state.status { + progress_indicator.set_state(status); + progress_indicator.setHidden(matches!(status, ProgressBarStatus::None)); + } - dock_tile.display(); + dock_tile.display(); } fn dock_progress_indicator( - app: &NSApplication, - dock_tile: &NSDockTile, - mtm: MainThreadMarker, + app: &NSApplication, + dock_tile: &NSDockTile, + mtm: MainThreadMarker, ) -> Option> { - let content_view = match dock_tile.contentView(mtm) { - Some(content_view) => content_view, - None => { - let app_icon = app.applicationIconImage()?; - let image_view = NSImageView::imageViewWithImage(&app_icon, mtm); - dock_tile.setContentView(Some(&image_view)); - dock_tile.contentView(mtm)? + let content_view = match dock_tile.contentView(mtm) { + Some(content_view) => content_view, + None => { + let app_icon = app.applicationIconImage()?; + let image_view = NSImageView::imageViewWithImage(&app_icon, mtm); + dock_tile.setContentView(Some(&image_view)); + dock_tile.contentView(mtm)? + } + }; + + if let Some(progress_indicator) = existing_progress_indicator(&content_view) { + return Some(progress_indicator); } - }; - if let Some(progress_indicator) = existing_progress_indicator(&content_view) { - return Some(progress_indicator); - } - - let dock_tile_size = dock_tile.size(); - let frame = NSRect::new( - NSPoint::new(0.0, 0.0), - NSSize::new(dock_tile_size.width, 15.0), - ); - let progress_indicator = DockProgressIndicator::new(mtm, frame); - content_view.addSubview(&progress_indicator); + let dock_tile_size = dock_tile.size(); + let frame = NSRect::new( + NSPoint::new(0.0, 0.0), + NSSize::new(dock_tile_size.width, 15.0), + ); + let progress_indicator = DockProgressIndicator::new(mtm, frame); + content_view.addSubview(&progress_indicator); - Some(progress_indicator) + Some(progress_indicator) } fn existing_progress_indicator(content_view: &NSView) -> Option> { - let subviews = content_view.subviews(); - for idx in 0..subviews.count() { - let subview = subviews.objectAtIndex(idx); - if let Ok(progress_indicator) = subview.downcast::() { - return Some(progress_indicator); + let subviews = content_view.subviews(); + for idx in 0..subviews.count() { + let subview = subviews.objectAtIndex(idx); + if let Ok(progress_indicator) = subview.downcast::() { + return Some(progress_indicator); + } } - } - None + None } diff --git a/src/platform/macos/utils.rs b/src/platform/macos/utils.rs index 6c941cf..6f5e694 100644 --- a/src/platform/macos/utils.rs +++ b/src/platform/macos/utils.rs @@ -5,14 +5,14 @@ use std::{ffi::c_void, sync::OnceLock, time::Instant}; use objc2::{ - ClassType, - ffi::{OBJC_ASSOCIATION_RETAIN_NONATOMIC, objc_getAssociatedObject, objc_setAssociatedObject}, - rc::Retained, - runtime::AnyObject, + ClassType, + ffi::{OBJC_ASSOCIATION_RETAIN_NONATOMIC, objc_getAssociatedObject, objc_setAssociatedObject}, + rc::Retained, + runtime::AnyObject, }; use objc2_app_kit::NSColor; use objc2_application_services::{ - ProcessApplicationTransformState, TransformProcessType, kCurrentProcess, + ProcessApplicationTransformState, TransformProcessType, kCurrentProcess, }; use objc2_foundation::NSValue; use tauri_utils::config::Color; @@ -20,63 +20,63 @@ use tauri_utils::config::Color; #[repr(C)] #[allow(non_snake_case)] struct ProcessSerialNumber { - highLongOfPSN: u32, - lowLongOfPSN: u32, + highLongOfPSN: u32, + lowLongOfPSN: u32, } pub fn transform_process_type(transform_state: ProcessApplicationTransformState) { - let process_serial_number = ProcessSerialNumber { - highLongOfPSN: 0, - lowLongOfPSN: kCurrentProcess, - }; + let process_serial_number = ProcessSerialNumber { + highLongOfPSN: 0, + lowLongOfPSN: kCurrentProcess, + }; - unsafe { - let serial = (&process_serial_number as *const ProcessSerialNumber).cast(); - let _ = TransformProcessType(serial, transform_state); - } + unsafe { + let serial = (&process_serial_number as *const ProcessSerialNumber).cast(); + let _ = TransformProcessType(serial, transform_state); + } } pub fn ns_color_from_tauri_color(color: Color) -> Retained { - let Color(red, green, blue, alpha) = color; - let scale = u8::MAX as f64; - NSColor::colorWithSRGBRed_green_blue_alpha( - red as f64 / scale, - green as f64 / scale, - blue as f64 / scale, - alpha as f64 / scale, - ) + let Color(red, green, blue, alpha) = color; + let scale = u8::MAX as f64; + NSColor::colorWithSRGBRed_green_blue_alpha( + red as f64 / scale, + green as f64 / scale, + blue as f64 / scale, + alpha as f64 / scale, + ) } pub fn instant_epoch() -> Instant { - static INSTANT_EPOCH: OnceLock = OnceLock::new(); - *INSTANT_EPOCH.get_or_init(Instant::now) + static INSTANT_EPOCH: OnceLock = OnceLock::new(); + *INSTANT_EPOCH.get_or_init(Instant::now) } pub(crate) fn set_associated_data(object: &O, key: *const c_void, data: *const T) { - let value: Retained = NSValue::new(data.cast::()).into(); - unsafe { - objc_setAssociatedObject( - object as *const O as *mut AnyObject, - key, - Retained::as_ptr(&value) as *mut AnyObject, - OBJC_ASSOCIATION_RETAIN_NONATOMIC, - ); - } + let value: Retained = NSValue::new(data.cast::()).into(); + unsafe { + objc_setAssociatedObject( + object as *const O as *mut AnyObject, + key, + Retained::as_ptr(&value) as *mut AnyObject, + OBJC_ASSOCIATION_RETAIN_NONATOMIC, + ); + } } pub(crate) unsafe fn associated_data( - object: &O, - key: *const c_void, + object: &O, + key: *const c_void, ) -> Option<&T> { - let value = unsafe { objc_getAssociatedObject(object as *const O as *const AnyObject, key) }; - if value.is_null() { - return None; - } + let value = unsafe { objc_getAssociatedObject(object as *const O as *const AnyObject, key) }; + if value.is_null() { + return None; + } - let data = unsafe { (*(value as *const NSValue)).get::<*const c_void>() }; - if data.is_null() { - return None; - } + let data = unsafe { (*(value as *const NSValue)).get::<*const c_void>() }; + if data.is_null() { + return None; + } - Some(unsafe { &*(data as *const T) }) + Some(unsafe { &*(data as *const T) }) } diff --git a/src/platform/macos/webview.rs b/src/platform/macos/webview.rs index 0ff667e..b1851e9 100644 --- a/src/platform/macos/webview.rs +++ b/src/platform/macos/webview.rs @@ -14,89 +14,96 @@ use crate::{webview::AppWebview, window::AppWindow}; use super::utils; impl AppWebview { - pub(crate) fn nsview(&self) -> Retained { - let handle = self.host.window_handle(); - let view = handle.cast::(); - unsafe { Retained::::retain(view).expect("failed to retain NSView") } - } - - pub(crate) fn set_background_color(&self, color: Option) { - let nsview = self.nsview(); - - nsview.setWantsLayer(true); - - let Some(layer) = nsview.layer() else { - return; - }; - - let nscolor = color - .map(utils::ns_color_from_tauri_color) - .unwrap_or_else(NSColor::windowBackgroundColor); - - let cg_color = nscolor.CGColor(); - layer.setBackgroundColor(Some(&*cg_color)); - } - - pub(crate) fn bounds(&self) -> Option { - let nsview = self.nsview(); - - let parent = unsafe { nsview.superview()? }; - let parent_frame = parent.frame(); - let frame = nsview.frame(); - - let y = if parent.isFlipped() { - frame.origin.y - } else { - parent_frame.size.height - frame.origin.y - frame.size.height - }; - - let position = LogicalPosition::new(frame.origin.x, y); - let size = LogicalSize::new(frame.size.width, frame.size.height); - - Some(Rect { - position: position.into(), - size: size.into(), - }) - } - - pub(crate) fn reparent(&self, parent: &AppWindow) { - let view = self.nsview(); - let parent = parent.nsview(); - - parent.addSubview(&view); - } - - pub(crate) fn apply_visible(&self, visible: bool) { - let nsview = self.nsview(); - - nsview.setHidden(!visible); - } - - pub(crate) fn destroy_native(&self) { - let nsview = self.nsview(); - nsview.removeFromSuperview(); - } - - pub(crate) fn apply_physical_bounds(&self, scale: f64, x: i32, y: i32, width: i32, height: i32) { - let nsview = self.nsview(); - let Some(parent) = (unsafe { nsview.superview() }) else { - return; - }; - - // CEF provides child bounds as physical pixels, but NSView frames are logical pixels. - let x = x as f64 / scale; - let y = y as f64 / scale; - let width = width as f64 / scale; - let height = height as f64 / scale; - - let parent_frame = parent.frame(); - let y = if parent.isFlipped() { - y - } else { - parent_frame.size.height - (y + height) - }; - - let frame = NSRect::new(NSPoint::new(x, y), NSSize::new(width, height)); - nsview.setFrame(frame); - } + pub(crate) fn nsview(&self) -> Retained { + let handle = self.host.window_handle(); + let view = handle.cast::(); + unsafe { Retained::::retain(view).expect("failed to retain NSView") } + } + + pub(crate) fn set_background_color(&self, color: Option) { + let nsview = self.nsview(); + + nsview.setWantsLayer(true); + + let Some(layer) = nsview.layer() else { + return; + }; + + let nscolor = color + .map(utils::ns_color_from_tauri_color) + .unwrap_or_else(NSColor::windowBackgroundColor); + + let cg_color = nscolor.CGColor(); + layer.setBackgroundColor(Some(&*cg_color)); + } + + pub(crate) fn bounds(&self) -> Option { + let nsview = self.nsview(); + + let parent = unsafe { nsview.superview()? }; + let parent_frame = parent.frame(); + let frame = nsview.frame(); + + let y = if parent.isFlipped() { + frame.origin.y + } else { + parent_frame.size.height - frame.origin.y - frame.size.height + }; + + let position = LogicalPosition::new(frame.origin.x, y); + let size = LogicalSize::new(frame.size.width, frame.size.height); + + Some(Rect { + position: position.into(), + size: size.into(), + }) + } + + pub(crate) fn reparent(&self, parent: &AppWindow) { + let view = self.nsview(); + let parent = parent.nsview(); + + parent.addSubview(&view); + } + + pub(crate) fn apply_visible(&self, visible: bool) { + let nsview = self.nsview(); + + nsview.setHidden(!visible); + } + + pub(crate) fn destroy_native(&self) { + let nsview = self.nsview(); + nsview.removeFromSuperview(); + } + + pub(crate) fn apply_physical_bounds( + &self, + scale: f64, + x: i32, + y: i32, + width: i32, + height: i32, + ) { + let nsview = self.nsview(); + let Some(parent) = (unsafe { nsview.superview() }) else { + return; + }; + + // CEF provides child bounds as physical pixels, but NSView frames are logical pixels. + let x = x as f64 / scale; + let y = y as f64 / scale; + let width = width as f64 / scale; + let height = height as f64 / scale; + + let parent_frame = parent.frame(); + let y = if parent.isFlipped() { + y + } else { + parent_frame.size.height - (y + height) + }; + + let frame = NSRect::new(NSPoint::new(x, y), NSSize::new(width, height)); + nsview.setFrame(frame); + } } diff --git a/src/platform/macos/window.rs b/src/platform/macos/window.rs index 29930e2..2e99892 100644 --- a/src/platform/macos/window.rs +++ b/src/platform/macos/window.rs @@ -6,13 +6,13 @@ use std::{cell::Cell, mem, ptr}; use dispatch2::MainThreadBound; use objc2::{ - rc::Retained, - runtime::{Imp, Sel}, - sel, + rc::Retained, + runtime::{Imp, Sel}, + sel, }; use objc2_app_kit::{ - NSBackingStoreType, NSColor, NSView, NSWindow, NSWindowButton, NSWindowCollectionBehavior, - NSWindowStyleMask, + NSBackingStoreType, NSColor, NSView, NSWindow, NSWindowButton, NSWindowCollectionBehavior, + NSWindowStyleMask, }; use objc2_foundation::{MainThreadMarker, NSPoint, NSRect}; use raw_window_handle::{HasWindowHandle, RawWindowHandle}; @@ -24,227 +24,226 @@ use crate::window::AppWindow; use super::{AppkitState, utils}; impl AppWindow { - pub(crate) fn raw_cef_handle(&self) -> cef::sys::cef_window_handle_t { - let nsview = self.nsview(); - Retained::as_ptr(&nsview).cast_mut().cast() - } - - pub(crate) fn nsview(&self) -> Retained { - let handle = self - .window - .window_handle() - .expect("failed to get window handle"); - match handle.as_raw() { - RawWindowHandle::AppKit(handle) => unsafe { - Retained::::retain(handle.ns_view.as_ptr().cast::()) - .expect("failed to retain NSView") - }, - other => panic!("expected AppKit window handle, got {other:?}"), + pub(crate) fn raw_cef_handle(&self) -> cef::sys::cef_window_handle_t { + let nsview = self.nsview(); + Retained::as_ptr(&nsview).cast_mut().cast() } - } - pub(crate) fn set_enabled(&self, enabled: bool) { - let nsview = self.nsview(); - let Some(nswindow) = nsview.window() else { - return; - }; - - if enabled { - if let Some(attached) = nswindow.attachedSheet() { - nswindow.endSheet(&attached); - } - } else { - if nswindow.attachedSheet().is_some() { - return; - } + pub(crate) fn nsview(&self) -> Retained { + let handle = self + .window + .window_handle() + .expect("failed to get window handle"); + match handle.as_raw() { + RawWindowHandle::AppKit(handle) => unsafe { + Retained::::retain(handle.ns_view.as_ptr().cast::()) + .expect("failed to retain NSView") + }, + other => panic!("expected AppKit window handle, got {other:?}"), + } + } - let Some(mtm) = MainThreadMarker::new() else { - return; - }; - let frame = nswindow.frame(); - let sheet = unsafe { - NSWindow::initWithContentRect_styleMask_backing_defer( - mtm.alloc(), - frame, - NSWindowStyleMask::Titled, - NSBackingStoreType::Buffered, - false, - ) - }; - sheet.setAlphaValue(0.5); - (&*nswindow).beginSheet_completionHandler(&*sheet, None); + pub(crate) fn set_enabled(&self, enabled: bool) { + let nsview = self.nsview(); + let Some(nswindow) = nsview.window() else { + return; + }; + + if enabled { + if let Some(attached) = nswindow.attachedSheet() { + nswindow.endSheet(&attached); + } + } else { + if nswindow.attachedSheet().is_some() { + return; + } + + let Some(mtm) = MainThreadMarker::new() else { + return; + }; + let frame = nswindow.frame(); + let sheet = unsafe { + NSWindow::initWithContentRect_styleMask_backing_defer( + mtm.alloc(), + frame, + NSWindowStyleMask::Titled, + NSBackingStoreType::Buffered, + false, + ) + }; + sheet.setAlphaValue(0.5); + (&*nswindow).beginSheet_completionHandler(&*sheet, None); + } } - } - - pub(crate) fn is_enabled(&self) -> bool { - self - .nsview() - .window() - .map(|nswindow| nswindow.attachedSheet().is_none()) - .unwrap_or(true) - } - - pub(crate) fn set_traffic_light_position(&self, position: &Position) { - let nsview = self.nsview(); - let Some(nswindow) = nsview.window() else { - return; - }; - let pos = position.to_logical::(nswindow.backingScaleFactor()); - if let Ok(mut state) = self.appkit_state.write() { - let pos = NSPoint::new(pos.x, pos.y); - state.traffic_light_position = Some(pos); + pub(crate) fn is_enabled(&self) -> bool { + self.nsview() + .window() + .map(|nswindow| nswindow.attachedSheet().is_none()) + .unwrap_or(true) } - inset_traffic_lights(&nswindow, pos.x, pos.y); - swizzle_draw_rect(&nsview); - } + pub(crate) fn set_traffic_light_position(&self, position: &Position) { + let nsview = self.nsview(); + let Some(nswindow) = nsview.window() else { + return; + }; - pub(crate) fn associate_appkit_state(&self) { - let nsview = self.nsview(); - let Some(nswindow) = nsview.window() else { - return; - }; + let pos = position.to_logical::(nswindow.backingScaleFactor()); + if let Ok(mut state) = self.appkit_state.write() { + let pos = NSPoint::new(pos.x, pos.y); + state.traffic_light_position = Some(pos); + } - AppkitState::associate(&self.appkit_state, &nswindow); - } + inset_traffic_lights(&nswindow, pos.x, pos.y); + swizzle_draw_rect(&nsview); + } - pub(crate) fn set_title_bar_style(&self, style: TitleBarStyle) { - let nsview = self.nsview(); - let Some(nswindow) = nsview.window() else { - return; - }; + pub(crate) fn associate_appkit_state(&self) { + let nsview = self.nsview(); + let Some(nswindow) = nsview.window() else { + return; + }; - match style { - TitleBarStyle::Visible => { - nswindow.setTitlebarAppearsTransparent(false); - let mut mask = nswindow.styleMask(); - mask.remove(NSWindowStyleMask::FullSizeContentView); - nswindow.setStyleMask(mask); - } - TitleBarStyle::Transparent => { - nswindow.setTitlebarAppearsTransparent(true); - let mut mask = nswindow.styleMask(); - mask.remove(NSWindowStyleMask::FullSizeContentView); - nswindow.setStyleMask(mask); - } - TitleBarStyle::Overlay => { - nswindow.setTitlebarAppearsTransparent(true); - let mut mask = nswindow.styleMask(); - mask.insert(NSWindowStyleMask::FullSizeContentView); - nswindow.setStyleMask(mask); - } - _ => {} + AppkitState::associate(&self.appkit_state, &nswindow); } - } - pub(crate) fn set_visible_on_all_workspaces(&self, visible: bool) { - let nsview = self.nsview(); - let Some(nswindow) = nsview.window() else { - return; - }; + pub(crate) fn set_title_bar_style(&self, style: TitleBarStyle) { + let nsview = self.nsview(); + let Some(nswindow) = nsview.window() else { + return; + }; + + match style { + TitleBarStyle::Visible => { + nswindow.setTitlebarAppearsTransparent(false); + let mut mask = nswindow.styleMask(); + mask.remove(NSWindowStyleMask::FullSizeContentView); + nswindow.setStyleMask(mask); + } + TitleBarStyle::Transparent => { + nswindow.setTitlebarAppearsTransparent(true); + let mut mask = nswindow.styleMask(); + mask.remove(NSWindowStyleMask::FullSizeContentView); + nswindow.setStyleMask(mask); + } + TitleBarStyle::Overlay => { + nswindow.setTitlebarAppearsTransparent(true); + let mut mask = nswindow.styleMask(); + mask.insert(NSWindowStyleMask::FullSizeContentView); + nswindow.setStyleMask(mask); + } + _ => {} + } + } - let mut collection_behavior = nswindow.collectionBehavior(); - collection_behavior.set(NSWindowCollectionBehavior::CanJoinAllSpaces, visible); - nswindow.setCollectionBehavior(collection_behavior); - } + pub(crate) fn set_visible_on_all_workspaces(&self, visible: bool) { + let nsview = self.nsview(); + let Some(nswindow) = nsview.window() else { + return; + }; - pub(crate) fn set_background_color(&self, color: Option) { - let nsview = self.nsview(); - let Some(nswindow) = nsview.window() else { - return; - }; + let mut collection_behavior = nswindow.collectionBehavior(); + collection_behavior.set(NSWindowCollectionBehavior::CanJoinAllSpaces, visible); + nswindow.setCollectionBehavior(collection_behavior); + } - let nscolor = color - .map(utils::ns_color_from_tauri_color) - .unwrap_or_else(NSColor::windowBackgroundColor); - nswindow.setOpaque(color.map(|color| color.3 == u8::MAX).unwrap_or(true)); - nswindow.setBackgroundColor(Some(&nscolor)); - } + pub(crate) fn set_background_color(&self, color: Option) { + let nsview = self.nsview(); + let Some(nswindow) = nsview.window() else { + return; + }; + + let nscolor = color + .map(utils::ns_color_from_tauri_color) + .unwrap_or_else(NSColor::windowBackgroundColor); + nswindow.setOpaque(color.map(|color| color.3 == u8::MAX).unwrap_or(true)); + nswindow.setBackgroundColor(Some(&nscolor)); + } } fn inset_traffic_lights(nswindow: &NSWindow, x: f64, y: f64) { - let Some(close) = nswindow.standardWindowButton(NSWindowButton::CloseButton) else { - return; - }; - let Some(miniaturize) = nswindow.standardWindowButton(NSWindowButton::MiniaturizeButton) else { - return; - }; - let Some(zoom) = nswindow.standardWindowButton(NSWindowButton::ZoomButton) else { - return; - }; - - let title_bar_container_view = unsafe { close.superview().and_then(|view| view.superview()) }; - let Some(title_bar_container_view) = title_bar_container_view else { - return; - }; - - let close_rect = close.frame(); - let title_bar_frame_height = close_rect.size.height + y; - let mut title_bar_rect = title_bar_container_view.frame(); - title_bar_rect.size.height = title_bar_frame_height; - title_bar_rect.origin.y = nswindow.frame().size.height - title_bar_frame_height; - title_bar_container_view.setFrame(title_bar_rect); - - let space_between = miniaturize.frame().origin.x - close_rect.origin.x; - for (index, button) in [close, miniaturize, zoom].into_iter().enumerate() { - let mut origin = button.frame().origin; - origin.x = x + (index as f64 * space_between); - button.setFrameOrigin(origin); - } + let Some(close) = nswindow.standardWindowButton(NSWindowButton::CloseButton) else { + return; + }; + let Some(miniaturize) = nswindow.standardWindowButton(NSWindowButton::MiniaturizeButton) else { + return; + }; + let Some(zoom) = nswindow.standardWindowButton(NSWindowButton::ZoomButton) else { + return; + }; + + let title_bar_container_view = unsafe { close.superview().and_then(|view| view.superview()) }; + let Some(title_bar_container_view) = title_bar_container_view else { + return; + }; + + let close_rect = close.frame(); + let title_bar_frame_height = close_rect.size.height + y; + let mut title_bar_rect = title_bar_container_view.frame(); + title_bar_rect.size.height = title_bar_frame_height; + title_bar_rect.origin.y = nswindow.frame().size.height - title_bar_frame_height; + title_bar_container_view.setFrame(title_bar_rect); + + let space_between = miniaturize.frame().origin.x - close_rect.origin.x; + for (index, button) in [close, miniaturize, zoom].into_iter().enumerate() { + let mut origin = button.frame().origin; + origin.x = x + (index as f64 * space_between); + button.setFrameOrigin(origin); + } } type DrawRect = extern "C-unwind" fn(&NSView, Sel, NSRect); static ORIGINAL_DRAW_RECT: MainThreadBound>> = { - // SAFETY: Creating in a `const` context, where there is no concept of the main thread. - MainThreadBound::new(Cell::new(None), unsafe { - MainThreadMarker::new_unchecked() - }) + // SAFETY: Creating in a `const` context, where there is no concept of the main thread. + MainThreadBound::new(Cell::new(None), unsafe { + MainThreadMarker::new_unchecked() + }) }; extern "C-unwind" fn draw_rect(view: &NSView, sel: Sel, rect: NSRect) { - let mtm = MainThreadMarker::from(view); - let original = ORIGINAL_DRAW_RECT - .get(mtm) - .get() - .expect("no existing drawRect: handler set"); + let mtm = MainThreadMarker::from(view); + let original = ORIGINAL_DRAW_RECT + .get(mtm) + .get() + .expect("no existing drawRect: handler set"); - original(view, sel, rect); + original(view, sel, rect); - post_draw_rect(view, rect); + post_draw_rect(view, rect); } fn post_draw_rect(view: &NSView, _rect: NSRect) { - let Some(nswindow) = view.window() else { - return; - }; - - let Some(state) = AppkitState::from_window(&nswindow) else { - return; - }; - let Ok(state) = state.read() else { - return; - }; - - if let Some(pos) = state.traffic_light_position { - inset_traffic_lights(&nswindow, pos.x, pos.y); - } + let Some(nswindow) = view.window() else { + return; + }; + + let Some(state) = AppkitState::from_window(&nswindow) else { + return; + }; + let Ok(state) = state.read() else { + return; + }; + + if let Some(pos) = state.traffic_light_position { + inset_traffic_lights(&nswindow, pos.x, pos.y); + } } fn swizzle_draw_rect(nsview: &NSView) { - let mtm = MainThreadMarker::from(nsview); - let class = nsview.class(); - let Some(method) = class.instance_method(sel!(drawRect:)) else { - return; - }; - - let overridden = unsafe { mem::transmute::(draw_rect) }; - if ptr::fn_addr_eq(overridden, method.implementation()) { - return; - } - - let original = unsafe { method.set_implementation(overridden) }; - let original = unsafe { mem::transmute::(original) }; - ORIGINAL_DRAW_RECT.get(mtm).set(Some(original)); + let mtm = MainThreadMarker::from(nsview); + let class = nsview.class(); + let Some(method) = class.instance_method(sel!(drawRect:)) else { + return; + }; + + let overridden = unsafe { mem::transmute::(draw_rect) }; + if ptr::fn_addr_eq(overridden, method.implementation()) { + return; + } + + let original = unsafe { method.set_implementation(overridden) }; + let original = unsafe { mem::transmute::(original) }; + ORIGINAL_DRAW_RECT.get(mtm).set(Some(original)); } diff --git a/src/platform/mod.rs b/src/platform/mod.rs index ea35617..704956f 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -9,11 +9,11 @@ mod windows; pub mod macos; #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" ))] mod linux; @@ -21,34 +21,34 @@ use tauri_runtime::dpi::PhysicalRect; use winit::monitor::MonitorHandle; pub(crate) trait MonitorExt { - /// Get the work area of this monitor. - /// - /// TODO: upstream work-area support into winit and replace this native shim. - fn work_area(&self) -> PhysicalRect; + /// Get the work area of this monitor. + /// + /// TODO: upstream work-area support into winit and replace this native shim. + fn work_area(&self) -> PhysicalRect; } fn monitor_bounds(monitor: &MonitorHandle) -> PhysicalRect { - PhysicalRect { - position: monitor.position().unwrap_or_default(), - size: monitor - .current_video_mode() - .map(|video_mode| video_mode.size()) - .unwrap_or_default(), - } + PhysicalRect { + position: monitor.position().unwrap_or_default(), + size: monitor + .current_video_mode() + .map(|video_mode| video_mode.size()) + .unwrap_or_default(), + } } pub trait EventLoopExt { - #[cfg(target_os = "macos")] - fn set_activation_policy(&self, policy: tauri_runtime::ActivationPolicy); - #[cfg(target_os = "macos")] - fn set_dock_visibility(&self, visible: bool); - #[cfg(target_os = "macos")] - fn show_application(&self); - #[cfg(target_os = "macos")] - fn hide_application(&self); - #[cfg(target_os = "macos")] - fn set_progress_bar(&self, state: tauri_runtime::ProgressBarState); - fn set_badge_count(&self, count: Option, desktop_filename: Option); - fn set_badge_label(&self, label: Option); - fn cursor_position(&self) -> tauri_runtime::Result>; + #[cfg(target_os = "macos")] + fn set_activation_policy(&self, policy: tauri_runtime::ActivationPolicy); + #[cfg(target_os = "macos")] + fn set_dock_visibility(&self, visible: bool); + #[cfg(target_os = "macos")] + fn show_application(&self); + #[cfg(target_os = "macos")] + fn hide_application(&self); + #[cfg(target_os = "macos")] + fn set_progress_bar(&self, state: tauri_runtime::ProgressBarState); + fn set_badge_count(&self, count: Option, desktop_filename: Option); + fn set_badge_label(&self, label: Option); + fn cursor_position(&self) -> tauri_runtime::Result>; } diff --git a/src/platform/windows/event_loop.rs b/src/platform/windows/event_loop.rs index a3d43c5..723c377 100644 --- a/src/platform/windows/event_loop.rs +++ b/src/platform/windows/event_loop.rs @@ -9,17 +9,17 @@ use winit::event_loop::ActiveEventLoop; use crate::platform::EventLoopExt; impl EventLoopExt for dyn ActiveEventLoop + '_ { - fn set_badge_count(&self, _count: Option, _desktop_filename: Option) { - // Unsupported on Windows - } + fn set_badge_count(&self, _count: Option, _desktop_filename: Option) { + // Unsupported on Windows + } - fn set_badge_label(&self, _label: Option) { - // Unsupported on Windows - } + fn set_badge_label(&self, _label: Option) { + // Unsupported on Windows + } - fn cursor_position(&self) -> Result> { - let mut point = POINT::default(); - unsafe { GetCursorPos(&mut point) }.map_err(|_| Error::FailedToGetCursorPosition)?; - Ok(PhysicalPosition::new(point.x as f64, point.y as f64)) - } + fn cursor_position(&self) -> Result> { + let mut point = POINT::default(); + unsafe { GetCursorPos(&mut point) }.map_err(|_| Error::FailedToGetCursorPosition)?; + Ok(PhysicalPosition::new(point.x as f64, point.y as f64)) + } } diff --git a/src/platform/windows/icon.rs b/src/platform/windows/icon.rs index 4a9b1fc..d77c5d2 100644 --- a/src/platform/windows/icon.rs +++ b/src/platform/windows/icon.rs @@ -6,29 +6,29 @@ use tauri_runtime::Icon; use windows::Win32::UI::WindowsAndMessaging::{CreateIcon, HICON}; pub fn icon_to_hicon(icon: Icon<'static>) -> Option { - let width = icon.width; - let height = icon.height; - let mut rgba = icon.rgba.into_owned(); - if width == 0 || height == 0 || rgba.len() != width as usize * height as usize * 4 { - return None; - } + let width = icon.width; + let height = icon.height; + let mut rgba = icon.rgba.into_owned(); + if width == 0 || height == 0 || rgba.len() != width as usize * height as usize * 4 { + return None; + } - let mut and_mask = Vec::with_capacity(width as usize * height as usize); - for pixel in rgba.chunks_exact_mut(4) { - and_mask.push(pixel[3].wrapping_sub(u8::MAX)); - pixel.swap(0, 2); - } + let mut and_mask = Vec::with_capacity(width as usize * height as usize); + for pixel in rgba.chunks_exact_mut(4) { + and_mask.push(pixel[3].wrapping_sub(u8::MAX)); + pixel.swap(0, 2); + } - unsafe { - CreateIcon( - None, - width as i32, - height as i32, - 1, - 32, - and_mask.as_ptr(), - rgba.as_ptr(), - ) - .ok() - } + unsafe { + CreateIcon( + None, + width as i32, + height as i32, + 1, + 32, + and_mask.as_ptr(), + rgba.as_ptr(), + ) + .ok() + } } diff --git a/src/platform/windows/monitor.rs b/src/platform/windows/monitor.rs index 062e7c9..09f22a0 100644 --- a/src/platform/windows/monitor.rs +++ b/src/platform/windows/monitor.rs @@ -9,24 +9,24 @@ use winit::monitor::MonitorHandle; use crate::platform::{MonitorExt, monitor_bounds}; impl MonitorExt for MonitorHandle { - fn work_area(&self) -> PhysicalRect { - let mut monitor_info = MONITORINFO { - cbSize: std::mem::size_of::() as u32, - ..Default::default() - }; + fn work_area(&self) -> PhysicalRect { + let mut monitor_info = MONITORINFO { + cbSize: std::mem::size_of::() as u32, + ..Default::default() + }; - let hmonitor = HMONITOR(self.native_id() as _); + let hmonitor = HMONITOR(self.native_id() as _); - let status = unsafe { GetMonitorInfoW(hmonitor, &mut monitor_info) }; - if !status.as_bool() { - return monitor_bounds(self); - } + let status = unsafe { GetMonitorInfoW(hmonitor, &mut monitor_info) }; + if !status.as_bool() { + return monitor_bounds(self); + } - let position = PhysicalPosition::new(monitor_info.rcWork.left, monitor_info.rcWork.top); - let size = PhysicalSize::new( - (monitor_info.rcWork.right - monitor_info.rcWork.left) as u32, - (monitor_info.rcWork.bottom - monitor_info.rcWork.top) as u32, - ); - PhysicalRect { position, size } - } + let position = PhysicalPosition::new(monitor_info.rcWork.left, monitor_info.rcWork.top); + let size = PhysicalSize::new( + (monitor_info.rcWork.right - monitor_info.rcWork.left) as u32, + (monitor_info.rcWork.bottom - monitor_info.rcWork.top) as u32, + ); + PhysicalRect { position, size } + } } diff --git a/src/platform/windows/webview.rs b/src/platform/windows/webview.rs index 02e4843..9d698e9 100644 --- a/src/platform/windows/webview.rs +++ b/src/platform/windows/webview.rs @@ -6,87 +6,94 @@ use cef::ImplBrowserHost; use tauri_runtime::dpi::{PhysicalPosition, PhysicalSize, Rect}; use tauri_utils::config::Color; use windows::Win32::{ - Foundation::{HWND, POINT, RECT}, - Graphics::Gdi::MapWindowPoints, - UI::WindowsAndMessaging::{ - DestroyWindow, GetParent, GetWindowRect, SW_HIDE, SW_SHOW, SWP_NOACTIVATE, SWP_NOZORDER, - SetParent, SetWindowPos, ShowWindow, - }, + Foundation::{HWND, POINT, RECT}, + Graphics::Gdi::MapWindowPoints, + UI::WindowsAndMessaging::{ + DestroyWindow, GetParent, GetWindowRect, SW_HIDE, SW_SHOW, SWP_NOACTIVATE, SWP_NOZORDER, + SetParent, SetWindowPos, ShowWindow, + }, }; use crate::{webview::AppWebview, window::AppWindow}; impl AppWebview { - pub(crate) fn hwnd(&self) -> HWND { - let hwnd = self.host.window_handle(); - HWND(hwnd.0 as _) - } + pub(crate) fn hwnd(&self) -> HWND { + let hwnd = self.host.window_handle(); + HWND(hwnd.0 as _) + } - pub(crate) fn set_background_color(&self, _color: Option) { - // TODO: might not be supported on Windows - } + pub(crate) fn set_background_color(&self, _color: Option) { + // TODO: might not be supported on Windows + } - pub(crate) fn bounds(&self) -> Option { - let hwnd = self.hwnd(); + pub(crate) fn bounds(&self) -> Option { + let hwnd = self.hwnd(); - let mut rect = RECT::default(); - unsafe { - let parent = GetParent(hwnd).ok()?; - if parent.0.is_null() { - return None; - } + let mut rect = RECT::default(); + unsafe { + let parent = GetParent(hwnd).ok()?; + if parent.0.is_null() { + return None; + } - GetWindowRect(hwnd, &mut rect).ok()?; + GetWindowRect(hwnd, &mut rect).ok()?; - let mut points = [ - POINT { - x: rect.left, - y: rect.top, - }, - POINT { - x: rect.right, - y: rect.bottom, - }, - ]; - if MapWindowPoints(None, Some(parent), &mut points) == 0 { - return None; - } + let mut points = [ + POINT { + x: rect.left, + y: rect.top, + }, + POINT { + x: rect.right, + y: rect.bottom, + }, + ]; + if MapWindowPoints(None, Some(parent), &mut points) == 0 { + return None; + } - let x = points[0].x; - let y = points[0].y; - let width = (points[1].x - points[0].x).max(0) as u32; - let height = (points[1].y - points[0].y).max(0) as u32; - Some(Rect { - position: PhysicalPosition::new(x, y).into(), - size: PhysicalSize::new(width, height).into(), - }) + let x = points[0].x; + let y = points[0].y; + let width = (points[1].x - points[0].x).max(0) as u32; + let height = (points[1].y - points[0].y).max(0) as u32; + Some(Rect { + position: PhysicalPosition::new(x, y).into(), + size: PhysicalSize::new(width, height).into(), + }) + } } - } - pub(crate) fn reparent(&self, parent: &AppWindow) { - let parent = parent.hwnd(); - let _ = unsafe { SetParent(self.hwnd(), Some(parent)) }; - } + pub(crate) fn reparent(&self, parent: &AppWindow) { + let parent = parent.hwnd(); + let _ = unsafe { SetParent(self.hwnd(), Some(parent)) }; + } - pub(crate) fn apply_visible(&self, visible: bool) { - let _ = unsafe { ShowWindow(self.hwnd(), if visible { SW_SHOW } else { SW_HIDE }) }; - } + pub(crate) fn apply_visible(&self, visible: bool) { + let _ = unsafe { ShowWindow(self.hwnd(), if visible { SW_SHOW } else { SW_HIDE }) }; + } - pub(crate) fn destroy_native(&self) { - let _ = unsafe { DestroyWindow(self.hwnd()) }; - } + pub(crate) fn destroy_native(&self) { + let _ = unsafe { DestroyWindow(self.hwnd()) }; + } - pub(crate) fn apply_physical_bounds(&self, _scale: f64, x: i32, y: i32, width: i32, height: i32) { - unsafe { - let _ = SetWindowPos( - self.hwnd(), - None, - x, - y, - width, - height, - SWP_NOZORDER | SWP_NOACTIVATE, - ); + pub(crate) fn apply_physical_bounds( + &self, + _scale: f64, + x: i32, + y: i32, + width: i32, + height: i32, + ) { + unsafe { + let _ = SetWindowPos( + self.hwnd(), + None, + x, + y, + width, + height, + SWP_NOZORDER | SWP_NOACTIVATE, + ); + } } - } } diff --git a/src/platform/windows/window.rs b/src/platform/windows/window.rs index ab5de61..aceaf2c 100644 --- a/src/platform/windows/window.rs +++ b/src/platform/windows/window.rs @@ -8,17 +8,17 @@ use raw_window_handle::{HasWindowHandle, RawWindowHandle}; use tauri_runtime::{Icon, ProgressBarState, ProgressBarStatus}; use tauri_utils::config::Color; use windows::Win32::{ - Foundation::{HWND, RECT}, - Graphics::Dwm::{DWMWA_EXTENDED_FRAME_BOUNDS, DwmGetWindowAttribute}, - System::Com::{CLSCTX_SERVER, CoCreateInstance}, - UI::{ - Input::KeyboardAndMouse::{EnableWindow, IsWindowEnabled}, - Shell::{ - ITaskbarList3, TBPF_ERROR, TBPF_INDETERMINATE, TBPF_NOPROGRESS, TBPF_NORMAL, TBPF_PAUSED, - TaskbarList, + Foundation::{HWND, RECT}, + Graphics::Dwm::{DWMWA_EXTENDED_FRAME_BOUNDS, DwmGetWindowAttribute}, + System::Com::{CLSCTX_SERVER, CoCreateInstance}, + UI::{ + Input::KeyboardAndMouse::{EnableWindow, IsWindowEnabled}, + Shell::{ + ITaskbarList3, TBPF_ERROR, TBPF_INDETERMINATE, TBPF_NOPROGRESS, TBPF_NORMAL, + TBPF_PAUSED, TaskbarList, + }, + WindowsAndMessaging::DestroyIcon, }, - WindowsAndMessaging::DestroyIcon, - }, }; use crate::{window::AppWindow, window_handle::SoftbufferWindowHandle}; @@ -26,138 +26,139 @@ use crate::{window::AppWindow, window_handle::SoftbufferWindowHandle}; use super::icon::icon_to_hicon; impl AppWindow { - pub(crate) fn raw_cef_handle(&self) -> cef::sys::cef_window_handle_t { - cef::sys::HWND(self.hwnd().0 as *mut _) - } - - pub(crate) fn hwnd(&self) -> HWND { - let handle = self - .window - .window_handle() - .expect("failed to get window handle"); - match handle.as_raw() { - RawWindowHandle::Win32(handle) => HWND(handle.hwnd.get() as _), - other => panic!("expected Win32 window handle, got {other:?}"), + pub(crate) fn raw_cef_handle(&self) -> cef::sys::cef_window_handle_t { + cef::sys::HWND(self.hwnd().0 as *mut _) } - } - - pub(crate) fn is_enabled(&self) -> bool { - unsafe { IsWindowEnabled(self.hwnd()) }.as_bool() - } - - pub(crate) fn set_enabled(&self, enabled: bool) { - let _ = unsafe { EnableWindow(self.hwnd(), enabled) }; - } - - pub(crate) fn set_overlay_icon(&self, icon: Option>) { - let Ok(taskbar) = - (unsafe { CoCreateInstance::<_, ITaskbarList3>(&TaskbarList, None, CLSCTX_SERVER) }) - else { - return; - }; - - let icon = icon.and_then(icon_to_hicon); - let hwnd = self.hwnd(); - - if let Some(icon) = icon { - let _ = unsafe { taskbar.SetOverlayIcon(hwnd, icon, None) }; - let _ = unsafe { DestroyIcon(icon) }; - } else { - let _ = unsafe { taskbar.SetOverlayIcon(hwnd, Default::default(), None) }; + + pub(crate) fn hwnd(&self) -> HWND { + let handle = self + .window + .window_handle() + .expect("failed to get window handle"); + match handle.as_raw() { + RawWindowHandle::Win32(handle) => HWND(handle.hwnd.get() as _), + other => panic!("expected Win32 window handle, got {other:?}"), + } + } + + pub(crate) fn is_enabled(&self) -> bool { + unsafe { IsWindowEnabled(self.hwnd()) }.as_bool() } - } - - pub(crate) fn set_progress_bar(&self, state: ProgressBarState) { - let Ok(taskbar) = - (unsafe { CoCreateInstance::<_, ITaskbarList3>(&TaskbarList, None, CLSCTX_SERVER) }) - else { - return; - }; - - let hwnd = self.hwnd(); - if let Some(status) = state.status { - let flag = match status { - ProgressBarStatus::None => TBPF_NOPROGRESS, - ProgressBarStatus::Normal => TBPF_NORMAL, - ProgressBarStatus::Indeterminate => TBPF_INDETERMINATE, - ProgressBarStatus::Paused => TBPF_PAUSED, - ProgressBarStatus::Error => TBPF_ERROR, - }; - let _ = unsafe { taskbar.SetProgressState(hwnd, flag) }; + + pub(crate) fn set_enabled(&self, enabled: bool) { + let _ = unsafe { EnableWindow(self.hwnd(), enabled) }; } - if let Some(progress) = state.progress { - let _ = unsafe { taskbar.SetProgressValue(hwnd, progress.min(100), 100) }; + pub(crate) fn set_overlay_icon(&self, icon: Option>) { + let Ok(taskbar) = + (unsafe { CoCreateInstance::<_, ITaskbarList3>(&TaskbarList, None, CLSCTX_SERVER) }) + else { + return; + }; + + let icon = icon.and_then(icon_to_hicon); + let hwnd = self.hwnd(); + + if let Some(icon) = icon { + let _ = unsafe { taskbar.SetOverlayIcon(hwnd, icon, None) }; + let _ = unsafe { DestroyIcon(icon) }; + } else { + let _ = unsafe { taskbar.SetOverlayIcon(hwnd, Default::default(), None) }; + } } - } - - pub(crate) fn set_background_color(&mut self, _color: Option) { - // Nothing to do here, the background color is already updated in the window attributes, - // and the background surface will be drawn in the next frame. - // Just request a redraw. - self.window.request_redraw(); - } - - pub(crate) fn draw_background_surface(&mut self) { - if !self.attrs.inner.transparent && self.attrs.background_color.is_none() { - self.background_surface = None; - return; + + pub(crate) fn set_progress_bar(&self, state: ProgressBarState) { + let Ok(taskbar) = + (unsafe { CoCreateInstance::<_, ITaskbarList3>(&TaskbarList, None, CLSCTX_SERVER) }) + else { + return; + }; + + let hwnd = self.hwnd(); + if let Some(status) = state.status { + let flag = match status { + ProgressBarStatus::None => TBPF_NOPROGRESS, + ProgressBarStatus::Normal => TBPF_NORMAL, + ProgressBarStatus::Indeterminate => TBPF_INDETERMINATE, + ProgressBarStatus::Paused => TBPF_PAUSED, + ProgressBarStatus::Error => TBPF_ERROR, + }; + let _ = unsafe { taskbar.SetProgressState(hwnd, flag) }; + } + + if let Some(progress) = state.progress { + let _ = unsafe { taskbar.SetProgressValue(hwnd, progress.min(100), 100) }; + } + } + + pub(crate) fn set_background_color(&mut self, _color: Option) { + // Nothing to do here, the background color is already updated in the window attributes, + // and the background surface will be drawn in the next frame. + // Just request a redraw. + self.window.request_redraw(); } - let size = self.window.surface_size(); - let (Some(width), Some(height)) = (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) - else { - return; - }; - - if self.background_surface.is_none() { - let Some(handle) = SoftbufferWindowHandle::new(self.window.as_ref()) else { - return; - }; - let Ok(context) = softbuffer::Context::new(handle) else { - return; - }; - let Ok(surface) = softbuffer::Surface::new(&context, handle) else { - return; - }; - self.background_surface = Some(surface); + pub(crate) fn draw_background_surface(&mut self) { + if !self.attrs.inner.transparent && self.attrs.background_color.is_none() { + self.background_surface = None; + return; + } + + let size = self.window.surface_size(); + let (Some(width), Some(height)) = + (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) + else { + return; + }; + + if self.background_surface.is_none() { + let Some(handle) = SoftbufferWindowHandle::new(self.window.as_ref()) else { + return; + }; + let Ok(context) = softbuffer::Context::new(handle) else { + return; + }; + let Ok(surface) = softbuffer::Surface::new(&context, handle) else { + return; + }; + self.background_surface = Some(surface); + } + + let Some(surface) = &mut self.background_surface else { + return; + }; + + let color = self + .attrs + .background_color + .map(|Color(r, g, b, _)| (b as u32) | ((g as u32) << 8) | ((r as u32) << 16)) + .unwrap_or(0); + + if surface.resize(width, height).is_ok() + && let Ok(mut buffer) = surface.buffer_mut() + { + buffer.fill(color); + let _ = buffer.present(); + } } - let Some(surface) = &mut self.background_surface else { - return; - }; - - let color = self - .attrs - .background_color - .map(|Color(r, g, b, _)| (b as u32) | ((g as u32) << 8) | ((r as u32) << 16)) - .unwrap_or(0); - - if surface.resize(width, height).is_ok() - && let Ok(mut buffer) = surface.buffer_mut() - { - buffer.fill(color); - let _ = buffer.present(); + /// The visible frame height reported by DWM (`DWMWA_EXTENDED_FRAME_BOUNDS`). + /// + /// winit's `outer_size` includes the invisible resize/shadow border, which + /// throws off vertical centering for decorated windows. The DWM extended + /// frame bounds describe the actually-visible window rectangle, so its height + /// is what should be used when centering. Returns `None` on failure. + pub(crate) fn dwm_visible_frame_height(&self) -> Option { + let mut rect = RECT::default(); + let result = unsafe { + DwmGetWindowAttribute( + self.hwnd(), + DWMWA_EXTENDED_FRAME_BOUNDS, + &mut rect as *mut _ as *mut _, + std::mem::size_of::() as u32, + ) + }; + result.ok()?; + Some((rect.bottom - rect.top) as u32) } - } - - /// The visible frame height reported by DWM (`DWMWA_EXTENDED_FRAME_BOUNDS`). - /// - /// winit's `outer_size` includes the invisible resize/shadow border, which - /// throws off vertical centering for decorated windows. The DWM extended - /// frame bounds describe the actually-visible window rectangle, so its height - /// is what should be used when centering. Returns `None` on failure. - pub(crate) fn dwm_visible_frame_height(&self) -> Option { - let mut rect = RECT::default(); - let result = unsafe { - DwmGetWindowAttribute( - self.hwnd(), - DWMWA_EXTENDED_FRAME_BOUNDS, - &mut rect as *mut _ as *mut _, - std::mem::size_of::() as u32, - ) - }; - result.ok()?; - Some((rect.bottom - rect.top) as u32) - } } diff --git a/src/policy.rs b/src/policy.rs index ff64b45..af6a90a 100644 --- a/src/policy.rs +++ b/src/policy.rs @@ -30,13 +30,13 @@ //! verdict still reaches the audit sink. use std::{ - collections::HashMap, - fmt, - sync::{ - Arc, Mutex, OnceLock, Weak, - atomic::{AtomicBool, AtomicU64, Ordering}, - }, - time::Duration, + collections::HashMap, + fmt, + sync::{ + Arc, Mutex, OnceLock, Weak, + atomic::{AtomicBool, AtomicU64, Ordering}, + }, + time::Duration, }; /// How long a deferred (interactive) request may stay unanswered before it is @@ -51,83 +51,83 @@ pub const DEFAULT_PROMPT_TIMEOUT: Duration = Duration::from_secs(30); /// tell what it would be granting). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum PermissionKind { - Microphone, - Camera, - CameraPanTiltZoom, - /// `getDisplayMedia` — screen/window/tab capture, audio or video. - ScreenCapture, - CapturedSurfaceControl, - /// The async clipboard *read* permission. Clipboard writes are gesture-gated - /// by Chromium and never reach a permission handler. - ClipboardRead, - Geolocation, - Notifications, - MidiSysex, - PointerLock, - KeyboardLock, - IdleDetection, - LocalFonts, - StorageAccess, - TopLevelStorageAccess, - DiskQuota, - ProtectedMediaIdentifier, - RegisterProtocolHandler, - MultipleDownloads, - WindowManagement, - FileSystemAccess, - LocalNetwork, - Sensors, - HandTracking, - IdentityProvider, - WebAppInstallation, - /// AR/VR immersive session. - ImmersiveSession, - /// A CEF permission bit unknown to this crate — carries the raw bit. - Unknown(u32), + Microphone, + Camera, + CameraPanTiltZoom, + /// `getDisplayMedia` — screen/window/tab capture, audio or video. + ScreenCapture, + CapturedSurfaceControl, + /// The async clipboard *read* permission. Clipboard writes are gesture-gated + /// by Chromium and never reach a permission handler. + ClipboardRead, + Geolocation, + Notifications, + MidiSysex, + PointerLock, + KeyboardLock, + IdleDetection, + LocalFonts, + StorageAccess, + TopLevelStorageAccess, + DiskQuota, + ProtectedMediaIdentifier, + RegisterProtocolHandler, + MultipleDownloads, + WindowManagement, + FileSystemAccess, + LocalNetwork, + Sensors, + HandTracking, + IdentityProvider, + WebAppInstallation, + /// AR/VR immersive session. + ImmersiveSession, + /// A CEF permission bit unknown to this crate — carries the raw bit. + Unknown(u32), } impl fmt::Display for PermissionKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Microphone => f.write_str("microphone"), - Self::Camera => f.write_str("camera"), - Self::CameraPanTiltZoom => f.write_str("camera-pan-tilt-zoom"), - Self::ScreenCapture => f.write_str("screen-capture"), - Self::CapturedSurfaceControl => f.write_str("captured-surface-control"), - Self::ClipboardRead => f.write_str("clipboard-read"), - Self::Geolocation => f.write_str("geolocation"), - Self::Notifications => f.write_str("notifications"), - Self::MidiSysex => f.write_str("midi-sysex"), - Self::PointerLock => f.write_str("pointer-lock"), - Self::KeyboardLock => f.write_str("keyboard-lock"), - Self::IdleDetection => f.write_str("idle-detection"), - Self::LocalFonts => f.write_str("local-fonts"), - Self::StorageAccess => f.write_str("storage-access"), - Self::TopLevelStorageAccess => f.write_str("top-level-storage-access"), - Self::DiskQuota => f.write_str("disk-quota"), - Self::ProtectedMediaIdentifier => f.write_str("protected-media-identifier"), - Self::RegisterProtocolHandler => f.write_str("register-protocol-handler"), - Self::MultipleDownloads => f.write_str("multiple-downloads"), - Self::WindowManagement => f.write_str("window-management"), - Self::FileSystemAccess => f.write_str("file-system-access"), - Self::LocalNetwork => f.write_str("local-network"), - Self::Sensors => f.write_str("sensors"), - Self::HandTracking => f.write_str("hand-tracking"), - Self::IdentityProvider => f.write_str("identity-provider"), - Self::WebAppInstallation => f.write_str("web-app-installation"), - Self::ImmersiveSession => f.write_str("immersive-session"), - Self::Unknown(bit) => write!(f, "unknown({bit:#x})"), - } - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Microphone => f.write_str("microphone"), + Self::Camera => f.write_str("camera"), + Self::CameraPanTiltZoom => f.write_str("camera-pan-tilt-zoom"), + Self::ScreenCapture => f.write_str("screen-capture"), + Self::CapturedSurfaceControl => f.write_str("captured-surface-control"), + Self::ClipboardRead => f.write_str("clipboard-read"), + Self::Geolocation => f.write_str("geolocation"), + Self::Notifications => f.write_str("notifications"), + Self::MidiSysex => f.write_str("midi-sysex"), + Self::PointerLock => f.write_str("pointer-lock"), + Self::KeyboardLock => f.write_str("keyboard-lock"), + Self::IdleDetection => f.write_str("idle-detection"), + Self::LocalFonts => f.write_str("local-fonts"), + Self::StorageAccess => f.write_str("storage-access"), + Self::TopLevelStorageAccess => f.write_str("top-level-storage-access"), + Self::DiskQuota => f.write_str("disk-quota"), + Self::ProtectedMediaIdentifier => f.write_str("protected-media-identifier"), + Self::RegisterProtocolHandler => f.write_str("register-protocol-handler"), + Self::MultipleDownloads => f.write_str("multiple-downloads"), + Self::WindowManagement => f.write_str("window-management"), + Self::FileSystemAccess => f.write_str("file-system-access"), + Self::LocalNetwork => f.write_str("local-network"), + Self::Sensors => f.write_str("sensors"), + Self::HandTracking => f.write_str("hand-tracking"), + Self::IdentityProvider => f.write_str("identity-provider"), + Self::WebAppInstallation => f.write_str("web-app-installation"), + Self::ImmersiveSession => f.write_str("immersive-session"), + Self::Unknown(bit) => write!(f, "unknown({bit:#x})"), + } + } } /// Which CEF callback a request arrived on. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RequestSource { - /// `OnRequestMediaAccessPermission` — `getUserMedia`/`getDisplayMedia`. - MediaAccess, - /// `OnShowPermissionPrompt` — a Chromium permission prompt. - Prompt, + /// `OnRequestMediaAccessPermission` — `getUserMedia`/`getDisplayMedia`. + MediaAccess, + /// `OnShowPermissionPrompt` — a Chromium permission prompt. + Prompt, } /// A requesting origin, normalized for policy matching: scheme and host @@ -139,198 +139,198 @@ pub enum RequestSource { /// policy should deny them. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct NormalizedOrigin { - pub scheme: String, - pub host: String, - pub port: Option, + pub scheme: String, + pub host: String, + pub port: Option, } impl NormalizedOrigin { - /// Normalize a serialized origin (`https://example.com:443`). Returns [`None`] - /// for opaque/host-less/malformed origins. - pub fn parse(raw: &str) -> Option { - let url = url::Url::parse(raw).ok()?; - let host = url.host_str()?; - if host.is_empty() { - return None; - } - Some(Self { - scheme: url.scheme().to_ascii_lowercase(), - host: host.to_ascii_lowercase(), - port: url.port_or_known_default(), - }) - } - - /// Whether this origin is served by the app itself rather than remote web - /// content: one of the app's registered custom schemes (see - /// [`crate::CefConfig::custom_schemes`]), or http(s) on a - /// localhost/loopback/`*.localhost` host. - /// - /// This is a *transport* fact, not a trust decision: an app that proxies - /// remote content through a local origin (a gateway, a preview server) must - /// not treat app-local as trusted — distinguish those by webview label. - pub fn is_app_local(&self) -> bool { - if crate::config::config() - .custom_schemes - .iter() - .any(|scheme| scheme == &self.scheme) - { - return true; - } - matches!(self.scheme.as_str(), "http" | "https") - && (self.host == "localhost" - || self.host == "127.0.0.1" - || self.host == "[::1]" - || self.host == "::1" - || self.host.ends_with(".localhost")) - } + /// Normalize a serialized origin (`https://example.com:443`). Returns [`None`] + /// for opaque/host-less/malformed origins. + pub fn parse(raw: &str) -> Option { + let url = url::Url::parse(raw).ok()?; + let host = url.host_str()?; + if host.is_empty() { + return None; + } + Some(Self { + scheme: url.scheme().to_ascii_lowercase(), + host: host.to_ascii_lowercase(), + port: url.port_or_known_default(), + }) + } + + /// Whether this origin is served by the app itself rather than remote web + /// content: one of the app's registered custom schemes (see + /// [`crate::CefConfig::custom_schemes`]), or http(s) on a + /// localhost/loopback/`*.localhost` host. + /// + /// This is a *transport* fact, not a trust decision: an app that proxies + /// remote content through a local origin (a gateway, a preview server) must + /// not treat app-local as trusted — distinguish those by webview label. + pub fn is_app_local(&self) -> bool { + if crate::config::config() + .custom_schemes + .iter() + .any(|scheme| scheme == &self.scheme) + { + return true; + } + matches!(self.scheme.as_str(), "http" | "https") + && (self.host == "localhost" + || self.host == "127.0.0.1" + || self.host == "[::1]" + || self.host == "::1" + || self.host.ends_with(".localhost")) + } } impl fmt::Display for NormalizedOrigin { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}://{}", self.scheme, self.host)?; - if let Some(port) = self.port { - write!(f, ":{port}")?; + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}://{}", self.scheme, self.host)?; + if let Some(port) = self.port { + write!(f, ":{port}")?; + } + Ok(()) } - Ok(()) - } } /// A privileged-permission request awaiting a policy verdict. #[derive(Debug, Clone)] pub struct PermissionRequest { - /// Process-unique id; appears in the matching [`PermissionAudit`]. - pub id: u64, - /// Label of the tauri webview the request came from. - pub webview_label: String, - /// The normalized requesting origin, or [`None`] when the origin is opaque, - /// host-less or malformed (deny those). - pub origin: Option, - /// The origin exactly as CEF reported it (`""` when it reported none). - pub raw_origin: String, - /// Every permission this request asks for, translated from CEF's bitmask. - pub kinds: Vec, - /// Whether the requesting frame is the top-level frame. [`None`] when CEF - /// does not report a frame (permission prompts are browser-scoped). - pub is_main_frame: Option, - pub source: RequestSource, + /// Process-unique id; appears in the matching [`PermissionAudit`]. + pub id: u64, + /// Label of the tauri webview the request came from. + pub webview_label: String, + /// The normalized requesting origin, or [`None`] when the origin is opaque, + /// host-less or malformed (deny those). + pub origin: Option, + /// The origin exactly as CEF reported it (`""` when it reported none). + pub raw_origin: String, + /// Every permission this request asks for, translated from CEF's bitmask. + pub kinds: Vec, + /// Whether the requesting frame is the top-level frame. [`None`] when CEF + /// does not report a frame (permission prompts are browser-scoped). + pub is_main_frame: Option, + pub source: RequestSource, } /// A per-kind verdict from the policy. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Verdict { - Allow, - Deny, + Allow, + Deny, } /// Why a request was denied. Recorded in the [`PermissionAudit`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DenyReason { - /// No policy was configured — the crate default. - NoPolicy, - /// The policy denied it. - PolicyDenied, - /// The policy asked the user, who said no. - UserDenied, - /// The origin was opaque, host-less or malformed. - InvalidOrigin, - /// A permission this build cannot reason about. - UnsupportedPermission, - /// Not every requested kind was allowed — CEF cannot grant a subset. - PartialGrantRefused, - /// A deferred request was not answered within its timeout. - RequestExpired, - /// The webview went away while the request was pending. - BrowserClosed, - /// The policy dropped the responder without deciding — a policy bug, denied - /// so it cannot become an accidental grant. - ProviderDropped, + /// No policy was configured — the crate default. + NoPolicy, + /// The policy denied it. + PolicyDenied, + /// The policy asked the user, who said no. + UserDenied, + /// The origin was opaque, host-less or malformed. + InvalidOrigin, + /// A permission this build cannot reason about. + UnsupportedPermission, + /// Not every requested kind was allowed — CEF cannot grant a subset. + PartialGrantRefused, + /// A deferred request was not answered within its timeout. + RequestExpired, + /// The webview went away while the request was pending. + BrowserClosed, + /// The policy dropped the responder without deciding — a policy bug, denied + /// so it cannot become an accidental grant. + ProviderDropped, } impl fmt::Display for DenyReason { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let reason = match self { - Self::NoPolicy => "no-policy", - Self::PolicyDenied => "policy-denied", - Self::UserDenied => "user-denied", - Self::InvalidOrigin => "invalid-origin", - Self::UnsupportedPermission => "unsupported-permission", - Self::PartialGrantRefused => "partial-grant-refused", - Self::RequestExpired => "request-expired", - Self::BrowserClosed => "browser-closed", - Self::ProviderDropped => "provider-dropped", - }; - f.write_str(reason) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let reason = match self { + Self::NoPolicy => "no-policy", + Self::PolicyDenied => "policy-denied", + Self::UserDenied => "user-denied", + Self::InvalidOrigin => "invalid-origin", + Self::UnsupportedPermission => "unsupported-permission", + Self::PartialGrantRefused => "partial-grant-refused", + Self::RequestExpired => "request-expired", + Self::BrowserClosed => "browser-closed", + Self::ProviderDropped => "provider-dropped", + }; + f.write_str(reason) + } } /// The final, enforced outcome of a request: exactly what CEF was told. #[derive(Debug, Clone)] pub struct PermissionAudit { - pub request_id: u64, - pub webview_label: String, - pub origin: Option, - pub raw_origin: String, - pub kinds: Vec, - /// Per-kind verdicts when the policy decided; empty when it never got that - /// far (timeout, browser close, no policy). - pub verdicts: Vec, - pub granted: bool, - /// Set whenever `granted` is false. - pub reason: Option, - pub source: RequestSource, + pub request_id: u64, + pub webview_label: String, + pub origin: Option, + pub raw_origin: String, + pub kinds: Vec, + /// Per-kind verdicts when the policy decided; empty when it never got that + /// far (timeout, browser close, no policy). + pub verdicts: Vec, + pub granted: bool, + /// Set whenever `granted` is false. + pub reason: Option, + pub source: RequestSource, } /// Shared state of one in-flight request. Completing it is idempotent: the /// first completion wins and answers CEF, every later one is a no-op. struct Pending { - request: PermissionRequest, - done: AtomicBool, - complete: Box, + request: PermissionRequest, + done: AtomicBool, + complete: Box, } impl Pending { - /// Answer CEF and emit the audit event. Returns false if already answered. - fn finish(&self, granted: bool, verdicts: Vec, reason: Option) -> bool { - if self - .done - .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) - .is_err() - { - return false; - } - (self.complete)(granted); - let audit = PermissionAudit { - request_id: self.request.id, - webview_label: self.request.webview_label.clone(), - origin: self.request.origin.clone(), - raw_origin: self.request.raw_origin.clone(), - kinds: self.request.kinds.clone(), - verdicts, - granted, - reason, - source: self.request.source, - }; - if granted { - log::info!( - "granted {:?} to {} in webview {:?}", - audit.kinds, - audit.raw_origin, - audit.webview_label - ); - } else { - log::info!( - "denied {:?} ({}) to {} in webview {:?}", - audit.kinds, - reason.map(|r| r.to_string()).unwrap_or_default(), - audit.raw_origin, - audit.webview_label - ); - } - if let Some(sink) = AUDIT_SINK.get() { - sink(&audit); + /// Answer CEF and emit the audit event. Returns false if already answered. + fn finish(&self, granted: bool, verdicts: Vec, reason: Option) -> bool { + if self + .done + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + .is_err() + { + return false; + } + (self.complete)(granted); + let audit = PermissionAudit { + request_id: self.request.id, + webview_label: self.request.webview_label.clone(), + origin: self.request.origin.clone(), + raw_origin: self.request.raw_origin.clone(), + kinds: self.request.kinds.clone(), + verdicts, + granted, + reason, + source: self.request.source, + }; + if granted { + log::info!( + "granted {:?} to {} in webview {:?}", + audit.kinds, + audit.raw_origin, + audit.webview_label + ); + } else { + log::info!( + "denied {:?} ({}) to {} in webview {:?}", + audit.kinds, + reason.map(|r| r.to_string()).unwrap_or_default(), + audit.raw_origin, + audit.webview_label + ); + } + if let Some(sink) = AUDIT_SINK.get() { + sink(&audit); + } + true } - true - } } /// The policy's handle on one request. It must be consumed — dropping it @@ -340,102 +340,102 @@ impl Pending { /// decision off the CEF thread with [`defer`](Self::defer) and answer later. #[must_use = "dropping a responder without deciding denies the request"] pub struct PermissionResponder { - pending: Option>, + pending: Option>, } impl PermissionResponder { - /// Allow every requested permission. - pub fn allow(mut self) { - let pending = self.pending.take().expect("responder consumed once"); - let verdicts = vec![Verdict::Allow; pending.request.kinds.len()]; - pending.finish(true, verdicts, None); - } - - /// Deny the whole request. - pub fn deny(mut self, reason: DenyReason) { - let pending = self.pending.take().expect("responder consumed once"); - let verdicts = vec![Verdict::Deny; pending.request.kinds.len()]; - pending.finish(false, verdicts, Some(reason)); - } - - /// Answer each requested kind separately: `verdicts[i]` is the verdict for - /// `request.kinds[i]`. - /// - /// CEF cannot grant a subset (see the module docs), so the request is - /// granted only when every verdict is [`Verdict::Allow`]; a mixed verdict - /// denies the request with [`DenyReason::PartialGrantRefused`] while the - /// per-kind verdicts still reach the audit sink. A length mismatch is a - /// policy bug and denies. - pub fn decide(mut self, verdicts: Vec) { - let pending = self.pending.take().expect("responder consumed once"); - if verdicts.len() != pending.request.kinds.len() { - log::error!( - "permission policy returned {} verdicts for {} kinds — denying", - verdicts.len(), - pending.request.kinds.len() - ); - let denied = vec![Verdict::Deny; pending.request.kinds.len()]; - pending.finish(false, denied, Some(DenyReason::ProviderDropped)); - return; - } - match refusal(&verdicts) { - None => { + /// Allow every requested permission. + pub fn allow(mut self) { + let pending = self.pending.take().expect("responder consumed once"); + let verdicts = vec![Verdict::Allow; pending.request.kinds.len()]; pending.finish(true, verdicts, None); - } - Some(reason) => { + } + + /// Deny the whole request. + pub fn deny(mut self, reason: DenyReason) { + let pending = self.pending.take().expect("responder consumed once"); + let verdicts = vec![Verdict::Deny; pending.request.kinds.len()]; pending.finish(false, verdicts, Some(reason)); - } - } - } - - /// Take the decision off the CEF thread — for a native consent prompt. - /// - /// The returned handle can be answered from any thread. If it is not - /// answered within `timeout`, the request is denied - /// ([`DenyReason::RequestExpired`]); it is also denied if the webview closes - /// first, or if the handle is dropped. - pub fn defer(mut self, timeout: Duration) -> DeferredResponder { - let pending = self.pending.take().expect("responder consumed once"); - register_pending(&pending); - // ponytail: one timer thread per interactive prompt — prompts are rare and - // short-lived. Move to a shared timer wheel if that stops being true. - let timer = Arc::downgrade(&pending); - std::thread::spawn(move || { - std::thread::sleep(timeout); - if let Some(pending) = timer.upgrade() { - let denied = vec![Verdict::Deny; pending.request.kinds.len()]; - pending.finish(false, denied, Some(DenyReason::RequestExpired)); - } - }); - DeferredResponder { pending } - } + } + + /// Answer each requested kind separately: `verdicts[i]` is the verdict for + /// `request.kinds[i]`. + /// + /// CEF cannot grant a subset (see the module docs), so the request is + /// granted only when every verdict is [`Verdict::Allow`]; a mixed verdict + /// denies the request with [`DenyReason::PartialGrantRefused`] while the + /// per-kind verdicts still reach the audit sink. A length mismatch is a + /// policy bug and denies. + pub fn decide(mut self, verdicts: Vec) { + let pending = self.pending.take().expect("responder consumed once"); + if verdicts.len() != pending.request.kinds.len() { + log::error!( + "permission policy returned {} verdicts for {} kinds — denying", + verdicts.len(), + pending.request.kinds.len() + ); + let denied = vec![Verdict::Deny; pending.request.kinds.len()]; + pending.finish(false, denied, Some(DenyReason::ProviderDropped)); + return; + } + match refusal(&verdicts) { + None => { + pending.finish(true, verdicts, None); + } + Some(reason) => { + pending.finish(false, verdicts, Some(reason)); + } + } + } + + /// Take the decision off the CEF thread — for a native consent prompt. + /// + /// The returned handle can be answered from any thread. If it is not + /// answered within `timeout`, the request is denied + /// ([`DenyReason::RequestExpired`]); it is also denied if the webview closes + /// first, or if the handle is dropped. + pub fn defer(mut self, timeout: Duration) -> DeferredResponder { + let pending = self.pending.take().expect("responder consumed once"); + register_pending(&pending); + // ponytail: one timer thread per interactive prompt — prompts are rare and + // short-lived. Move to a shared timer wheel if that stops being true. + let timer = Arc::downgrade(&pending); + std::thread::spawn(move || { + std::thread::sleep(timeout); + if let Some(pending) = timer.upgrade() { + let denied = vec![Verdict::Deny; pending.request.kinds.len()]; + pending.finish(false, denied, Some(DenyReason::RequestExpired)); + } + }); + DeferredResponder { pending } + } } /// Why a set of per-kind verdicts denies the request, or [`None`] when every /// kind was allowed and the request is granted whole. fn refusal(verdicts: &[Verdict]) -> Option { - if verdicts.iter().all(|verdict| *verdict == Verdict::Allow) { - None - } else if verdicts.contains(&Verdict::Allow) { - // Some allowed, some not: CEF has no way to grant the subset. - Some(DenyReason::PartialGrantRefused) - } else { - // Nothing was allowed — an ordinary denial, not a subset CEF refused. - Some(DenyReason::PolicyDenied) - } + if verdicts.iter().all(|verdict| *verdict == Verdict::Allow) { + None + } else if verdicts.contains(&Verdict::Allow) { + // Some allowed, some not: CEF has no way to grant the subset. + Some(DenyReason::PartialGrantRefused) + } else { + // Nothing was allowed — an ordinary denial, not a subset CEF refused. + Some(DenyReason::PolicyDenied) + } } impl Drop for PermissionResponder { - fn drop(&mut self) { - if let Some(pending) = self.pending.take() { - log::error!( - "permission policy dropped request {} without deciding — denying", - pending.request.id - ); - let denied = vec![Verdict::Deny; pending.request.kinds.len()]; - pending.finish(false, denied, Some(DenyReason::ProviderDropped)); + fn drop(&mut self) { + if let Some(pending) = self.pending.take() { + log::error!( + "permission policy dropped request {} without deciding — denying", + pending.request.id + ); + let denied = vec![Verdict::Deny; pending.request.kinds.len()]; + pending.finish(false, denied, Some(DenyReason::ProviderDropped)); + } } - } } /// A request whose verdict is being decided off the CEF thread (a native @@ -443,43 +443,43 @@ impl Drop for PermissionResponder { /// timeout, a closing webview, or a previous answer — is a harmless no-op that /// returns `false`. pub struct DeferredResponder { - pending: Arc, + pending: Arc, } impl DeferredResponder { - pub fn request_id(&self) -> u64 { - self.pending.request.id - } + pub fn request_id(&self) -> u64 { + self.pending.request.id + } - pub fn request(&self) -> &PermissionRequest { - &self.pending.request - } + pub fn request(&self) -> &PermissionRequest { + &self.pending.request + } - /// Whether this request is still awaiting an answer. - pub fn is_live(&self) -> bool { - !self.pending.done.load(Ordering::SeqCst) - } + /// Whether this request is still awaiting an answer. + pub fn is_live(&self) -> bool { + !self.pending.done.load(Ordering::SeqCst) + } - /// Allow every requested permission. Returns whether this call was the one - /// that answered CEF. - pub fn allow(&self) -> bool { - let verdicts = vec![Verdict::Allow; self.pending.request.kinds.len()]; - self.pending.finish(true, verdicts, None) - } + /// Allow every requested permission. Returns whether this call was the one + /// that answered CEF. + pub fn allow(&self) -> bool { + let verdicts = vec![Verdict::Allow; self.pending.request.kinds.len()]; + self.pending.finish(true, verdicts, None) + } - /// Deny the request. Returns whether this call was the one that answered CEF. - pub fn deny(&self, reason: DenyReason) -> bool { - let verdicts = vec![Verdict::Deny; self.pending.request.kinds.len()]; - self.pending.finish(false, verdicts, Some(reason)) - } + /// Deny the request. Returns whether this call was the one that answered CEF. + pub fn deny(&self, reason: DenyReason) -> bool { + let verdicts = vec![Verdict::Deny; self.pending.request.kinds.len()]; + self.pending.finish(false, verdicts, Some(reason)) + } } impl Drop for DeferredResponder { - fn drop(&mut self) { - if self.is_live() { - self.deny(DenyReason::ProviderDropped); + fn drop(&mut self) { + if self.is_live() { + self.deny(DenyReason::ProviderDropped); + } } - } } type PermissionPolicy = dyn Fn(PermissionRequest, PermissionResponder) + Send + Sync; @@ -498,170 +498,260 @@ static PENDING: Mutex>>>> = Mutex::new( /// The policy runs on a CEF thread: it must not block. To ask the user, call /// [`PermissionResponder::defer`] and answer from the app's event loop. pub fn set_permission_policy( - policy: impl Fn(PermissionRequest, PermissionResponder) + Send + Sync + 'static, + policy: impl Fn(PermissionRequest, PermissionResponder) + Send + Sync + 'static, ) { - let _ = PERMISSION_POLICY.set(Box::new(policy)); + let _ = PERMISSION_POLICY.set(Box::new(policy)); } /// Sets the sink for permission audit events — one per *final* decision, /// emitted after CEF has been answered. Call before `tauri::Builder::run`. pub fn set_permission_audit(sink: impl Fn(&PermissionAudit) + Send + Sync + 'static) { - let _ = AUDIT_SINK.set(Box::new(sink)); + let _ = AUDIT_SINK.set(Box::new(sink)); } fn register_pending(pending: &Arc) { - let mut guard = PENDING.lock().expect("permission registry poisoned"); - let registry = guard.get_or_insert_with(HashMap::new); - let entries = registry - .entry(pending.request.webview_label.clone()) - .or_default(); - entries.retain(|entry| entry.upgrade().is_some_and(|entry| !entry.done.load(Ordering::SeqCst))); - entries.push(Arc::downgrade(pending)); + let mut guard = PENDING.lock().expect("permission registry poisoned"); + let registry = guard.get_or_insert_with(HashMap::new); + let entries = registry + .entry(pending.request.webview_label.clone()) + .or_default(); + entries.retain(|entry| { + entry + .upgrade() + .is_some_and(|entry| !entry.done.load(Ordering::SeqCst)) + }); + entries.push(Arc::downgrade(pending)); } /// Deny every request still pending for `label` — its webview is going away. pub(crate) fn cancel_pending(label: &str) { - let entries = { - let mut guard = PENDING.lock().expect("permission registry poisoned"); - guard.as_mut().and_then(|registry| registry.remove(label)) - }; - for entry in entries.into_iter().flatten() { - if let Some(pending) = entry.upgrade() { - let denied = vec![Verdict::Deny; pending.request.kinds.len()]; - pending.finish(false, denied, Some(DenyReason::BrowserClosed)); + let entries = { + let mut guard = PENDING.lock().expect("permission registry poisoned"); + guard.as_mut().and_then(|registry| registry.remove(label)) + }; + for entry in entries.into_iter().flatten() { + if let Some(pending) = entry.upgrade() { + let denied = vec![Verdict::Deny; pending.request.kinds.len()]; + pending.finish(false, denied, Some(DenyReason::BrowserClosed)); + } } - } } /// Route one CEF permission request through the policy. `complete` answers the /// CEF callback: `true` grants exactly the requested set, `false` grants /// nothing. pub(crate) fn dispatch( - webview_label: &str, - raw_origin: &str, - source: RequestSource, - kinds: Vec, - is_main_frame: Option, - complete: impl Fn(bool) + Send + Sync + 'static, + webview_label: &str, + raw_origin: &str, + source: RequestSource, + kinds: Vec, + is_main_frame: Option, + complete: impl Fn(bool) + Send + Sync + 'static, ) { - let request = PermissionRequest { - id: NEXT_REQUEST_ID.fetch_add(1, Ordering::Relaxed), - webview_label: webview_label.to_string(), - origin: NormalizedOrigin::parse(raw_origin), - raw_origin: raw_origin.to_string(), - kinds, - is_main_frame, - source, - }; - let pending = Arc::new(Pending { - request: request.clone(), - done: AtomicBool::new(false), - complete: Box::new(complete), - }); - let responder = PermissionResponder { - pending: Some(pending), - }; - match PERMISSION_POLICY.get() { - Some(policy) => policy(request, responder), - None => responder.deny(DenyReason::NoPolicy), - } + let request = PermissionRequest { + id: NEXT_REQUEST_ID.fetch_add(1, Ordering::Relaxed), + webview_label: webview_label.to_string(), + origin: NormalizedOrigin::parse(raw_origin), + raw_origin: raw_origin.to_string(), + kinds, + is_main_frame, + source, + }; + let pending = Arc::new(Pending { + request: request.clone(), + done: AtomicBool::new(false), + complete: Box::new(complete), + }); + let responder = PermissionResponder { + pending: Some(pending), + }; + match PERMISSION_POLICY.get() { + Some(policy) => policy(request, responder), + None => responder.deny(DenyReason::NoPolicy), + } } /// CEF media-access bits → [`PermissionKind`]s (deduplicated: desktop audio and /// video capture are both [`PermissionKind::ScreenCapture`]). pub(crate) fn media_kinds(mask: u32) -> Vec { - use cef::sys::cef_media_access_permission_types_t as bits; - let table = [ - ( - bits::CEF_MEDIA_PERMISSION_DEVICE_AUDIO_CAPTURE as u32, - PermissionKind::Microphone, - ), - ( - bits::CEF_MEDIA_PERMISSION_DEVICE_VIDEO_CAPTURE as u32, - PermissionKind::Camera, - ), - ( - bits::CEF_MEDIA_PERMISSION_DESKTOP_AUDIO_CAPTURE as u32, - PermissionKind::ScreenCapture, - ), - ( - bits::CEF_MEDIA_PERMISSION_DESKTOP_VIDEO_CAPTURE as u32, - PermissionKind::ScreenCapture, - ), - ]; - kinds_from_mask(mask, &table) + use cef::sys::cef_media_access_permission_types_t as bits; + let table = [ + ( + bits::CEF_MEDIA_PERMISSION_DEVICE_AUDIO_CAPTURE as u32, + PermissionKind::Microphone, + ), + ( + bits::CEF_MEDIA_PERMISSION_DEVICE_VIDEO_CAPTURE as u32, + PermissionKind::Camera, + ), + ( + bits::CEF_MEDIA_PERMISSION_DESKTOP_AUDIO_CAPTURE as u32, + PermissionKind::ScreenCapture, + ), + ( + bits::CEF_MEDIA_PERMISSION_DESKTOP_VIDEO_CAPTURE as u32, + PermissionKind::ScreenCapture, + ), + ]; + kinds_from_mask(mask, &table) } /// CEF permission-prompt bits → [`PermissionKind`]s. pub(crate) fn prompt_kinds(mask: u32) -> Vec { - use cef::sys::cef_permission_request_types_t as bits; - let table = [ - (bits::CEF_PERMISSION_TYPE_AR_SESSION as u32, PermissionKind::ImmersiveSession), - (bits::CEF_PERMISSION_TYPE_VR_SESSION as u32, PermissionKind::ImmersiveSession), - (bits::CEF_PERMISSION_TYPE_CAMERA_PAN_TILT_ZOOM as u32, PermissionKind::CameraPanTiltZoom), - (bits::CEF_PERMISSION_TYPE_CAMERA_STREAM as u32, PermissionKind::Camera), - (bits::CEF_PERMISSION_TYPE_MIC_STREAM as u32, PermissionKind::Microphone), - (bits::CEF_PERMISSION_TYPE_CAPTURED_SURFACE_CONTROL as u32, PermissionKind::CapturedSurfaceControl), - (bits::CEF_PERMISSION_TYPE_CLIPBOARD as u32, PermissionKind::ClipboardRead), - (bits::CEF_PERMISSION_TYPE_TOP_LEVEL_STORAGE_ACCESS as u32, PermissionKind::TopLevelStorageAccess), - (bits::CEF_PERMISSION_TYPE_DISK_QUOTA as u32, PermissionKind::DiskQuota), - (bits::CEF_PERMISSION_TYPE_LOCAL_FONTS as u32, PermissionKind::LocalFonts), - (bits::CEF_PERMISSION_TYPE_GEOLOCATION as u32, PermissionKind::Geolocation), - (bits::CEF_PERMISSION_TYPE_HAND_TRACKING as u32, PermissionKind::HandTracking), - (bits::CEF_PERMISSION_TYPE_IDENTITY_PROVIDER as u32, PermissionKind::IdentityProvider), - (bits::CEF_PERMISSION_TYPE_IDLE_DETECTION as u32, PermissionKind::IdleDetection), - (bits::CEF_PERMISSION_TYPE_MIDI_SYSEX as u32, PermissionKind::MidiSysex), - (bits::CEF_PERMISSION_TYPE_MULTIPLE_DOWNLOADS as u32, PermissionKind::MultipleDownloads), - (bits::CEF_PERMISSION_TYPE_NOTIFICATIONS as u32, PermissionKind::Notifications), - (bits::CEF_PERMISSION_TYPE_KEYBOARD_LOCK as u32, PermissionKind::KeyboardLock), - (bits::CEF_PERMISSION_TYPE_POINTER_LOCK as u32, PermissionKind::PointerLock), - (bits::CEF_PERMISSION_TYPE_PROTECTED_MEDIA_IDENTIFIER as u32, PermissionKind::ProtectedMediaIdentifier), - (bits::CEF_PERMISSION_TYPE_REGISTER_PROTOCOL_HANDLER as u32, PermissionKind::RegisterProtocolHandler), - (bits::CEF_PERMISSION_TYPE_STORAGE_ACCESS as u32, PermissionKind::StorageAccess), - (bits::CEF_PERMISSION_TYPE_WEB_APP_INSTALLATION as u32, PermissionKind::WebAppInstallation), - (bits::CEF_PERMISSION_TYPE_WINDOW_MANAGEMENT as u32, PermissionKind::WindowManagement), - (bits::CEF_PERMISSION_TYPE_FILE_SYSTEM_ACCESS as u32, PermissionKind::FileSystemAccess), - (bits::CEF_PERMISSION_TYPE_LOCAL_NETWORK_ACCESS as u32, PermissionKind::LocalNetwork), - (bits::CEF_PERMISSION_TYPE_LOCAL_NETWORK as u32, PermissionKind::LocalNetwork), - (bits::CEF_PERMISSION_TYPE_LOOPBACK_NETWORK as u32, PermissionKind::LocalNetwork), - (bits::CEF_PERMISSION_TYPE_SENSORS as u32, PermissionKind::Sensors), - ]; - kinds_from_mask(mask, &table) + use cef::sys::cef_permission_request_types_t as bits; + let table = [ + ( + bits::CEF_PERMISSION_TYPE_AR_SESSION as u32, + PermissionKind::ImmersiveSession, + ), + ( + bits::CEF_PERMISSION_TYPE_VR_SESSION as u32, + PermissionKind::ImmersiveSession, + ), + ( + bits::CEF_PERMISSION_TYPE_CAMERA_PAN_TILT_ZOOM as u32, + PermissionKind::CameraPanTiltZoom, + ), + ( + bits::CEF_PERMISSION_TYPE_CAMERA_STREAM as u32, + PermissionKind::Camera, + ), + ( + bits::CEF_PERMISSION_TYPE_MIC_STREAM as u32, + PermissionKind::Microphone, + ), + ( + bits::CEF_PERMISSION_TYPE_CAPTURED_SURFACE_CONTROL as u32, + PermissionKind::CapturedSurfaceControl, + ), + ( + bits::CEF_PERMISSION_TYPE_CLIPBOARD as u32, + PermissionKind::ClipboardRead, + ), + ( + bits::CEF_PERMISSION_TYPE_TOP_LEVEL_STORAGE_ACCESS as u32, + PermissionKind::TopLevelStorageAccess, + ), + ( + bits::CEF_PERMISSION_TYPE_DISK_QUOTA as u32, + PermissionKind::DiskQuota, + ), + ( + bits::CEF_PERMISSION_TYPE_LOCAL_FONTS as u32, + PermissionKind::LocalFonts, + ), + ( + bits::CEF_PERMISSION_TYPE_GEOLOCATION as u32, + PermissionKind::Geolocation, + ), + ( + bits::CEF_PERMISSION_TYPE_HAND_TRACKING as u32, + PermissionKind::HandTracking, + ), + ( + bits::CEF_PERMISSION_TYPE_IDENTITY_PROVIDER as u32, + PermissionKind::IdentityProvider, + ), + ( + bits::CEF_PERMISSION_TYPE_IDLE_DETECTION as u32, + PermissionKind::IdleDetection, + ), + ( + bits::CEF_PERMISSION_TYPE_MIDI_SYSEX as u32, + PermissionKind::MidiSysex, + ), + ( + bits::CEF_PERMISSION_TYPE_MULTIPLE_DOWNLOADS as u32, + PermissionKind::MultipleDownloads, + ), + ( + bits::CEF_PERMISSION_TYPE_NOTIFICATIONS as u32, + PermissionKind::Notifications, + ), + ( + bits::CEF_PERMISSION_TYPE_KEYBOARD_LOCK as u32, + PermissionKind::KeyboardLock, + ), + ( + bits::CEF_PERMISSION_TYPE_POINTER_LOCK as u32, + PermissionKind::PointerLock, + ), + ( + bits::CEF_PERMISSION_TYPE_PROTECTED_MEDIA_IDENTIFIER as u32, + PermissionKind::ProtectedMediaIdentifier, + ), + ( + bits::CEF_PERMISSION_TYPE_REGISTER_PROTOCOL_HANDLER as u32, + PermissionKind::RegisterProtocolHandler, + ), + ( + bits::CEF_PERMISSION_TYPE_STORAGE_ACCESS as u32, + PermissionKind::StorageAccess, + ), + ( + bits::CEF_PERMISSION_TYPE_WEB_APP_INSTALLATION as u32, + PermissionKind::WebAppInstallation, + ), + ( + bits::CEF_PERMISSION_TYPE_WINDOW_MANAGEMENT as u32, + PermissionKind::WindowManagement, + ), + ( + bits::CEF_PERMISSION_TYPE_FILE_SYSTEM_ACCESS as u32, + PermissionKind::FileSystemAccess, + ), + ( + bits::CEF_PERMISSION_TYPE_LOCAL_NETWORK as u32, + PermissionKind::LocalNetwork, + ), + ( + bits::CEF_PERMISSION_TYPE_LOOPBACK_NETWORK as u32, + PermissionKind::LocalNetwork, + ), + ( + bits::CEF_PERMISSION_TYPE_SENSORS as u32, + PermissionKind::Sensors, + ), + ]; + kinds_from_mask(mask, &table) } /// Translate a bitmask, deduplicating kinds and turning every bit the table /// does not name into [`PermissionKind::Unknown`] — so a policy denying /// unknown kinds keeps denying permissions added by a future Chromium. fn kinds_from_mask(mask: u32, table: &[(u32, PermissionKind)]) -> Vec { - let mut kinds: Vec = Vec::new(); - let mut push = |kind: PermissionKind| { - if !kinds.contains(&kind) { - kinds.push(kind); - } - }; - let known: u32 = table.iter().map(|(bit, _)| *bit).fold(0, |acc, bit| acc | bit); - for (bit, kind) in table { - if mask & bit != 0 { - push(*kind); - } - } - let mut unknown = mask & !known; - while unknown != 0 { - let bit = unknown & unknown.wrapping_neg(); - push(PermissionKind::Unknown(bit)); - unknown &= !bit; - } - kinds + let mut kinds: Vec = Vec::new(); + let mut push = |kind: PermissionKind| { + if !kinds.contains(&kind) { + kinds.push(kind); + } + }; + let known: u32 = table + .iter() + .map(|(bit, _)| *bit) + .fold(0, |acc, bit| acc | bit); + for (bit, kind) in table { + if mask & bit != 0 { + push(*kind); + } + } + let mut unknown = mask & !known; + while unknown != 0 { + let bit = unknown & unknown.wrapping_neg(); + push(PermissionKind::Unknown(bit)); + unknown &= !bit; + } + kinds } /// A `window.open` / popup request routed through the policy set by /// [`set_popup_policy`]. #[derive(Debug)] pub struct PopupRequest<'a> { - /// Label of the tauri webview that initiated the popup. - pub webview_label: &'a str, - /// The URL the popup wants to open. Empty if CEF reports none. - pub url: &'a str, + /// Label of the tauri webview that initiated the popup. + pub webview_label: &'a str, + /// The URL the popup wants to open. Empty if CEF reports none. + pub url: &'a str, } type PopupPolicy = dyn Fn(&PopupRequest<'_>) -> bool + Send + Sync; @@ -673,205 +763,215 @@ static POPUP_POLICY: OnceLock> = OnceLock::new(); /// `on_new_window` handler deny popups and all others allow them — see /// `life_span.rs` for why the tauri handler itself cannot be consulted. pub fn set_popup_policy(policy: impl Fn(&PopupRequest<'_>) -> bool + Send + Sync + 'static) { - let _ = POPUP_POLICY.set(Box::new(policy)); + let _ = POPUP_POLICY.set(Box::new(policy)); } pub(crate) fn popup_allowed(request: &PopupRequest<'_>) -> Option { - POPUP_POLICY.get().map(|policy| policy(request)) + POPUP_POLICY.get().map(|policy| policy(request)) } #[cfg(test)] mod tests { - use super::*; - use std::sync::mpsc; + use super::*; + use std::sync::mpsc; + + /// A request wired to a recording sink instead of a CEF callback: `outcomes` + /// receives one `bool` per completion that actually reached CEF. + fn pending(kinds: Vec) -> (PermissionResponder, mpsc::Receiver) { + let (tx, rx) = mpsc::channel(); + let request = PermissionRequest { + id: NEXT_REQUEST_ID.fetch_add(1, Ordering::Relaxed), + webview_label: "test".into(), + origin: NormalizedOrigin::parse("https://example.com"), + raw_origin: "https://example.com".into(), + kinds, + is_main_frame: Some(true), + source: RequestSource::MediaAccess, + }; + let responder = PermissionResponder { + pending: Some(Arc::new(Pending { + request, + done: AtomicBool::new(false), + complete: Box::new(move |granted| { + let _ = tx.send(granted); + }), + })), + }; + (responder, rx) + } - /// A request wired to a recording sink instead of a CEF callback: `outcomes` - /// receives one `bool` per completion that actually reached CEF. - fn pending(kinds: Vec) -> (PermissionResponder, mpsc::Receiver) { - let (tx, rx) = mpsc::channel(); - let request = PermissionRequest { - id: NEXT_REQUEST_ID.fetch_add(1, Ordering::Relaxed), - webview_label: "test".into(), - origin: NormalizedOrigin::parse("https://example.com"), - raw_origin: "https://example.com".into(), - kinds, - is_main_frame: Some(true), - source: RequestSource::MediaAccess, - }; - let responder = PermissionResponder { - pending: Some(Arc::new(Pending { - request, - done: AtomicBool::new(false), - complete: Box::new(move |granted| { - let _ = tx.send(granted); - }), - })), - }; - (responder, rx) - } - - #[test] - fn allow_grants_and_deny_refuses() { - let (responder, granted) = pending(vec![PermissionKind::Microphone]); - responder.allow(); - assert_eq!(granted.try_recv(), Ok(true)); - - let (responder, granted) = pending(vec![PermissionKind::Camera]); - responder.deny(DenyReason::PolicyDenied); - assert_eq!(granted.try_recv(), Ok(false)); - } - - #[test] - fn a_dropped_responder_denies() { - let (responder, granted) = pending(vec![PermissionKind::Camera]); - drop(responder); - assert_eq!( - granted.try_recv(), - Ok(false), - "a policy that drops the responder must not grant" - ); - } - - #[test] - fn a_partial_verdict_denies_the_whole_request() { - let (responder, granted) = pending(vec![PermissionKind::Microphone, PermissionKind::Camera]); - responder.decide(vec![Verdict::Allow, Verdict::Deny]); - assert_eq!( - granted.try_recv(), - Ok(false), - "CEF cannot grant a subset — a mixed verdict denies" - ); - - let (responder, granted) = pending(vec![PermissionKind::Microphone, PermissionKind::Camera]); - responder.decide(vec![Verdict::Allow, Verdict::Allow]); - assert_eq!(granted.try_recv(), Ok(true)); - } - - #[test] - fn the_audit_reason_tells_a_plain_no_from_a_refused_subset() { - assert_eq!(refusal(&[Verdict::Allow, Verdict::Allow]), None); - assert_eq!( - refusal(&[Verdict::Deny, Verdict::Deny]), - Some(DenyReason::PolicyDenied), - "nothing was allowed — an ordinary denial, not a subset CEF refused" - ); - assert_eq!( - refusal(&[Verdict::Allow, Verdict::Deny]), - Some(DenyReason::PartialGrantRefused) - ); - } - - #[test] - fn a_verdict_of_the_wrong_length_denies() { - let (responder, granted) = pending(vec![PermissionKind::Microphone, PermissionKind::Camera]); - responder.decide(vec![Verdict::Allow]); - assert_eq!(granted.try_recv(), Ok(false)); - } - - #[test] - fn cef_is_answered_exactly_once() { - let (responder, granted) = pending(vec![PermissionKind::Microphone]); - let deferred = responder.defer(Duration::from_secs(60)); - assert!(deferred.allow(), "first answer wins"); - assert!(!deferred.deny(DenyReason::UserDenied), "second is a no-op"); - assert!(!deferred.allow()); - drop(deferred); - assert_eq!(granted.try_recv(), Ok(true)); - assert!( - granted.try_recv().is_err(), - "CEF must be answered exactly once" - ); - } - - #[test] - fn a_deferred_request_times_out_into_a_denial() { - let (responder, granted) = pending(vec![PermissionKind::Camera]); - let deferred = responder.defer(Duration::from_millis(50)); - assert_eq!( - granted.recv_timeout(Duration::from_secs(5)), - Ok(false), - "an unanswered prompt must deny" - ); - assert!(!deferred.is_live()); - assert!(!deferred.allow(), "a timed-out request cannot be granted"); - } - - #[test] - fn a_dropped_deferred_responder_denies() { - let (responder, granted) = pending(vec![PermissionKind::Camera]); - drop(responder.defer(Duration::from_secs(60))); - assert_eq!(granted.try_recv(), Ok(false)); - } - - #[test] - fn a_closing_webview_denies_its_pending_requests() { - let (responder, granted) = pending(vec![PermissionKind::Camera]); - let deferred = responder.defer(Duration::from_secs(60)); - cancel_pending("test"); - assert_eq!(granted.try_recv(), Ok(false)); - assert!(!deferred.is_live()); - assert!(!deferred.allow(), "a closed browser cannot be granted to"); - } - - #[test] - fn origins_normalize_and_opaque_ones_do_not() { - let origin = NormalizedOrigin::parse("HTTPS://Example.COM").expect("normalizes"); - assert_eq!(origin.scheme, "https"); - assert_eq!(origin.host, "example.com"); - assert_eq!(origin.port, Some(443), "default port is resolved"); - assert_eq!(origin.to_string(), "https://example.com:443"); - - assert_ne!( - NormalizedOrigin::parse("https://example.com"), - NormalizedOrigin::parse("http://example.com"), - "scheme distinguishes origins on the same host" - ); - - for opaque in ["null", "", "about:blank", "data:text/html,x", "file:///etc/passwd", "not a url"] { - assert!( - NormalizedOrigin::parse(opaque).is_none(), - "{opaque} must not normalize" - ); - } - } - - #[test] - fn masks_translate_and_unknown_bits_stay_unknown() { - use cef::sys::cef_media_access_permission_types_t as media; - let mask = media::CEF_MEDIA_PERMISSION_DEVICE_AUDIO_CAPTURE as u32 - | media::CEF_MEDIA_PERMISSION_DEVICE_VIDEO_CAPTURE as u32; - assert_eq!( - media_kinds(mask), - vec![PermissionKind::Microphone, PermissionKind::Camera] - ); - - let desktop = media::CEF_MEDIA_PERMISSION_DESKTOP_AUDIO_CAPTURE as u32 - | media::CEF_MEDIA_PERMISSION_DESKTOP_VIDEO_CAPTURE as u32; - assert_eq!( - media_kinds(desktop), - vec![PermissionKind::ScreenCapture], - "desktop audio+video is one screen-capture grant" - ); - - use cef::sys::cef_permission_request_types_t as prompt; - assert_eq!( - prompt_kinds(prompt::CEF_PERMISSION_TYPE_GEOLOCATION as u32), - vec![PermissionKind::Geolocation] - ); - assert_eq!( - prompt_kinds(prompt::CEF_PERMISSION_TYPE_CLIPBOARD as u32), - vec![PermissionKind::ClipboardRead] - ); - - let future_bit = 1u32 << 31; - assert_eq!( - prompt_kinds(prompt::CEF_PERMISSION_TYPE_NOTIFICATIONS as u32 | future_bit), - vec![ - PermissionKind::Notifications, - PermissionKind::Unknown(future_bit) - ], - "a permission this build does not know must surface as Unknown, not vanish" - ); - assert!(media_kinds(0).is_empty()); - } + #[test] + fn allow_grants_and_deny_refuses() { + let (responder, granted) = pending(vec![PermissionKind::Microphone]); + responder.allow(); + assert_eq!(granted.try_recv(), Ok(true)); + + let (responder, granted) = pending(vec![PermissionKind::Camera]); + responder.deny(DenyReason::PolicyDenied); + assert_eq!(granted.try_recv(), Ok(false)); + } + + #[test] + fn a_dropped_responder_denies() { + let (responder, granted) = pending(vec![PermissionKind::Camera]); + drop(responder); + assert_eq!( + granted.try_recv(), + Ok(false), + "a policy that drops the responder must not grant" + ); + } + + #[test] + fn a_partial_verdict_denies_the_whole_request() { + let (responder, granted) = + pending(vec![PermissionKind::Microphone, PermissionKind::Camera]); + responder.decide(vec![Verdict::Allow, Verdict::Deny]); + assert_eq!( + granted.try_recv(), + Ok(false), + "CEF cannot grant a subset — a mixed verdict denies" + ); + + let (responder, granted) = + pending(vec![PermissionKind::Microphone, PermissionKind::Camera]); + responder.decide(vec![Verdict::Allow, Verdict::Allow]); + assert_eq!(granted.try_recv(), Ok(true)); + } + + #[test] + fn the_audit_reason_tells_a_plain_no_from_a_refused_subset() { + assert_eq!(refusal(&[Verdict::Allow, Verdict::Allow]), None); + assert_eq!( + refusal(&[Verdict::Deny, Verdict::Deny]), + Some(DenyReason::PolicyDenied), + "nothing was allowed — an ordinary denial, not a subset CEF refused" + ); + assert_eq!( + refusal(&[Verdict::Allow, Verdict::Deny]), + Some(DenyReason::PartialGrantRefused) + ); + } + + #[test] + fn a_verdict_of_the_wrong_length_denies() { + let (responder, granted) = + pending(vec![PermissionKind::Microphone, PermissionKind::Camera]); + responder.decide(vec![Verdict::Allow]); + assert_eq!(granted.try_recv(), Ok(false)); + } + + #[test] + fn cef_is_answered_exactly_once() { + let (responder, granted) = pending(vec![PermissionKind::Microphone]); + let deferred = responder.defer(Duration::from_secs(60)); + assert!(deferred.allow(), "first answer wins"); + assert!(!deferred.deny(DenyReason::UserDenied), "second is a no-op"); + assert!(!deferred.allow()); + drop(deferred); + assert_eq!(granted.try_recv(), Ok(true)); + assert!( + granted.try_recv().is_err(), + "CEF must be answered exactly once" + ); + } + + #[test] + fn a_deferred_request_times_out_into_a_denial() { + let (responder, granted) = pending(vec![PermissionKind::Camera]); + let deferred = responder.defer(Duration::from_millis(50)); + assert_eq!( + granted.recv_timeout(Duration::from_secs(5)), + Ok(false), + "an unanswered prompt must deny" + ); + assert!(!deferred.is_live()); + assert!(!deferred.allow(), "a timed-out request cannot be granted"); + } + + #[test] + fn a_dropped_deferred_responder_denies() { + let (responder, granted) = pending(vec![PermissionKind::Camera]); + drop(responder.defer(Duration::from_secs(60))); + assert_eq!(granted.try_recv(), Ok(false)); + } + + #[test] + fn a_closing_webview_denies_its_pending_requests() { + let (responder, granted) = pending(vec![PermissionKind::Camera]); + let deferred = responder.defer(Duration::from_secs(60)); + cancel_pending("test"); + assert_eq!(granted.try_recv(), Ok(false)); + assert!(!deferred.is_live()); + assert!(!deferred.allow(), "a closed browser cannot be granted to"); + } + + #[test] + fn origins_normalize_and_opaque_ones_do_not() { + let origin = NormalizedOrigin::parse("HTTPS://Example.COM").expect("normalizes"); + assert_eq!(origin.scheme, "https"); + assert_eq!(origin.host, "example.com"); + assert_eq!(origin.port, Some(443), "default port is resolved"); + assert_eq!(origin.to_string(), "https://example.com:443"); + + assert_ne!( + NormalizedOrigin::parse("https://example.com"), + NormalizedOrigin::parse("http://example.com"), + "scheme distinguishes origins on the same host" + ); + + for opaque in [ + "null", + "", + "about:blank", + "data:text/html,x", + "file:///etc/passwd", + "not a url", + ] { + assert!( + NormalizedOrigin::parse(opaque).is_none(), + "{opaque} must not normalize" + ); + } + } + + #[test] + fn masks_translate_and_unknown_bits_stay_unknown() { + use cef::sys::cef_media_access_permission_types_t as media; + let mask = media::CEF_MEDIA_PERMISSION_DEVICE_AUDIO_CAPTURE as u32 + | media::CEF_MEDIA_PERMISSION_DEVICE_VIDEO_CAPTURE as u32; + assert_eq!( + media_kinds(mask), + vec![PermissionKind::Microphone, PermissionKind::Camera] + ); + + let desktop = media::CEF_MEDIA_PERMISSION_DESKTOP_AUDIO_CAPTURE as u32 + | media::CEF_MEDIA_PERMISSION_DESKTOP_VIDEO_CAPTURE as u32; + assert_eq!( + media_kinds(desktop), + vec![PermissionKind::ScreenCapture], + "desktop audio+video is one screen-capture grant" + ); + + use cef::sys::cef_permission_request_types_t as prompt; + assert_eq!( + prompt_kinds(prompt::CEF_PERMISSION_TYPE_GEOLOCATION as u32), + vec![PermissionKind::Geolocation] + ); + assert_eq!( + prompt_kinds(prompt::CEF_PERMISSION_TYPE_CLIPBOARD as u32), + vec![PermissionKind::ClipboardRead] + ); + + let future_bit = 1u32 << 31; + assert_eq!( + prompt_kinds(prompt::CEF_PERMISSION_TYPE_NOTIFICATIONS as u32 | future_bit), + vec![ + PermissionKind::Notifications, + PermissionKind::Unknown(future_bit) + ], + "a permission this build does not know must surface as Unknown, not vanish" + ); + assert!(media_kinds(0).is_empty()); + } } diff --git a/src/runtime.rs b/src/runtime.rs index 9393647..3cfdddd 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -6,63 +6,62 @@ #![allow(clippy::too_many_arguments)] use std::{ - collections::HashMap, - fmt, - fs::create_dir_all, - path::PathBuf, - sync::{ - Arc, Mutex, - atomic::{AtomicBool, AtomicPtr, AtomicU32, Ordering}, - mpsc::{self, Receiver, Sender}, - }, - time::Duration, + collections::HashMap, + fmt, + fs::create_dir_all, + path::PathBuf, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, AtomicPtr, AtomicU32, Ordering}, + mpsc::{self, Receiver, Sender}, + }, + time::Duration, }; use cef::*; use raw_window_handle::{DisplayHandle, HasDisplayHandle}; use tauri_runtime::{ - DeviceEventFilter, Error, EventLoopProxy, ExitRequestedEventAction, Result, RunEvent, Runtime, - RuntimeHandle, RuntimeInitArgs, UserEvent, - dpi::PhysicalPosition, - monitor::Monitor, - webview::{DetachedWebview, PendingWebview}, - window::{ - DetachedWindow, DragDropEvent, PendingWindow, RawWindow, WebviewEvent, WindowEvent, WindowId, - }, + DeviceEventFilter, Error, EventLoopProxy, ExitRequestedEventAction, Result, RunEvent, Runtime, + RuntimeHandle, RuntimeInitArgs, UserEvent, + dpi::PhysicalPosition, + monitor::Monitor, + webview::{DetachedWebview, PendingWebview}, + window::{ + DetachedWindow, DragDropEvent, PendingWindow, RawWindow, WebviewEvent, WindowEvent, + WindowId, + }, }; use tauri_utils::Theme; use winit::{ - application::ApplicationHandler, - event::{StartCause, WindowEvent as WinitWindowEvent}, - event_loop::{ - ActiveEventLoop, EventLoop, EventLoopBuilder, EventLoopProxy as WinitEventLoopProxy, - }, - window::WindowId as WinitWindowId, + application::ApplicationHandler, + event::{StartCause, WindowEvent as WinitWindowEvent}, + event_loop::{ + ActiveEventLoop, EventLoop, EventLoopBuilder, EventLoopProxy as WinitEventLoopProxy, + }, + window::WindowId as WinitWindowId, }; use crate::external_message_pump::CefExternalPump; use crate::platform::EventLoopExt; use crate::{ - cef_impl::{client as browser_client, ipc, request_handler}, - webview::{self, AppWebview, CefWebviewDispatcher, WebviewMessage, - create_webview_detached, - }, - window::{ - AppWindow, CefWindowDispatcher, WindowMessage, create_window_detached, - winit_monitor_to_tauri_monitor, winit_theme_to_tauri_theme, - }, - window_handle::SendRawDisplayHandle, + cef_impl::{client as browser_client, ipc, request_handler}, + webview::{self, AppWebview, CefWebviewDispatcher, WebviewMessage, create_webview_detached}, + window::{ + AppWindow, CefWindowDispatcher, WindowMessage, create_window_detached, + winit_monitor_to_tauri_monitor, winit_theme_to_tauri_theme, + }, + window_handle::SendRawDisplayHandle, }; #[cfg(target_os = "macos")] use winit::platform::macos::EventLoopBuilderExtMacOS; #[cfg(windows)] use winit::platform::windows::EventLoopBuilderExtWindows; #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" ))] use winit::platform::x11::EventLoopBuilderExtX11; @@ -77,31 +76,31 @@ pub use cef; #[derive(Clone, Debug)] pub struct EventProxy { - context: RuntimeContext, + context: RuntimeContext, } impl EventLoopProxy for EventProxy { - fn send_event(&self, event: T) -> Result<()> { - self.context.send_message(Message::UserEvent(event)) - } + fn send_event(&self, event: T) -> Result<()> { + self.context.send_message(Message::UserEvent(event)) + } } #[derive(Clone)] pub(crate) struct RuntimeContext { - pub(crate) sender: Sender>, - pub(crate) proxy: WinitEventLoopProxy, - main_thread_id: std::thread::ThreadId, - next_window_id: Arc, - next_webview_id: Arc, - next_window_event_id: Arc, - next_webview_event_id: Arc, - current_dispatch: Arc>, - pub(crate) app_wide_theme: Arc>>, - pub(crate) cef_pump: CefExternalPump, - /// Root cache path passed to [`cef::Settings::cache_path`] during - /// [`cef::initialize`]. Per-webview `data_directory` profiles must resolve - /// under this root for CEF request contexts to be accepted. - pub(crate) cache_path: Arc, + pub(crate) sender: Sender>, + pub(crate) proxy: WinitEventLoopProxy, + main_thread_id: std::thread::ThreadId, + next_window_id: Arc, + next_webview_id: Arc, + next_window_event_id: Arc, + next_webview_event_id: Arc, + current_dispatch: Arc>, + pub(crate) app_wide_theme: Arc>>, + pub(crate) cef_pump: CefExternalPump, + /// Root cache path passed to [`cef::Settings::cache_path`] during + /// [`cef::initialize`]. Per-webview `data_directory` profiles must resolve + /// under this root for CEF request contexts to be accepted. + pub(crate) cache_path: Arc, } /// Scoped access to the current winit callback state. @@ -115,831 +114,835 @@ pub(crate) struct RuntimeContext { /// treated as valid beyond their callback. #[derive(Clone, Copy)] struct MainThreadDispatch { - app: *mut WinitCefApp, - event_loop: *const dyn ActiveEventLoop, + app: *mut WinitCefApp, + event_loop: *const dyn ActiveEventLoop, } struct MainThreadDispatchSlot { - current: AtomicPtr>, + current: AtomicPtr>, } impl MainThreadDispatchSlot { - fn install(&self, dispatch: &mut MainThreadDispatch) -> *mut MainThreadDispatch { - self.current.swap(dispatch, Ordering::AcqRel) - } + fn install(&self, dispatch: &mut MainThreadDispatch) -> *mut MainThreadDispatch { + self.current.swap(dispatch, Ordering::AcqRel) + } - fn restore(&self, current: *mut MainThreadDispatch, previous: *mut MainThreadDispatch) { - let installed = self.current.swap(previous, Ordering::AcqRel); - debug_assert_eq!(installed, current); - } + fn restore(&self, current: *mut MainThreadDispatch, previous: *mut MainThreadDispatch) { + let installed = self.current.swap(previous, Ordering::AcqRel); + debug_assert_eq!(installed, current); + } - fn current(&self) -> Option<&MainThreadDispatch> { - let current = self.current.load(Ordering::Acquire); - if current.is_null() { - None - } else { - // SAFETY: the pointer targets the boxed dispatch state owned by - // `MainThreadDispatchGuard`, whose allocation remains stable while the - // guard is moved. The slot is restored before that state is dropped, and - // it is only read by `send_message` after verifying that it is running on - // the runtime main thread. - Some(unsafe { &*current }) + fn current(&self) -> Option<&MainThreadDispatch> { + let current = self.current.load(Ordering::Acquire); + if current.is_null() { + None + } else { + // SAFETY: the pointer targets the boxed dispatch state owned by + // `MainThreadDispatchGuard`, whose allocation remains stable while the + // guard is moved. The slot is restored before that state is dropped, and + // it is only read by `send_message` after verifying that it is running on + // the runtime main thread. + Some(unsafe { &*current }) + } } - } } impl Default for MainThreadDispatchSlot { - fn default() -> Self { - Self { - current: AtomicPtr::new(std::ptr::null_mut()), + fn default() -> Self { + Self { + current: AtomicPtr::new(std::ptr::null_mut()), + } } - } } struct MainThreadDispatchGuard { - context: RuntimeContext, - dispatch: Box>, - previous: *mut MainThreadDispatch, + context: RuntimeContext, + dispatch: Box>, + previous: *mut MainThreadDispatch, } impl Drop for MainThreadDispatchGuard { - fn drop(&mut self) { - self - .context - .current_dispatch - .restore(self.dispatch.as_mut(), self.previous); - } + fn drop(&mut self) { + self.context + .current_dispatch + .restore(self.dispatch.as_mut(), self.previous); + } } #[allow(clippy::result_large_err)] fn handle_main_thread_message( - context: &RuntimeContext, - message: Message, + context: &RuntimeContext, + message: Message, ) -> std::result::Result<(), Message> { - let Some(dispatch) = context.current_dispatch.current() else { - return Err(message); - }; + let Some(dispatch) = context.current_dispatch.current() else { + return Err(message); + }; - // SAFETY: `WinitCefApp::install_current_dispatch` stores pointers to the currently - // executing winit application handler and event-loop callback. This function - // is only called on the runtime main thread while that callback is active. - let app = unsafe { &mut *dispatch.app }; - let event_loop = unsafe { &*dispatch.event_loop }; + // SAFETY: `WinitCefApp::install_current_dispatch` stores pointers to the currently + // executing winit application handler and event-loop callback. This function + // is only called on the runtime main thread while that callback is active. + let app = unsafe { &mut *dispatch.app }; + let event_loop = unsafe { &*dispatch.event_loop }; - app.handle_message(event_loop, message); + app.handle_message(event_loop, message); - Ok(()) + Ok(()) } impl fmt::Debug for RuntimeContext { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("RuntimeContext").finish() - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RuntimeContext").finish() + } } impl RuntimeContext { - pub(crate) fn send_message(&self, message: Message) -> Result<()> { - let message = if self.is_main_thread() { - match handle_main_thread_message(self, message) { - Ok(()) => return Ok(()), - Err(message) => message, - } - } else { - message - }; - - self - .sender - .send(message) - .map_err(|_| Error::FailedToSendMessage)?; - self.proxy.wake_up(); - Ok(()) - } + pub(crate) fn send_message(&self, message: Message) -> Result<()> { + let message = if self.is_main_thread() { + match handle_main_thread_message(self, message) { + Ok(()) => return Ok(()), + Err(message) => message, + } + } else { + message + }; + + self.sender + .send(message) + .map_err(|_| Error::FailedToSendMessage)?; + self.proxy.wake_up(); + Ok(()) + } - pub(crate) fn is_main_thread(&self) -> bool { - std::thread::current().id() == self.main_thread_id - } + pub(crate) fn is_main_thread(&self) -> bool { + std::thread::current().id() == self.main_thread_id + } - /// Run `f` on the main (event-loop) thread. - /// - /// When called from the main thread we execute `f` inline instead of posting - /// it to the channel. Tauri implements several blocking getters as - /// `run_on_main_thread(|| { .. tx.send(..) }); rx.recv()` (e.g. - /// `Window::add_child`). Those run during `setup`, which the runtime drives - /// from winit's `can_create_surfaces`; posting the closure instead of running - /// it inline would deadlock because the loop cannot drain the task while the - /// main thread blocks on `rx.recv()`. - pub(crate) fn run_on_main_thread(&self, f: F) -> Result<()> { - if self.is_main_thread() { - f(); - Ok(()) - } else { - self.send_message(Message::Task(Box::new(f))) + /// Run `f` on the main (event-loop) thread. + /// + /// When called from the main thread we execute `f` inline instead of posting + /// it to the channel. Tauri implements several blocking getters as + /// `run_on_main_thread(|| { .. tx.send(..) }); rx.recv()` (e.g. + /// `Window::add_child`). Those run during `setup`, which the runtime drives + /// from winit's `can_create_surfaces`; posting the closure instead of running + /// it inline would deadlock because the loop cannot drain the task while the + /// main thread blocks on `rx.recv()`. + pub(crate) fn run_on_main_thread(&self, f: F) -> Result<()> { + if self.is_main_thread() { + f(); + Ok(()) + } else { + self.send_message(Message::Task(Box::new(f))) + } } - } - pub(crate) fn next_window_id(&self) -> WindowId { - self.next_window_id.fetch_add(1, Ordering::Relaxed).into() - } + pub(crate) fn next_window_id(&self) -> WindowId { + self.next_window_id.fetch_add(1, Ordering::Relaxed).into() + } - pub(crate) fn next_webview_id(&self) -> u32 { - self.next_webview_id.fetch_add(1, Ordering::Relaxed) - } + pub(crate) fn next_webview_id(&self) -> u32 { + self.next_webview_id.fetch_add(1, Ordering::Relaxed) + } - pub(crate) fn next_window_event_id(&self) -> u32 { - self.next_window_event_id.fetch_add(1, Ordering::Relaxed) - } + pub(crate) fn next_window_event_id(&self) -> u32 { + self.next_window_event_id.fetch_add(1, Ordering::Relaxed) + } - pub(crate) fn next_webview_event_id(&self) -> u32 { - self.next_webview_event_id.fetch_add(1, Ordering::Relaxed) - } + pub(crate) fn next_webview_event_id(&self) -> u32 { + self.next_webview_event_id.fetch_add(1, Ordering::Relaxed) + } } pub(crate) type AfterWindowCreationCallback = Box Fn(RawWindow<'a>) + Send>; pub(crate) enum Message { - EventLoop(EventLoopMessage), - BrowserClosed(WindowId, u32), - Opened(Vec), - #[cfg(target_os = "macos")] - Reopen { - has_visible_windows: bool, - }, - #[cfg(target_os = "macos")] - AccessibilityChanged { - enabled: bool, - }, - CreateWindow { - window_id: WindowId, - webview_id: Option, - pending: Box>>, - after_window_creation: Option, - result_tx: Sender>, - }, - CreateWebview { - window_id: WindowId, - webview_id: u32, - pending: Box>>, - result_tx: Sender>, - }, - Window { - window_id: WindowId, - message: WindowMessage, - }, - Webview { - window_id: WindowId, - webview_id: u32, - message: WebviewMessage, - }, - DragDropScriptEvent { - window_id: WindowId, - webview_id: u32, - target: browser_client::DragDropEventTarget, - drag_drop_state: Arc>, - event: browser_client::DragDropScriptEvent, - }, - Task(Box), - RequestExit(i32), - UserEvent(T), + EventLoop(EventLoopMessage), + BrowserClosed(WindowId, u32), + Opened(Vec), + #[cfg(target_os = "macos")] + Reopen { + has_visible_windows: bool, + }, + #[cfg(target_os = "macos")] + AccessibilityChanged { + enabled: bool, + }, + CreateWindow { + window_id: WindowId, + webview_id: Option, + pending: Box>>, + after_window_creation: Option, + result_tx: Sender>, + }, + CreateWebview { + window_id: WindowId, + webview_id: u32, + pending: Box>>, + result_tx: Sender>, + }, + Window { + window_id: WindowId, + message: WindowMessage, + }, + Webview { + window_id: WindowId, + webview_id: u32, + message: WebviewMessage, + }, + DragDropScriptEvent { + window_id: WindowId, + webview_id: u32, + target: browser_client::DragDropEventTarget, + drag_drop_state: Arc>, + event: browser_client::DragDropScriptEvent, + }, + Task(Box), + RequestExit(i32), + UserEvent(T), } fn device_event_filter_to_winit(filter: DeviceEventFilter) -> winit::event_loop::DeviceEvents { - match filter { - DeviceEventFilter::Always => winit::event_loop::DeviceEvents::Never, - DeviceEventFilter::Unfocused => winit::event_loop::DeviceEvents::WhenFocused, - DeviceEventFilter::Never => winit::event_loop::DeviceEvents::Always, - } + match filter { + DeviceEventFilter::Always => winit::event_loop::DeviceEvents::Never, + DeviceEventFilter::Unfocused => winit::event_loop::DeviceEvents::WhenFocused, + DeviceEventFilter::Never => winit::event_loop::DeviceEvents::Always, + } } pub(crate) enum EventLoopMessage { - SetTheme(Option), - SetDeviceEventFilter(DeviceEventFilter), - PrimaryMonitor(Sender>), - MonitorFromPoint(Sender>, f64, f64), - AvailableMonitors(Sender>), - CursorPosition(Sender>>), - DisplayHandle(Sender>), - #[cfg(target_os = "macos")] - SetActivationPolicy(tauri_runtime::ActivationPolicy), - #[cfg(target_os = "macos")] - SetDockVisibility(bool), - #[cfg(target_os = "macos")] - ShowApplication, - #[cfg(target_os = "macos")] - HideApplication, + SetTheme(Option), + SetDeviceEventFilter(DeviceEventFilter), + PrimaryMonitor(Sender>), + MonitorFromPoint(Sender>, f64, f64), + AvailableMonitors(Sender>), + CursorPosition(Sender>>), + DisplayHandle( + Sender>, + ), + #[cfg(target_os = "macos")] + SetActivationPolicy(tauri_runtime::ActivationPolicy), + #[cfg(target_os = "macos")] + SetDockVisibility(bool), + #[cfg(target_os = "macos")] + ShowApplication, + #[cfg(target_os = "macos")] + HideApplication, } macro_rules! event_loop_getter { - ($self:ident, $variant:ident) => {{ - let (tx, rx) = mpsc::channel(); - match $self - .context - .send_message(Message::EventLoop(EventLoopMessage::$variant(tx))) - { - Ok(()) => rx.recv().map_err(|_| Error::FailedToReceiveMessage), - Err(error) => Err(error), - } - }}; + ($self:ident, $variant:ident) => {{ + let (tx, rx) = mpsc::channel(); + match $self + .context + .send_message(Message::EventLoop(EventLoopMessage::$variant(tx))) + { + Ok(()) => rx.recv().map_err(|_| Error::FailedToReceiveMessage), + Err(error) => Err(error), + } + }}; } fn find_monitor_from_point( - monitors: impl Iterator, - x: f64, - y: f64, + monitors: impl Iterator, + x: f64, + y: f64, ) -> Option { - monitors.into_iter().find(|monitor| { - let pos = monitor.position().unwrap_or_default(); - let size = monitor - .current_video_mode() - .map(|mode| mode.size()) - .unwrap_or_default(); - x >= pos.x as f64 - && x <= pos.x as f64 + size.width as f64 - && y >= pos.y as f64 - && y <= pos.y as f64 + size.height as f64 - }) + monitors.into_iter().find(|monitor| { + let pos = monitor.position().unwrap_or_default(); + let size = monitor + .current_video_mode() + .map(|mode| mode.size()) + .unwrap_or_default(); + x >= pos.x as f64 + && x <= pos.x as f64 + size.width as f64 + && y >= pos.y as f64 + && y <= pos.y as f64 + size.height as f64 + }) } #[cfg(target_os = "macos")] fn is_cef_helper_process() -> bool { - const HELPER_SUFFIXES: &[&str] = &[ - " Helper (GPU)", - " Helper (Renderer)", - " Helper (Plugin)", - " Helper (Alerts)", - " Helper", - ]; - - std::env::current_exe() - .ok() - .and_then(|path| { - path - .file_name() - .and_then(|name| name.to_str()) - .map(|name| HELPER_SUFFIXES.iter().any(|suffix| name.ends_with(suffix))) - }) - .unwrap_or_default() + const HELPER_SUFFIXES: &[&str] = &[ + " Helper (GPU)", + " Helper (Renderer)", + " Helper (Plugin)", + " Helper (Alerts)", + " Helper", + ]; + + std::env::current_exe() + .ok() + .and_then(|path| { + path.file_name() + .and_then(|name| name.to_str()) + .map(|name| HELPER_SUFFIXES.iter().any(|suffix| name.ends_with(suffix))) + }) + .unwrap_or_default() } pub(crate) struct AppState { - pub(crate) windows: HashMap, - pub(crate) winid_id_to_window_id_map: HashMap, - pub(crate) callback: Box)>, - pub(crate) live_browsers: usize, - pub(crate) exiting: bool, + pub(crate) windows: HashMap, + pub(crate) winid_id_to_window_id_map: HashMap, + pub(crate) callback: Box)>, + pub(crate) live_browsers: usize, + pub(crate) exiting: bool, } pub(crate) struct WinitCefApp { - pub(crate) context: RuntimeContext, - receiver: Receiver>, - pub(crate) state: AppState, - pub(crate) scheme_registry: request_handler::SchemeRegistry, - /// Exit code from `RequestExit`, read back by `Runtime::run_return` after - /// the event loop finishes (winit's `run_app` return carries no code). - exit_code: Arc, + pub(crate) context: RuntimeContext, + receiver: Receiver>, + pub(crate) state: AppState, + pub(crate) scheme_registry: request_handler::SchemeRegistry, + /// Exit code from `RequestExit`, read back by `Runtime::run_return` after + /// the event loop finishes (winit's `run_app` return carries no code). + exit_code: Arc, } impl WinitCefApp { - fn new( - context: RuntimeContext, - receiver: Receiver>, - callback: Box)>, - scheme_registry: request_handler::SchemeRegistry, - exit_code: Arc, - ) -> Self { - Self { - context, - receiver, - state: AppState { - windows: HashMap::new(), - winid_id_to_window_id_map: HashMap::new(), - callback, - live_browsers: 0, - exiting: false, - }, - scheme_registry, - exit_code, + fn new( + context: RuntimeContext, + receiver: Receiver>, + callback: Box)>, + scheme_registry: request_handler::SchemeRegistry, + exit_code: Arc, + ) -> Self { + Self { + context, + receiver, + state: AppState { + windows: HashMap::new(), + winid_id_to_window_id_map: HashMap::new(), + callback, + live_browsers: 0, + exiting: false, + }, + scheme_registry, + exit_code, + } } - } - fn run_callback(&mut self, event: RunEvent) { - (self.state.callback)(event); - } + fn run_callback(&mut self, event: RunEvent) { + (self.state.callback)(event); + } - fn install_current_dispatch( - &mut self, - event_loop: &dyn ActiveEventLoop, - ) -> MainThreadDispatchGuard { - let mut dispatch = Box::new(MainThreadDispatch { - app: self as *mut _, - event_loop: event_loop as *const _, - }); + fn install_current_dispatch( + &mut self, + event_loop: &dyn ActiveEventLoop, + ) -> MainThreadDispatchGuard { + let mut dispatch = Box::new(MainThreadDispatch { + app: self as *mut _, + event_loop: event_loop as *const _, + }); - let previous = self.context.current_dispatch.install(dispatch.as_mut()); + let previous = self.context.current_dispatch.install(dispatch.as_mut()); - MainThreadDispatchGuard { - context: self.context.clone(), - dispatch, - previous, - } - } - - fn drain_messages(&mut self, event_loop: &dyn ActiveEventLoop) { - while let Ok(message) = self.receiver.try_recv() { - self.handle_message(event_loop, message); + MainThreadDispatchGuard { + context: self.context.clone(), + dispatch, + previous, + } } - } - fn handle_message(&mut self, event_loop: &dyn ActiveEventLoop, message: Message) { - match message { - Message::EventLoop(message) => self.handle_event_loop_message(event_loop, message), - Message::BrowserClosed(_window_id, webview_id) => { - // Standalone webview.close() keeps the child in state until this - // callback, so cleanup happens here. Window/app teardown removes child - // bookkeeping before asking CEF to close; then this message is only the - // lifecycle acknowledgement that lets live_browsers drain. - // - // The window_id baked into the browser's handlers can be stale after a - // reparent, so locate the webview by its process-unique id across every - // window rather than trusting the message's window_id — otherwise a - // reparented webview's scheme-handler entries would leak and its - // AppWebview would linger in the target window forever. - let child = self.state.windows.values_mut().find_map(|appwindow| { - appwindow - .children - .iter() - .position(|child| child.webview_id == webview_id) - .map(|index| appwindow.children.remove(index)) - }); - if let Some(child) = child { - self.remove_scheme_handler_entries(&child); + fn drain_messages(&mut self, event_loop: &dyn ActiveEventLoop) { + while let Ok(message) = self.receiver.try_recv() { + self.handle_message(event_loop, message); } + } - self.state.live_browsers = self.state.live_browsers.saturating_sub(1); - self.exit_if_done(event_loop); - } - Message::CreateWindow { - window_id, - webview_id, - pending, - after_window_creation, - result_tx, - } => { - let result = self.create_window( - event_loop, - window_id, - webview_id, - pending, - after_window_creation, - ); - let _ = result_tx.send(result); - } - Message::CreateWebview { - window_id, - webview_id, - pending, - result_tx, - } => { - let _ = result_tx.send(self.create_webview(window_id, webview_id, *pending)); - } - Message::Window { window_id, message } => { - self.handle_window_message(event_loop, window_id, message) - } - Message::Webview { - window_id, - webview_id, - message, - } => self.handle_webview_message(window_id, webview_id, message), - Message::DragDropScriptEvent { - window_id, - webview_id, - target, - drag_drop_state, - event, - } => { - if let Some(event) = browser_client::event_from_script_event(&drag_drop_state, event) { - self.emit_drag_drop_event(window_id, webview_id, target, event); + fn handle_message(&mut self, event_loop: &dyn ActiveEventLoop, message: Message) { + match message { + Message::EventLoop(message) => self.handle_event_loop_message(event_loop, message), + Message::BrowserClosed(_window_id, webview_id) => { + // Standalone webview.close() keeps the child in state until this + // callback, so cleanup happens here. Window/app teardown removes child + // bookkeeping before asking CEF to close; then this message is only the + // lifecycle acknowledgement that lets live_browsers drain. + // + // The window_id baked into the browser's handlers can be stale after a + // reparent, so locate the webview by its process-unique id across every + // window rather than trusting the message's window_id — otherwise a + // reparented webview's scheme-handler entries would leak and its + // AppWebview would linger in the target window forever. + let child = self.state.windows.values_mut().find_map(|appwindow| { + appwindow + .children + .iter() + .position(|child| child.webview_id == webview_id) + .map(|index| appwindow.children.remove(index)) + }); + if let Some(child) = child { + self.remove_scheme_handler_entries(&child); + } + + self.state.live_browsers = self.state.live_browsers.saturating_sub(1); + self.exit_if_done(event_loop); + } + Message::CreateWindow { + window_id, + webview_id, + pending, + after_window_creation, + result_tx, + } => { + let result = self.create_window( + event_loop, + window_id, + webview_id, + pending, + after_window_creation, + ); + let _ = result_tx.send(result); + } + Message::CreateWebview { + window_id, + webview_id, + pending, + result_tx, + } => { + let _ = result_tx.send(self.create_webview(window_id, webview_id, *pending)); + } + Message::Window { window_id, message } => { + self.handle_window_message(event_loop, window_id, message) + } + Message::Webview { + window_id, + webview_id, + message, + } => self.handle_webview_message(window_id, webview_id, message), + Message::DragDropScriptEvent { + window_id, + webview_id, + target, + drag_drop_state, + event, + } => { + if let Some(event) = + browser_client::event_from_script_event(&drag_drop_state, event) + { + self.emit_drag_drop_event(window_id, webview_id, target, event); + } + } + Message::Task(task) => task(), + Message::RequestExit(code) => { + if self.request_exit(Some(code)) { + self.exit_code.store(code, Ordering::Release); + self.close_all_browsers(); + self.exit_if_done(event_loop); + } + } + // Published tauri-runtime only has RunEvent::Opened on macOS/iOS/ + // Android; elsewhere the deep-link relaunch event has nowhere to go. + #[cfg(target_os = "macos")] + Message::Opened(urls) => self.run_callback(RunEvent::Opened { urls }), + #[cfg(not(target_os = "macos"))] + Message::Opened(urls) => { + log::warn!( + "dropping deep-link open event {urls:?}: no RunEvent::Opened on this platform in published tauri-runtime" + ); + } + #[cfg(target_os = "macos")] + Message::Reopen { + has_visible_windows, + } => self.run_callback(RunEvent::Reopen { + has_visible_windows, + }), + #[cfg(target_os = "macos")] + Message::AccessibilityChanged { enabled } => { + self.set_browsers_accessibility_state(enabled) + } + Message::UserEvent(event) => self.run_callback(RunEvent::UserEvent(event)), } - } - Message::Task(task) => task(), - Message::RequestExit(code) => { - if self.request_exit(Some(code)) { - self.exit_code.store(code, Ordering::Release); - self.close_all_browsers(); - self.exit_if_done(event_loop); - } - } - // Published tauri-runtime only has RunEvent::Opened on macOS/iOS/ - // Android; elsewhere the deep-link relaunch event has nowhere to go. - #[cfg(target_os = "macos")] - Message::Opened(urls) => self.run_callback(RunEvent::Opened { urls }), - #[cfg(not(target_os = "macos"))] - Message::Opened(urls) => { - log::warn!("dropping deep-link open event {urls:?}: no RunEvent::Opened on this platform in published tauri-runtime"); - } - #[cfg(target_os = "macos")] - Message::Reopen { - has_visible_windows, - } => self.run_callback(RunEvent::Reopen { - has_visible_windows, - }), - #[cfg(target_os = "macos")] - Message::AccessibilityChanged { enabled } => self.set_browsers_accessibility_state(enabled), - Message::UserEvent(event) => self.run_callback(RunEvent::UserEvent(event)), } - } - fn handle_event_loop_message( - &mut self, - event_loop: &dyn ActiveEventLoop, - message: EventLoopMessage, - ) { - match message { - EventLoopMessage::SetTheme(theme) => { - *self.context.app_wide_theme.lock().unwrap() = theme; - for appwindow in self.state.windows.values_mut() { - appwindow.set_theme(theme); + fn handle_event_loop_message( + &mut self, + event_loop: &dyn ActiveEventLoop, + message: EventLoopMessage, + ) { + match message { + EventLoopMessage::SetTheme(theme) => { + *self.context.app_wide_theme.lock().unwrap() = theme; + for appwindow in self.state.windows.values_mut() { + appwindow.set_theme(theme); + } + } + EventLoopMessage::PrimaryMonitor(tx) => { + let monitor = event_loop + .primary_monitor() + .map(|monitor| winit_monitor_to_tauri_monitor(&monitor)); + let _ = tx.send(monitor); + } + EventLoopMessage::MonitorFromPoint(tx, x, y) => { + let monitor = find_monitor_from_point(event_loop.available_monitors(), x, y) + .map(|monitor| winit_monitor_to_tauri_monitor(&monitor)); + let _ = tx.send(monitor); + } + EventLoopMessage::AvailableMonitors(tx) => { + let monitors = event_loop + .available_monitors() + .map(|monitor| winit_monitor_to_tauri_monitor(&monitor)) + .collect(); + let _ = tx.send(monitors); + } + EventLoopMessage::SetDeviceEventFilter(filter) => { + event_loop.listen_device_events(device_event_filter_to_winit(filter)); + } + EventLoopMessage::CursorPosition(tx) => { + let _ = tx.send(event_loop.cursor_position()); + } + EventLoopMessage::DisplayHandle(tx) => { + let handle = event_loop + .display_handle() + .map(|handle| SendRawDisplayHandle(handle.as_raw())); + let _ = tx.send(handle); + } + #[cfg(target_os = "macos")] + EventLoopMessage::SetActivationPolicy(activation_policy) => { + event_loop.set_activation_policy(activation_policy) + } + #[cfg(target_os = "macos")] + EventLoopMessage::SetDockVisibility(visible) => event_loop.set_dock_visibility(visible), + #[cfg(target_os = "macos")] + EventLoopMessage::ShowApplication => event_loop.show_application(), + #[cfg(target_os = "macos")] + EventLoopMessage::HideApplication => event_loop.hide_application(), } - } - EventLoopMessage::PrimaryMonitor(tx) => { - let monitor = event_loop - .primary_monitor() - .map(|monitor| winit_monitor_to_tauri_monitor(&monitor)); - let _ = tx.send(monitor); - } - EventLoopMessage::MonitorFromPoint(tx, x, y) => { - let monitor = find_monitor_from_point(event_loop.available_monitors(), x, y) - .map(|monitor| winit_monitor_to_tauri_monitor(&monitor)); - let _ = tx.send(monitor); - } - EventLoopMessage::AvailableMonitors(tx) => { - let monitors = event_loop - .available_monitors() - .map(|monitor| winit_monitor_to_tauri_monitor(&monitor)) - .collect(); - let _ = tx.send(monitors); - } - EventLoopMessage::SetDeviceEventFilter(filter) => { - event_loop.listen_device_events(device_event_filter_to_winit(filter)); - } - EventLoopMessage::CursorPosition(tx) => { - let _ = tx.send(event_loop.cursor_position()); - } - EventLoopMessage::DisplayHandle(tx) => { - let handle = event_loop - .display_handle() - .map(|handle| SendRawDisplayHandle(handle.as_raw())); - let _ = tx.send(handle); - } - #[cfg(target_os = "macos")] - EventLoopMessage::SetActivationPolicy(activation_policy) => { - event_loop.set_activation_policy(activation_policy) - } - #[cfg(target_os = "macos")] - EventLoopMessage::SetDockVisibility(visible) => event_loop.set_dock_visibility(visible), - #[cfg(target_os = "macos")] - EventLoopMessage::ShowApplication => event_loop.show_application(), - #[cfg(target_os = "macos")] - EventLoopMessage::HideApplication => event_loop.hide_application(), } - } - /// Removes the webview's `(browser_id, scheme)` entries from the scheme registry. - fn remove_scheme_handler_entries(&self, child: &AppWebview) { - let mut registry = self.scheme_registry.lock().unwrap(); - for scheme in child.uri_scheme_protocols.keys() { - registry.remove(&(child.browser_id, scheme.clone())); + /// Removes the webview's `(browser_id, scheme)` entries from the scheme registry. + fn remove_scheme_handler_entries(&self, child: &AppWebview) { + let mut registry = self.scheme_registry.lock().unwrap(); + for scheme in child.uri_scheme_protocols.keys() { + registry.remove(&(child.browser_id, scheme.clone())); + } } - } - fn emit_drag_drop_event( - &mut self, - window_id: WindowId, - webview_id: u32, - target: browser_client::DragDropEventTarget, - event: DragDropEvent, - ) { - match target { - browser_client::DragDropEventTarget::Window => { - self.emit_window_event(window_id, WindowEvent::DragDrop(event)); - } - browser_client::DragDropEventTarget::Webview => { - self.emit_webview_event(window_id, webview_id, WebviewEvent::DragDrop(event)); - } + fn emit_drag_drop_event( + &mut self, + window_id: WindowId, + webview_id: u32, + target: browser_client::DragDropEventTarget, + event: DragDropEvent, + ) { + match target { + browser_client::DragDropEventTarget::Window => { + self.emit_window_event(window_id, WindowEvent::DragDrop(event)); + } + browser_client::DragDropEventTarget::Webview => { + self.emit_webview_event(window_id, webview_id, WebviewEvent::DragDrop(event)); + } + } } - } - fn emit_window_event(&mut self, window_id: WindowId, event: WindowEvent) { - let Some(appwindow) = self.state.windows.get(&window_id) else { - return; - }; - let label = appwindow.label.clone(); - let listeners = appwindow.listeners.clone(); - - self.run_callback(RunEvent::WindowEvent { - label, - event: event.clone(), - }); - - { - let listeners = listeners.lock().unwrap(); - for handler in listeners.values() { - handler(&event); - } - } - } + fn emit_window_event(&mut self, window_id: WindowId, event: WindowEvent) { + let Some(appwindow) = self.state.windows.get(&window_id) else { + return; + }; + let label = appwindow.label.clone(); + let listeners = appwindow.listeners.clone(); - fn emit_webview_event(&mut self, window_id: WindowId, webview_id: u32, event: WebviewEvent) { - let Some(appwindow) = self.state.windows.get(&window_id) else { - return; - }; - let Some(child) = appwindow - .children - .iter() - .find(|child| child.webview_id == webview_id) - else { - return; - }; - let label = child.label.clone(); - let listeners = child.listeners.clone(); - - self.run_callback(RunEvent::WebviewEvent { - label, - event: event.clone(), - }); - - { - let listeners = listeners.lock().unwrap(); - for handler in listeners.values() { - handler(&event); - } - } - } + self.run_callback(RunEvent::WindowEvent { + label, + event: event.clone(), + }); - fn request_exit(&mut self, code: Option) -> bool { - // if we already exiting, don't request exit again - if self.state.exiting { - return false; + { + let listeners = listeners.lock().unwrap(); + for handler in listeners.values() { + handler(&event); + } + } } - let (tx, rx) = mpsc::channel(); - self.run_callback(RunEvent::ExitRequested { code, tx }); + fn emit_webview_event(&mut self, window_id: WindowId, webview_id: u32, event: WebviewEvent) { + let Some(appwindow) = self.state.windows.get(&window_id) else { + return; + }; + let Some(child) = appwindow + .children + .iter() + .find(|child| child.webview_id == webview_id) + else { + return; + }; + let label = child.label.clone(); + let listeners = child.listeners.clone(); + + self.run_callback(RunEvent::WebviewEvent { + label, + event: event.clone(), + }); - if matches!(rx.try_recv(), Ok(ExitRequestedEventAction::Prevent)) { - false - } else { - self.state.exiting = true; - true + { + let listeners = listeners.lock().unwrap(); + for handler in listeners.values() { + handler(&event); + } + } } - } - pub(crate) fn close_window(&mut self, window_id: WindowId, event_loop: &dyn ActiveEventLoop) { - if !self.state.windows.contains_key(&window_id) { - return; - } - // Emit Destroyed while the window is still in state (emit_window_event - // needs it): tauri's core prunes its window registry on this event, and - // app close hooks rely on it. Without it every closed window lives on as - // a zombie label — get_webview_window keeps returning a dead handle. The - // winit Destroyed event can't cover this: by the time it fires the id - // mapping below is already gone, so it never routes back to this window. - if !self.state.exiting { - self.emit_window_event(window_id, WindowEvent::Destroyed); - } - let Some(appwindow) = self.state.windows.remove(&window_id) else { - return; - }; - self - .state - .winid_id_to_window_id_map - .remove(&appwindow.window.id()); - // The window is gone from state, so BrowserClosed will not find these - // children later. Clean registry entries while we still hold them; the CEF - // shutdown drain is still enforced by live_browsers. - for child in &appwindow.children { - self.remove_scheme_handler_entries(child); - child.host.close_browser(1); - } - self.exit_if_done(event_loop); - } + fn request_exit(&mut self, code: Option) -> bool { + // if we already exiting, don't request exit again + if self.state.exiting { + return false; + } + + let (tx, rx) = mpsc::channel(); + self.run_callback(RunEvent::ExitRequested { code, tx }); - pub(crate) fn request_window_close( - &mut self, - window_id: WindowId, - event_loop: &dyn ActiveEventLoop, - ) { - // Avoid requesting window close if we already exisitng - if self.state.exiting { - self.close_window(window_id, event_loop); - return; + if matches!(rx.try_recv(), Ok(ExitRequestedEventAction::Prevent)) { + false + } else { + self.state.exiting = true; + true + } } - let (tx, rx) = mpsc::channel(); - let Some(appwindow) = self.state.windows.get(&window_id) else { - return; - }; - let label = appwindow.label.clone(); - let listeners = appwindow.listeners.clone(); - - { - let listeners = listeners.lock().unwrap(); - for handler in listeners.values() { - handler(&WindowEvent::CloseRequested { - signal_tx: tx.clone(), - }); - } + pub(crate) fn close_window(&mut self, window_id: WindowId, event_loop: &dyn ActiveEventLoop) { + if !self.state.windows.contains_key(&window_id) { + return; + } + // Emit Destroyed while the window is still in state (emit_window_event + // needs it): tauri's core prunes its window registry on this event, and + // app close hooks rely on it. Without it every closed window lives on as + // a zombie label — get_webview_window keeps returning a dead handle. The + // winit Destroyed event can't cover this: by the time it fires the id + // mapping below is already gone, so it never routes back to this window. + if !self.state.exiting { + self.emit_window_event(window_id, WindowEvent::Destroyed); + } + let Some(appwindow) = self.state.windows.remove(&window_id) else { + return; + }; + self.state + .winid_id_to_window_id_map + .remove(&appwindow.window.id()); + // The window is gone from state, so BrowserClosed will not find these + // children later. Clean registry entries while we still hold them; the CEF + // shutdown drain is still enforced by live_browsers. + for child in &appwindow.children { + self.remove_scheme_handler_entries(child); + child.host.close_browser(1); + } + self.exit_if_done(event_loop); } - self.run_callback(RunEvent::WindowEvent { - label, - event: WindowEvent::CloseRequested { signal_tx: tx }, - }); + pub(crate) fn request_window_close( + &mut self, + window_id: WindowId, + event_loop: &dyn ActiveEventLoop, + ) { + // Avoid requesting window close if we already exisitng + if self.state.exiting { + self.close_window(window_id, event_loop); + return; + } - if !matches!(rx.try_recv(), Ok(true)) { - self.close_window(window_id, event_loop); - } - } + let (tx, rx) = mpsc::channel(); + let Some(appwindow) = self.state.windows.get(&window_id) else { + return; + }; + let label = appwindow.label.clone(); + let listeners = appwindow.listeners.clone(); + + { + let listeners = listeners.lock().unwrap(); + for handler in listeners.values() { + handler(&WindowEvent::CloseRequested { + signal_tx: tx.clone(), + }); + } + } - fn close_all_browsers(&mut self) { - // App shutdown follows the same eager bookkeeping cleanup as window - // teardown. live_browsers keeps the loop alive until CEF confirms every - // browser close through BrowserClosed. - for appwindow in self.state.windows.values() { - for child in &appwindow.children { - self.remove_scheme_handler_entries(child); - child.host.close_browser(1); - } - } - self.state.windows.clear(); - self.state.winid_id_to_window_id_map.clear(); - } + self.run_callback(RunEvent::WindowEvent { + label, + event: WindowEvent::CloseRequested { signal_tx: tx }, + }); - #[cfg(target_os = "macos")] - fn set_browsers_accessibility_state(&self, enabled: bool) { - let state = if enabled { - State::ENABLED - } else { - State::DISABLED - }; - for appwindow in self.state.windows.values() { - for child in &appwindow.children { - child.host.set_accessibility_state(state); - } + if !matches!(rx.try_recv(), Ok(true)) { + self.close_window(window_id, event_loop); + } } - } - fn exit_if_done(&mut self, event_loop: &dyn ActiveEventLoop) { - if self.state.live_browsers != 0 { - return; + fn close_all_browsers(&mut self) { + // App shutdown follows the same eager bookkeeping cleanup as window + // teardown. live_browsers keeps the loop alive until CEF confirms every + // browser close through BrowserClosed. + for appwindow in self.state.windows.values() { + for child in &appwindow.children { + self.remove_scheme_handler_entries(child); + child.host.close_browser(1); + } + } + self.state.windows.clear(); + self.state.winid_id_to_window_id_map.clear(); } - if self.state.exiting || (self.state.windows.is_empty() && self.request_exit(None)) { - self.run_callback(RunEvent::Exit); - event_loop.exit(); + #[cfg(target_os = "macos")] + fn set_browsers_accessibility_state(&self, enabled: bool) { + let state = if enabled { + State::ENABLED + } else { + State::DISABLED + }; + for appwindow in self.state.windows.values() { + for child in &appwindow.children { + child.host.set_accessibility_state(state); + } + } } - } - /// Service the default GLib main context so the external message pump's GLib - /// timeout (and any GTK work CEF schedules) gets dispatched, then arm winit to - /// wake when the next tick is due. CEF is driven by that timeout firing, not - /// from here. Windows/macOS need no equivalent: their pump timers live on the - /// native loop winit already runs. - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - fn service_glib(&self, event_loop: &dyn ActiveEventLoop) { - let context = gtk::glib::MainContext::default(); - while context.pending() { - context.iteration(false); + fn exit_if_done(&mut self, event_loop: &dyn ActiveEventLoop) { + if self.state.live_browsers != 0 { + return; + } + + if self.state.exiting || (self.state.windows.is_empty() && self.request_exit(None)) { + self.run_callback(RunEvent::Exit); + event_loop.exit(); + } } - if let Some(deadline) = self.context.cef_pump.next_deadline() { - event_loop.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(deadline)); + + /// Service the default GLib main context so the external message pump's GLib + /// timeout (and any GTK work CEF schedules) gets dispatched, then arm winit to + /// wake when the next tick is due. CEF is driven by that timeout firing, not + /// from here. Windows/macOS need no equivalent: their pump timers live on the + /// native loop winit already runs. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn service_glib(&self, event_loop: &dyn ActiveEventLoop) { + let context = gtk::glib::MainContext::default(); + while context.pending() { + context.iteration(false); + } + if let Some(deadline) = self.context.cef_pump.next_deadline() { + event_loop.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(deadline)); + } } - } } impl ApplicationHandler for WinitCefApp { - fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) { - let _guard = self.install_current_dispatch(event_loop); - self.drain_messages(event_loop); - } - - fn new_events(&mut self, event_loop: &dyn ActiveEventLoop, cause: StartCause) { - let _guard = self.install_current_dispatch(event_loop); - match cause { - StartCause::Init => { - self.run_callback(RunEvent::Ready); - self.context.cef_pump.do_message_loop_work(); - } - // Match wry/tao, which emit `Resumed` on each `Poll` start cause. - StartCause::Poll => self.run_callback(RunEvent::Resumed), - _ => {} + fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) { + let _guard = self.install_current_dispatch(event_loop); + self.drain_messages(event_loop); } - } - - fn proxy_wake_up(&mut self, event_loop: &dyn ActiveEventLoop) { - let _guard = self.install_current_dispatch(event_loop); - self.drain_messages(event_loop); - } - fn about_to_wait(&mut self, event_loop: &dyn ActiveEventLoop) { - let _guard = self.install_current_dispatch(event_loop); - // TODO: remove once migrated to winit-gtk4 - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - self.service_glib(event_loop); - self.run_callback(RunEvent::MainEventsCleared); - } + fn new_events(&mut self, event_loop: &dyn ActiveEventLoop, cause: StartCause) { + let _guard = self.install_current_dispatch(event_loop); + match cause { + StartCause::Init => { + self.run_callback(RunEvent::Ready); + self.context.cef_pump.do_work(); + } + // Match wry/tao, which emit `Resumed` on each `Poll` start cause. + StartCause::Poll => self.run_callback(RunEvent::Resumed), + _ => {} + } + } - fn window_event( - &mut self, - event_loop: &dyn ActiveEventLoop, - winit_id: WinitWindowId, - event: WinitWindowEvent, - ) { - let _guard = self.install_current_dispatch(event_loop); - let Some(window_id) = self.state.winid_id_to_window_id_map.get(&winit_id).copied() else { - return; - }; - let Some(appwindow) = self.state.windows.get_mut(&window_id) else { - return; - }; + fn proxy_wake_up(&mut self, event_loop: &dyn ActiveEventLoop) { + let _guard = self.install_current_dispatch(event_loop); + self.drain_messages(event_loop); + } - match event { - WinitWindowEvent::CloseRequested => self.request_window_close(window_id, event_loop), + fn about_to_wait(&mut self, event_loop: &dyn ActiveEventLoop) { + let _guard = self.install_current_dispatch(event_loop); + // TODO: remove once migrated to winit-gtk4 + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + self.service_glib(event_loop); + self.run_callback(RunEvent::MainEventsCleared); + } - WinitWindowEvent::Destroyed => { - // close_window emits WindowEvent::Destroyed (exactly once — a window - // that already went through close_window no longer routes here). - self.close_window(window_id, event_loop); - } - WinitWindowEvent::SurfaceResized(size) => { - webview::layout_app_window(appwindow); - self.emit_window_event(window_id, WindowEvent::Resized(size)); - } - WinitWindowEvent::ScaleFactorChanged { - scale_factor, - surface_size_writer, - } => { - let new_inner_size = surface_size_writer - .surface_size() - .unwrap_or_else(|_| appwindow.window.surface_size()); - webview::layout_app_window(appwindow); - self.emit_window_event( - window_id, - WindowEvent::ScaleFactorChanged { - scale_factor, - new_inner_size, - }, - ); - } - WinitWindowEvent::Moved(pos) => { - self.emit_window_event( - window_id, - WindowEvent::Moved(PhysicalPosition::new(pos.x, pos.y)), - ); - } - WinitWindowEvent::Focused(focused) => { - self.emit_window_event(window_id, WindowEvent::Focused(focused)); - } - WinitWindowEvent::ThemeChanged(theme) => { - let system_theme = winit_theme_to_tauri_theme(theme); - if let Some(explicit_theme) = appwindow.preferred_theme() { - appwindow.set_theme(Some(explicit_theme)); + fn window_event( + &mut self, + event_loop: &dyn ActiveEventLoop, + winit_id: WinitWindowId, + event: WinitWindowEvent, + ) { + let _guard = self.install_current_dispatch(event_loop); + let Some(window_id) = self.state.winid_id_to_window_id_map.get(&winit_id).copied() else { + return; + }; + let Some(appwindow) = self.state.windows.get_mut(&window_id) else { + return; + }; + + match event { + WinitWindowEvent::CloseRequested => self.request_window_close(window_id, event_loop), + + WinitWindowEvent::Destroyed => { + // close_window emits WindowEvent::Destroyed (exactly once — a window + // that already went through close_window no longer routes here). + self.close_window(window_id, event_loop); + } + WinitWindowEvent::SurfaceResized(size) => { + webview::layout_app_window(appwindow); + self.emit_window_event(window_id, WindowEvent::Resized(size)); + } + WinitWindowEvent::ScaleFactorChanged { + scale_factor, + surface_size_writer, + } => { + let new_inner_size = surface_size_writer + .surface_size() + .unwrap_or_else(|_| appwindow.window.surface_size()); + webview::layout_app_window(appwindow); + self.emit_window_event( + window_id, + WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size, + }, + ); + } + WinitWindowEvent::Moved(pos) => { + self.emit_window_event( + window_id, + WindowEvent::Moved(PhysicalPosition::new(pos.x, pos.y)), + ); + } + WinitWindowEvent::Focused(focused) => { + self.emit_window_event(window_id, WindowEvent::Focused(focused)); + } + WinitWindowEvent::ThemeChanged(theme) => { + let system_theme = winit_theme_to_tauri_theme(theme); + if let Some(explicit_theme) = appwindow.preferred_theme() { + appwindow.set_theme(Some(explicit_theme)); + } + self.emit_window_event(window_id, WindowEvent::ThemeChanged(system_theme)); + } + WinitWindowEvent::DragEntered { paths, position } => { + let event = DragDropEvent::Enter { paths, position }; + self.emit_window_event(window_id, WindowEvent::DragDrop(event)); + } + WinitWindowEvent::DragMoved { position } => { + let event = DragDropEvent::Over { position }; + self.emit_window_event(window_id, WindowEvent::DragDrop(event)); + } + WinitWindowEvent::DragDropped { paths, position } => { + let event = DragDropEvent::Drop { paths, position }; + self.emit_window_event(window_id, WindowEvent::DragDrop(event)); + } + WinitWindowEvent::DragLeft { .. } => { + self.emit_window_event(window_id, WindowEvent::DragDrop(DragDropEvent::Leave)); + } + #[cfg(windows)] + WinitWindowEvent::RedrawRequested => { + appwindow.draw_background_surface(); + } + _ => {} } - self.emit_window_event(window_id, WindowEvent::ThemeChanged(system_theme)); - } - WinitWindowEvent::DragEntered { paths, position } => { - let event = DragDropEvent::Enter { paths, position }; - self.emit_window_event(window_id, WindowEvent::DragDrop(event)); - } - WinitWindowEvent::DragMoved { position } => { - let event = DragDropEvent::Over { position }; - self.emit_window_event(window_id, WindowEvent::DragDrop(event)); - } - WinitWindowEvent::DragDropped { paths, position } => { - let event = DragDropEvent::Drop { paths, position }; - self.emit_window_event(window_id, WindowEvent::DragDrop(event)); - } - WinitWindowEvent::DragLeft { .. } => { - self.emit_window_event(window_id, WindowEvent::DragDrop(DragDropEvent::Leave)); - } - #[cfg(windows)] - WinitWindowEvent::RedrawRequested => { - appwindow.draw_background_surface(); - } - _ => {} } - } } /// Registers the config-listed tauri custom protocol schemes with Chromium. @@ -952,14 +955,14 @@ impl ApplicationHandler for WinitCefApp { /// `fetch` POST to `ipc://localhost/`). Runs in every CEF process — the /// helper re-exec path registers the same set via `TauriCefHelperApp`. fn register_tauri_schemes(registrar: Option<&mut SchemeRegistrar>) { - let Some(registrar) = registrar else { return }; - let options = sys::cef_scheme_options_t::CEF_SCHEME_OPTION_STANDARD as i32 - | sys::cef_scheme_options_t::CEF_SCHEME_OPTION_SECURE as i32 - | sys::cef_scheme_options_t::CEF_SCHEME_OPTION_CORS_ENABLED as i32 - | sys::cef_scheme_options_t::CEF_SCHEME_OPTION_FETCH_ENABLED as i32; - for scheme in &crate::config::config().custom_schemes { - registrar.add_custom_scheme(Some(&CefString::from(scheme.as_str())), options); - } + let Some(registrar) = registrar else { return }; + let options = sys::cef_scheme_options_t::CEF_SCHEME_OPTION_STANDARD as i32 + | sys::cef_scheme_options_t::CEF_SCHEME_OPTION_SECURE as i32 + | sys::cef_scheme_options_t::CEF_SCHEME_OPTION_CORS_ENABLED as i32 + | sys::cef_scheme_options_t::CEF_SCHEME_OPTION_FETCH_ENABLED as i32; + for scheme in &crate::config::config().custom_schemes { + registrar.add_custom_scheme(Some(&CefString::from(scheme.as_str())), options); + } } wrap_app! { @@ -1015,51 +1018,52 @@ wrap_app! { /// `-`; a stale lock (dead pid, or another host on a shared /// home) is ignored — Chromium recovers those itself. fn live_singleton_lock_holder(cache_path: &std::path::Path) -> Option { - let target = std::fs::read_link(cache_path.join("SingletonLock")).ok()?; - let target = target.to_string_lossy(); - let (host, pid) = target.rsplit_once('-')?; - let pid: u32 = pid.parse().ok()?; - let our_host = std::fs::read_to_string("/proc/sys/kernel/hostname") - .map(|h| h.trim().to_string()) - .unwrap_or_default(); - if !our_host.is_empty() && host != our_host { - return None; - } - #[cfg(target_os = "linux")] - let alive = std::path::Path::new(&format!("/proc/{pid}")).exists(); - #[cfg(not(target_os = "linux"))] - let alive = std::process::Command::new("kill") - .args(["-0", &pid.to_string()]) - .status() - .map(|s| s.success()) - .unwrap_or(false); - alive.then_some(pid) + let target = std::fs::read_link(cache_path.join("SingletonLock")).ok()?; + let target = target.to_string_lossy(); + let (host, pid) = target.rsplit_once('-')?; + let pid: u32 = pid.parse().ok()?; + let our_host = std::fs::read_to_string("/proc/sys/kernel/hostname") + .map(|h| h.trim().to_string()) + .unwrap_or_default(); + if !our_host.is_empty() && host != our_host { + return None; + } + #[cfg(target_os = "linux")] + let alive = std::path::Path::new(&format!("/proc/{pid}")).exists(); + #[cfg(not(target_os = "linux"))] + let alive = std::process::Command::new("kill") + .args(["-0", &pid.to_string()]) + .status() + .map(|s| s.success()) + .unwrap_or(false); + alive.then_some(pid) } pub fn run_cef_helper_process() { - let args = cef::args::Args::new(); - - #[cfg(all(target_os = "macos", feature = "sandbox"))] - let _sandbox = { - let mut sandbox = cef::sandbox::Sandbox::new(); - sandbox.initialize(args.as_main_args()); - sandbox - }; - - #[cfg(target_os = "macos")] - let _loader = { - let loader = cef::library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), true); - assert!(loader.load()); - loader - }; - - let _ = cef::api_hash(sys::CEF_API_VERSION_LAST, 0); - let mut app = TauriCefHelperApp::new(); - let _ = cef::execute_process( - Some(args.as_main_args()), - Some(&mut app), - std::ptr::null_mut(), - ); + let args = cef::args::Args::new(); + + #[cfg(all(target_os = "macos", feature = "sandbox"))] + let _sandbox = { + let mut sandbox = cef::sandbox::Sandbox::new(); + sandbox.initialize(args.as_main_args()); + sandbox + }; + + #[cfg(target_os = "macos")] + let _loader = { + let loader = + cef::library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), true); + assert!(loader.load()); + loader + }; + + let _ = cef::api_hash(sys::CEF_API_VERSION_LAST, 0); + let mut app = TauriCefHelperApp::new(); + let _ = cef::execute_process( + Some(args.as_main_args()), + Some(&mut app), + std::ptr::null_mut(), + ); } wrap_app! { @@ -1078,517 +1082,507 @@ wrap_app! { #[derive(Debug, Clone)] pub struct CefRuntimeHandle { - context: RuntimeContext, + context: RuntimeContext, } impl RuntimeHandle for CefRuntimeHandle { - type Runtime = CefRuntime; + type Runtime = CefRuntime; - fn create_proxy(&self) -> >::EventLoopProxy { - EventProxy { - context: self.context.clone(), + fn create_proxy(&self) -> >::EventLoopProxy { + EventProxy { + context: self.context.clone(), + } } - } - #[cfg(target_os = "macos")] - fn set_activation_policy( - &self, - activation_policy: tauri_runtime::ActivationPolicy, - ) -> Result<()> { - let message = Message::EventLoop(EventLoopMessage::SetActivationPolicy(activation_policy)); - self.context.send_message(message) - } + #[cfg(target_os = "macos")] + fn set_activation_policy( + &self, + activation_policy: tauri_runtime::ActivationPolicy, + ) -> Result<()> { + let message = Message::EventLoop(EventLoopMessage::SetActivationPolicy(activation_policy)); + self.context.send_message(message) + } - #[cfg(target_os = "macos")] - fn set_dock_visibility(&self, visible: bool) -> Result<()> { - let message = Message::EventLoop(EventLoopMessage::SetDockVisibility(visible)); - self.context.send_message(message) - } + #[cfg(target_os = "macos")] + fn set_dock_visibility(&self, visible: bool) -> Result<()> { + let message = Message::EventLoop(EventLoopMessage::SetDockVisibility(visible)); + self.context.send_message(message) + } - fn request_exit(&self, code: i32) -> Result<()> { - self.context.send_message(Message::RequestExit(code)) - } + fn request_exit(&self, code: i32) -> Result<()> { + self.context.send_message(Message::RequestExit(code)) + } - fn create_window) + Send + 'static>( - &self, - pending: PendingWindow, - after_window_creation: Option, - ) -> Result> { - create_window_detached(&self.context, pending, after_window_creation) - } + fn create_window) + Send + 'static>( + &self, + pending: PendingWindow, + after_window_creation: Option, + ) -> Result> { + create_window_detached(&self.context, pending, after_window_creation) + } - fn create_webview( - &self, - window_id: WindowId, - pending: PendingWebview, - ) -> Result> { - create_webview_detached(&self.context, window_id, pending) - } + fn create_webview( + &self, + window_id: WindowId, + pending: PendingWebview, + ) -> Result> { + create_webview_detached(&self.context, window_id, pending) + } - fn run_on_main_thread(&self, f: F) -> Result<()> { - self.context.run_on_main_thread(f) - } + fn run_on_main_thread(&self, f: F) -> Result<()> { + self.context.run_on_main_thread(f) + } - fn display_handle( - &self, - ) -> std::result::Result, raw_window_handle::HandleError> { - let raw = event_loop_getter!(self, DisplayHandle) - .map_err(|_| raw_window_handle::HandleError::Unavailable)??; - // SAFETY: the descriptor was produced by the live event loop on its own - // thread; the borrowed handle is valid for as long as the runtime is. - Ok(unsafe { DisplayHandle::borrow_raw(raw.0) }) - } + fn display_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> { + let raw = event_loop_getter!(self, DisplayHandle) + .map_err(|_| raw_window_handle::HandleError::Unavailable)??; + // SAFETY: the descriptor was produced by the live event loop on its own + // thread; the borrowed handle is valid for as long as the runtime is. + Ok(unsafe { DisplayHandle::borrow_raw(raw.0) }) + } - fn primary_monitor(&self) -> Option { - event_loop_getter!(self, PrimaryMonitor).ok().flatten() - } + fn primary_monitor(&self) -> Option { + event_loop_getter!(self, PrimaryMonitor).ok().flatten() + } - fn monitor_from_point(&self, x: f64, y: f64) -> Option { - let (tx, rx) = mpsc::channel(); - self - .context - .send_message(Message::EventLoop(EventLoopMessage::MonitorFromPoint( - tx, x, y, - ))) - .and_then(|_| rx.recv().map_err(|_| Error::FailedToReceiveMessage)) - .ok() - .flatten() - } + fn monitor_from_point(&self, x: f64, y: f64) -> Option { + let (tx, rx) = mpsc::channel(); + self.context + .send_message(Message::EventLoop(EventLoopMessage::MonitorFromPoint( + tx, x, y, + ))) + .and_then(|_| rx.recv().map_err(|_| Error::FailedToReceiveMessage)) + .ok() + .flatten() + } - fn available_monitors(&self) -> Vec { - event_loop_getter!(self, AvailableMonitors).unwrap_or_default() - } + fn available_monitors(&self) -> Vec { + event_loop_getter!(self, AvailableMonitors).unwrap_or_default() + } - fn cursor_position(&self) -> Result> { - event_loop_getter!(self, CursorPosition)? - } + fn cursor_position(&self) -> Result> { + event_loop_getter!(self, CursorPosition)? + } - fn set_theme(&self, theme: Option) { - let message = Message::EventLoop(EventLoopMessage::SetTheme(theme)); - let _ = self.context.send_message(message); - } + fn set_theme(&self, theme: Option) { + let message = Message::EventLoop(EventLoopMessage::SetTheme(theme)); + let _ = self.context.send_message(message); + } - #[cfg(target_os = "macos")] - fn show(&self) -> Result<()> { - let message = Message::EventLoop(EventLoopMessage::ShowApplication); - self.context.send_message(message) - } + #[cfg(target_os = "macos")] + fn show(&self) -> Result<()> { + let message = Message::EventLoop(EventLoopMessage::ShowApplication); + self.context.send_message(message) + } - #[cfg(target_os = "macos")] - fn hide(&self) -> Result<()> { - let message = Message::EventLoop(EventLoopMessage::HideApplication); - self.context.send_message(message) - } + #[cfg(target_os = "macos")] + fn hide(&self) -> Result<()> { + let message = Message::EventLoop(EventLoopMessage::HideApplication); + self.context.send_message(message) + } - fn set_device_event_filter(&self, filter: DeviceEventFilter) { - let message = Message::EventLoop(EventLoopMessage::SetDeviceEventFilter(filter)); - let _ = self.context.send_message(message); - } + fn set_device_event_filter(&self, filter: DeviceEventFilter) { + let message = Message::EventLoop(EventLoopMessage::SetDeviceEventFilter(filter)); + let _ = self.context.send_message(message); + } - #[cfg(any(target_os = "macos", target_os = "ios"))] - fn fetch_data_store_identifiers) + Send + 'static>( - &self, - cb: F, - ) -> Result<()> { - cb(Vec::new()); - Ok(()) - } + #[cfg(any(target_os = "macos", target_os = "ios"))] + fn fetch_data_store_identifiers) + Send + 'static>( + &self, + cb: F, + ) -> Result<()> { + cb(Vec::new()); + Ok(()) + } - #[cfg(any(target_os = "macos", target_os = "ios"))] - fn remove_data_store) + Send + 'static>( - &self, - _uuid: [u8; 16], - cb: F, - ) -> Result<()> { - cb(Ok(())); - Ok(()) - } + #[cfg(any(target_os = "macos", target_os = "ios"))] + fn remove_data_store) + Send + 'static>( + &self, + _uuid: [u8; 16], + cb: F, + ) -> Result<()> { + cb(Ok(())); + Ok(()) + } } pub struct CefRuntime { - event_loop: EventLoop, - receiver: Receiver>, - context: RuntimeContext, - scheme_registry: request_handler::SchemeRegistry, - #[cfg(target_os = "macos")] - _app_delegate: Option>, + event_loop: EventLoop, + receiver: Receiver>, + context: RuntimeContext, + scheme_registry: request_handler::SchemeRegistry, + #[cfg(target_os = "macos")] + _app_delegate: Option>, } impl fmt::Debug for CefRuntime { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("CefRuntime").finish() - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("CefRuntime").finish() + } } impl CefRuntime { - fn init( - mut event_loop_builder: EventLoopBuilder, - #[allow(unused_variables)] runtime_args: RuntimeInitArgs, - ) -> Result { - let args = cef::args::Args::new(); + fn init( + mut event_loop_builder: EventLoopBuilder, + #[allow(unused_variables)] runtime_args: RuntimeInitArgs, + ) -> Result { + let args = cef::args::Args::new(); + + #[cfg(target_os = "macos")] + let is_helper = is_cef_helper_process(); + + #[cfg(target_os = "macos")] + let (_sandbox, _loader) = { + #[cfg(feature = "sandbox")] + let sandbox = if is_helper { + let mut sandbox = cef::sandbox::Sandbox::new(); + sandbox.initialize(args.as_main_args()); + Some(sandbox) + } else { + None + }; + #[cfg(not(feature = "sandbox"))] + let sandbox = (); + + let loader = cef::library_loader::LibraryLoader::new( + &std::env::current_exe().unwrap(), + is_helper, + ); + assert!(loader.load()); - #[cfg(target_os = "macos")] - let is_helper = is_cef_helper_process(); + (sandbox, loader) + }; - #[cfg(target_os = "macos")] - let (_sandbox, _loader) = { - #[cfg(feature = "sandbox")] - let sandbox = if is_helper { - let mut sandbox = cef::sandbox::Sandbox::new(); - sandbox.initialize(args.as_main_args()); - Some(sandbox) - } else { - None - }; - #[cfg(not(feature = "sandbox"))] - let sandbox = (); - - let loader = - cef::library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), is_helper); - assert!(loader.load()); - - (sandbox, loader) - }; + #[cfg(target_os = "macos")] + if !is_helper { + crate::platform::macos::setup_application(); + } - #[cfg(target_os = "macos")] - if !is_helper { - crate::platform::macos::setup_application(); - } + // The CEF API version table must be initialized before any other CEF call + // (e.g. `args.as_cmd_line()` below), otherwise the process crashes with no + // diagnostics. + let _ = cef::api_hash(sys::CEF_API_VERSION_LAST, 0); + + // Handle CEF subprocesses (renderer/GPU/utility) before any browser-only + // setup such as building the event loop, creating cache directories, or the + // runtime context. The browser (main) process has no `type` switch; + // subprocesses are launched with one (e.g. `--type=renderer`). + let is_browser_process = args + .as_cmd_line() + .map(|cmd| cmd.has_switch(Some(&CefString::from("type"))) != 1) + .unwrap_or(true); + + if !is_browser_process { + let mut helper_app = TauriCefHelperApp::new(); + let ret = cef::execute_process( + Some(args.as_main_args()), + Some(&mut helper_app), + std::ptr::null_mut(), + ); + // A subprocess finished its work; exit with its exit code instead of + // falling through to browser runtime initialization. + std::process::exit(ret.max(0)); + } - // The CEF API version table must be initialized before any other CEF call - // (e.g. `args.as_cmd_line()` below), otherwise the process crashes with no - // diagnostics. - let _ = cef::api_hash(sys::CEF_API_VERSION_LAST, 0); + // Published tauri's RuntimeInitArgs has no channel for CEF-specific init + // data (identifier/switches/cache path), so it comes from the + // process-global crate config instead — see `crate::configure`. + let cef_config = crate::config::config(); + let mut command_line_args = cef_config.command_line_args.clone(); + let deep_link_schemes = cef_config.deep_link_schemes.clone(); - // Handle CEF subprocesses (renderer/GPU/utility) before any browser-only - // setup such as building the event loop, creating cache directories, or the - // runtime context. The browser (main) process has no `type` switch; - // subprocesses are launched with one (e.g. `--type=renderer`). - let is_browser_process = args - .as_cmd_line() - .map(|cmd| cmd.has_switch(Some(&CefString::from("type"))) != 1) - .unwrap_or(true); - - if !is_browser_process { - let mut helper_app = TauriCefHelperApp::new(); - let ret = cef::execute_process( - Some(args.as_main_args()), - Some(&mut helper_app), - std::ptr::null_mut(), - ); - // A subprocess finished its work; exit with its exit code instead of - // falling through to browser runtime initialization. - std::process::exit(ret.max(0)); - } - - // Published tauri's RuntimeInitArgs has no channel for CEF-specific init - // data (identifier/switches/cache path), so it comes from the - // process-global crate config instead — see `crate::configure`. - let cef_config = crate::config::config(); - let mut command_line_args = cef_config.command_line_args.clone(); - let deep_link_schemes = cef_config.deep_link_schemes.clone(); - - let cache_path = cef_config.cache_path.clone().unwrap_or_else(|| { - let cache_base = dirs::cache_dir().unwrap_or_else(std::env::temp_dir); - cache_base.join(&cef_config.identifier).join("cef") - }); - let _ = create_dir_all(&cache_path); - - // Chromium guards its profile with a `SingletonLock` symlink whose target - // is `-`. A second browser process on the same cache dir - // doesn't fail at initialize — Chromium only surfaces the conflict later, - // as a renderer/GPU startup failure. Fail fast with an actionable error - // instead when the holder is verifiably alive. - if let Some(holder_pid) = live_singleton_lock_holder(&cache_path) { - return Err(Error::CreateWebview( - format!( - "CEF cache {} is held by running process {holder_pid} (SingletonLock); \ + let cache_path = cef_config.cache_path.clone().unwrap_or_else(|| { + let cache_base = dirs::cache_dir().unwrap_or_else(std::env::temp_dir); + cache_base.join(&cef_config.identifier).join("cef") + }); + let _ = create_dir_all(&cache_path); + + // Chromium guards its profile with a `SingletonLock` symlink whose target + // is `-`. A second browser process on the same cache dir + // doesn't fail at initialize — Chromium only surfaces the conflict later, + // as a renderer/GPU startup failure. Fail fast with an actionable error + // instead when the holder is verifiably alive. + if let Some(holder_pid) = live_singleton_lock_holder(&cache_path) { + return Err(Error::CreateWebview( + format!( + "CEF cache {} is held by running process {holder_pid} (SingletonLock); \ close that instance or configure a distinct cache_path/identifier", - cache_path.display() - ) - .into(), - )); - } - - // Force X11 usage on Linux - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - { - command_line_args.push(("ozone-platform".to_string(), Some("x11".to_string()))); - event_loop_builder.with_x11(); - } - - #[cfg(windows)] - if let Some(hook) = runtime_args.msg_hook { - use winit::platform::windows::EventLoopBuilderExtWindows; - event_loop_builder.with_msg_hook(hook); - } - - #[cfg(target_os = "macos")] - event_loop_builder.with_default_menu(false); - - let event_loop = event_loop_builder - .build() - .map_err(|_| Error::CreateWindow)?; - let proxy = event_loop.create_proxy(); - let (sender, receiver) = mpsc::channel(); - let context_initialized = Arc::new(AtomicBool::new(false)); - let cef_pump = CefExternalPump::new( - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - proxy.clone(), - ); - let context = RuntimeContext { - sender: sender.clone(), - proxy: proxy.clone(), - main_thread_id: std::thread::current().id(), - next_window_id: Default::default(), - next_webview_id: Default::default(), - next_window_event_id: Default::default(), - next_webview_event_id: Default::default(), - current_dispatch: Default::default(), - app_wide_theme: Default::default(), - cef_pump, - cache_path: Arc::new(cache_path.clone()), - }; + cache_path.display() + ) + .into(), + )); + } - // NOT `--enable-media-stream`: CEF documents that switch as granting all - // media permissions, and it suppresses OnRequestMediaAccessPermission - // entirely ("This function will not be called if the --enable-media-stream - // command-line switch is used"). Every camera, microphone and screen - // request would bypass the permission policy — silently, since the handler - // never runs. Media access is gated like any other permission; an app that - // wants the blanket grant can set the switch itself through CefConfig. - let mut app = TauriCefApp::new( - context.clone(), - context_initialized.clone(), - deep_link_schemes, - command_line_args, - ); + // Force X11 usage on Linux + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + command_line_args.push(("ozone-platform".to_string(), Some("x11".to_string()))); + event_loop_builder.with_x11(); + } - // Subprocesses already exited above, so this must be the browser process; - // `execute_process` returns -1 there to signal normal startup should follow. - let ret = cef::execute_process( - Some(args.as_main_args()), - Some(&mut app), - std::ptr::null_mut(), - ); - assert_eq!( - ret, -1, - "CEF browser process unexpectedly returned from execute_process" - ); + #[cfg(windows)] + if let Some(hook) = runtime_args.msg_hook { + use winit::platform::windows::EventLoopBuilderExtWindows; + event_loop_builder.with_msg_hook(hook); + } - let settings = cef::Settings { - no_sandbox: !cfg!(feature = "sandbox") as i32, - cache_path: cache_path.to_string_lossy().to_string().as_str().into(), - external_message_pump: 1, - // Comma-delimited; empty keeps CEF's http/https-only default. The - // defaults stay included because exclude_defaults is left 0. - cookieable_schemes_list: cef_config.cookieable_schemes.join(",").as_str().into(), - ..Default::default() - }; - if cef::initialize( - Some(args.as_main_args()), - Some(&settings), - Some(&mut app), - std::ptr::null_mut(), - ) != 1 - { - return Err(Error::WebviewRuntimeNotInstalled); - } + #[cfg(target_os = "macos")] + event_loop_builder.with_default_menu(false); + + let event_loop = event_loop_builder + .build() + .map_err(|_| Error::CreateWindow)?; + let proxy = event_loop.create_proxy(); + let (sender, receiver) = mpsc::channel(); + let context_initialized = Arc::new(AtomicBool::new(false)); + let cef_pump = CefExternalPump::new(); + let context = RuntimeContext { + sender: sender.clone(), + proxy: proxy.clone(), + main_thread_id: std::thread::current().id(), + next_window_id: Default::default(), + next_webview_id: Default::default(), + next_window_event_id: Default::default(), + next_webview_event_id: Default::default(), + current_dispatch: Default::default(), + app_wide_theme: Default::default(), + cef_pump, + cache_path: Arc::new(cache_path.clone()), + }; + + // NOT `--enable-media-stream`: CEF documents that switch as granting all + // media permissions, and it suppresses OnRequestMediaAccessPermission + // entirely ("This function will not be called if the --enable-media-stream + // command-line switch is used"). Every camera, microphone and screen + // request would bypass the permission policy — silently, since the handler + // never runs. Media access is gated like any other permission; an app that + // wants the blanket grant can set the switch itself through CefConfig. + let mut app = TauriCefApp::new( + context.clone(), + context_initialized.clone(), + deep_link_schemes, + command_line_args, + ); - #[cfg(target_os = "macos")] - let app_delegate = if !is_helper { - use crate::platform::macos::AppDelegateEvent; + // Subprocesses already exited above, so this must be the browser process; + // `execute_process` returns -1 there to signal normal startup should follow. + let ret = cef::execute_process( + Some(args.as_main_args()), + Some(&mut app), + std::ptr::null_mut(), + ); + assert_eq!( + ret, -1, + "CEF browser process unexpectedly returned from execute_process" + ); - let context_ = context.clone(); - let handler = Box::new(move |event| match event { - AppDelegateEvent::TryTerminate => { - let _ = context_.send_message(Message::RequestExit(0)); - } - AppDelegateEvent::Reopen { - has_visible_windows, - } => { - let _ = context_.send_message(Message::Reopen { - has_visible_windows, - }); + let settings = cef::Settings { + no_sandbox: !cfg!(feature = "sandbox") as i32, + cache_path: cache_path.to_string_lossy().to_string().as_str().into(), + external_message_pump: 1, + // Comma-delimited; empty keeps CEF's http/https-only default. The + // defaults stay included because exclude_defaults is left 0. + cookieable_schemes_list: cef_config.cookieable_schemes.join(",").as_str().into(), + ..Default::default() + }; + if cef::initialize( + Some(args.as_main_args()), + Some(&settings), + Some(&mut app), + std::ptr::null_mut(), + ) != 1 + { + return Err(Error::WebviewRuntimeNotInstalled); } - AppDelegateEvent::AccessibilityChanged { enabled } => { - let _ = context_.send_message(Message::AccessibilityChanged { enabled }); - } - AppDelegateEvent::OpenURLs { urls } => { - let _ = context_.send_message(Message::Opened(urls)); + + #[cfg(target_os = "macos")] + let app_delegate = if !is_helper { + use crate::platform::macos::AppDelegateEvent; + + let context_ = context.clone(); + let handler = Box::new(move |event| match event { + AppDelegateEvent::TryTerminate => { + let _ = context_.send_message(Message::RequestExit(0)); + } + AppDelegateEvent::Reopen { + has_visible_windows, + } => { + let _ = context_.send_message(Message::Reopen { + has_visible_windows, + }); + } + AppDelegateEvent::AccessibilityChanged { enabled } => { + let _ = context_.send_message(Message::AccessibilityChanged { enabled }); + } + AppDelegateEvent::OpenURLs { urls } => { + let _ = context_.send_message(Message::Opened(urls)); + } + }); + let app_delegate = crate::platform::macos::set_application_event_handler(handler); + Some(app_delegate) + } else { + None + }; + + // Wait for the CEF context to initialize before returning, so that the runtime is ready to create browsers. + while !context_initialized.load(Ordering::SeqCst) { + context.cef_pump.do_work(); + std::thread::sleep(Duration::from_millis(1)); } - }); - let app_delegate = crate::platform::macos::set_application_event_handler(handler); - Some(app_delegate) - } else { - None - }; - // Wait for the CEF context to initialize before returning, so that the runtime is ready to create browsers. - while !context_initialized.load(Ordering::SeqCst) { - context.cef_pump.do_message_loop_work(); - std::thread::sleep(Duration::from_millis(1)); + Ok(Self { + event_loop, + receiver, + context, + scheme_registry: Default::default(), + #[cfg(target_os = "macos")] + _app_delegate: app_delegate, + }) } - - Ok(Self { - event_loop, - receiver, - context, - scheme_registry: Default::default(), - #[cfg(target_os = "macos")] - _app_delegate: app_delegate, - }) - } } impl Runtime for CefRuntime { - type WindowDispatcher = CefWindowDispatcher; - type WebviewDispatcher = CefWebviewDispatcher; - type Handle = CefRuntimeHandle; - type EventLoopProxy = EventProxy; + type WindowDispatcher = CefWindowDispatcher; + type WebviewDispatcher = CefWebviewDispatcher; + type Handle = CefRuntimeHandle; + type EventLoopProxy = EventProxy; - fn new(args: RuntimeInitArgs) -> Result { - Self::init(EventLoopBuilder::default(), args) - } + fn new(args: RuntimeInitArgs) -> Result { + Self::init(EventLoopBuilder::default(), args) + } - #[cfg(any( - windows, - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - fn new_any_thread(args: RuntimeInitArgs) -> Result { - let mut event_loop_builder = EventLoopBuilder::default(); - event_loop_builder.with_any_thread(true); - Self::init(event_loop_builder, args) - } + #[cfg(any( + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn new_any_thread(args: RuntimeInitArgs) -> Result { + let mut event_loop_builder = EventLoopBuilder::default(); + event_loop_builder.with_any_thread(true); + Self::init(event_loop_builder, args) + } - fn create_proxy(&self) -> Self::EventLoopProxy { - EventProxy { - context: self.context.clone(), + fn create_proxy(&self) -> Self::EventLoopProxy { + EventProxy { + context: self.context.clone(), + } } - } - fn handle(&self) -> Self::Handle { - CefRuntimeHandle { - context: self.context.clone(), + fn handle(&self) -> Self::Handle { + CefRuntimeHandle { + context: self.context.clone(), + } } - } - fn create_window) + Send + 'static>( - &self, - pending: PendingWindow, - after_window_creation: Option, - ) -> Result> { - create_window_detached(&self.context, pending, after_window_creation) - } + fn create_window) + Send + 'static>( + &self, + pending: PendingWindow, + after_window_creation: Option, + ) -> Result> { + create_window_detached(&self.context, pending, after_window_creation) + } - fn create_webview( - &self, - window_id: WindowId, - pending: PendingWebview, - ) -> Result> { - create_webview_detached(&self.context, window_id, pending) - } + fn create_webview( + &self, + window_id: WindowId, + pending: PendingWebview, + ) -> Result> { + create_webview_detached(&self.context, window_id, pending) + } - fn primary_monitor(&self) -> Option { - event_loop_getter!(self, PrimaryMonitor).ok().flatten() - } + fn primary_monitor(&self) -> Option { + event_loop_getter!(self, PrimaryMonitor).ok().flatten() + } - fn monitor_from_point(&self, x: f64, y: f64) -> Option { - let (tx, rx) = mpsc::channel(); - self - .context - .send_message(Message::EventLoop(EventLoopMessage::MonitorFromPoint( - tx, x, y, - ))) - .and_then(|_| rx.recv().map_err(|_| Error::FailedToReceiveMessage)) - .ok() - .flatten() - } + fn monitor_from_point(&self, x: f64, y: f64) -> Option { + let (tx, rx) = mpsc::channel(); + self.context + .send_message(Message::EventLoop(EventLoopMessage::MonitorFromPoint( + tx, x, y, + ))) + .and_then(|_| rx.recv().map_err(|_| Error::FailedToReceiveMessage)) + .ok() + .flatten() + } - fn available_monitors(&self) -> Vec { - event_loop_getter!(self, AvailableMonitors).unwrap_or_default() - } + fn available_monitors(&self) -> Vec { + event_loop_getter!(self, AvailableMonitors).unwrap_or_default() + } - fn cursor_position(&self) -> Result> { - event_loop_getter!(self, CursorPosition)? - } + fn cursor_position(&self) -> Result> { + event_loop_getter!(self, CursorPosition)? + } - fn set_theme(&self, theme: Option) { - let message = Message::EventLoop(EventLoopMessage::SetTheme(theme)); - let _ = self.context.send_message(message); - } + fn set_theme(&self, theme: Option) { + let message = Message::EventLoop(EventLoopMessage::SetTheme(theme)); + let _ = self.context.send_message(message); + } - #[cfg(target_os = "macos")] - fn set_activation_policy(&mut self, activation_policy: tauri_runtime::ActivationPolicy) { - let message = Message::EventLoop(EventLoopMessage::SetActivationPolicy(activation_policy)); - let _ = self.context.send_message(message); - } + #[cfg(target_os = "macos")] + fn set_activation_policy(&mut self, activation_policy: tauri_runtime::ActivationPolicy) { + let message = Message::EventLoop(EventLoopMessage::SetActivationPolicy(activation_policy)); + let _ = self.context.send_message(message); + } - #[cfg(target_os = "macos")] - fn set_dock_visibility(&mut self, visible: bool) { - let message = Message::EventLoop(EventLoopMessage::SetDockVisibility(visible)); - let _ = self.context.send_message(message); - } + #[cfg(target_os = "macos")] + fn set_dock_visibility(&mut self, visible: bool) { + let message = Message::EventLoop(EventLoopMessage::SetDockVisibility(visible)); + let _ = self.context.send_message(message); + } - #[cfg(target_os = "macos")] - fn show(&self) { - let message = Message::EventLoop(EventLoopMessage::ShowApplication); - let _ = self.context.send_message(message); - } + #[cfg(target_os = "macos")] + fn show(&self) { + let message = Message::EventLoop(EventLoopMessage::ShowApplication); + let _ = self.context.send_message(message); + } - #[cfg(target_os = "macos")] - fn hide(&self) { - let message = Message::EventLoop(EventLoopMessage::HideApplication); - let _ = self.context.send_message(message); - } + #[cfg(target_os = "macos")] + fn hide(&self) { + let message = Message::EventLoop(EventLoopMessage::HideApplication); + let _ = self.context.send_message(message); + } - fn set_device_event_filter(&mut self, filter: DeviceEventFilter) { - self - .event_loop - .listen_device_events(device_event_filter_to_winit(filter)); - } + fn set_device_event_filter(&mut self, filter: DeviceEventFilter) { + self.event_loop + .listen_device_events(device_event_filter_to_winit(filter)); + } - fn run_iteration) + 'static>(&mut self, mut callback: F) { - while let Ok(message) = self.receiver.try_recv() { - if let Message::UserEvent(event) = message { - callback(RunEvent::UserEvent(event)); - } + fn run_iteration) + 'static>(&mut self, mut callback: F) { + while let Ok(message) = self.receiver.try_recv() { + if let Message::UserEvent(event) = message { + callback(RunEvent::UserEvent(event)); + } + } + self.context.cef_pump.do_work(); + callback(RunEvent::MainEventsCleared); } - self.context.cef_pump.do_message_loop_work(); - callback(RunEvent::MainEventsCleared); - } - fn run_return) + 'static>(self, callback: F) -> i32 { - let exit_code = Arc::new(std::sync::atomic::AtomicI32::new(0)); - let app = WinitCefApp::new( - self.context, - self.receiver, - Box::new(callback), - self.scheme_registry, - exit_code.clone(), - ); - let _ = self.event_loop.run_app(app); - cef::shutdown(); - exit_code.load(Ordering::Acquire) - } + fn run_return) + 'static>(self, callback: F) -> i32 { + let exit_code = Arc::new(std::sync::atomic::AtomicI32::new(0)); + let app = WinitCefApp::new( + self.context, + self.receiver, + Box::new(callback), + self.scheme_registry, + exit_code.clone(), + ); + let _ = self.event_loop.run_app(app); + cef::shutdown(); + exit_code.load(Ordering::Acquire) + } - fn run) + 'static>(self, callback: F) { - self.run_return(callback); - } + fn run) + 'static>(self, callback: F) { + self.run_return(callback); + } } diff --git a/src/streaming.rs b/src/streaming.rs index 1d156b5..8bf8421 100644 --- a/src/streaming.rs +++ b/src/streaming.rs @@ -17,7 +17,7 @@ //! drives it from `read()` and parks/wakes CEF's `ResourceReadCallback`. use std::collections::HashMap; -use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; +use std::sync::mpsc::{Receiver, SyncSender, sync_channel}; use std::sync::{Arc, Mutex, OnceLock}; /// Bounded in-flight chunk budget. Backpressure: a producer blocks on the @@ -40,32 +40,32 @@ pub struct InitiatorOrigin(pub Option); /// The `http::Request` carries the request body and, as an extension, the /// [`InitiatorOrigin`]. pub type StreamingSchemeHandler = - Box>, StreamResponder) + Send + Sync>; + Box>, StreamResponder) + Send + Sync>; static REGISTRY: OnceLock>>> = OnceLock::new(); fn registry() -> &'static Mutex>> { - REGISTRY.get_or_init(|| Mutex::new(HashMap::new())) + REGISTRY.get_or_init(|| Mutex::new(HashMap::new())) } /// Register a streaming handler for `scheme` (e.g. `"duck"`). Later /// registrations for the same scheme replace earlier ones. The scheme must /// still be declared in `CefConfig::custom_schemes` so Chromium knows it. pub fn register_streaming_scheme_handler(scheme: &str, handler: StreamingSchemeHandler) { - registry() - .lock() - .expect("streaming registry poisoned") - .insert(scheme.to_string(), Arc::new(handler)); + registry() + .lock() + .expect("streaming registry poisoned") + .insert(scheme.to_string(), Arc::new(handler)); } /// The handler registered for `scheme`, if any. Used by the resource handler /// to decide whether a request streams or falls through to the buffered path. pub(crate) fn streaming_handler_for(scheme: &str) -> Option> { - registry() - .lock() - .expect("streaming registry poisoned") - .get(scheme) - .cloned() + registry() + .lock() + .expect("streaming registry poisoned") + .get(scheme) + .cloned() } /// Returned by [`StreamWriter::write`] when the renderer cancelled the request @@ -78,298 +78,298 @@ type WakeSlot = Arc>>>; /// Handed to a streaming handler. Send the head exactly once with /// [`respond`](Self::respond); that unblocks CEF and yields the body writer. pub struct StreamResponder { - head_slot: Arc>>>, - on_head: Box, - body_tx: SyncSender>, - wake: WakeSlot, + head_slot: Arc>>>, + on_head: Box, + body_tx: SyncSender>, + wake: WakeSlot, } impl StreamResponder { - /// Publish status + headers and return the body writer. CEF reports the - /// body length as unknown; the body ends when the writer is dropped or - /// [`finish`](StreamWriter::finish)ed. - pub fn respond(self, head: http::Response<()>) -> StreamWriter { - *self.head_slot.lock().expect("head slot poisoned") = Some(head); - (self.on_head)(); - StreamWriter { - body_tx: self.body_tx, - wake: self.wake, + /// Publish status + headers and return the body writer. CEF reports the + /// body length as unknown; the body ends when the writer is dropped or + /// [`finish`](StreamWriter::finish)ed. + pub fn respond(self, head: http::Response<()>) -> StreamWriter { + *self.head_slot.lock().expect("head slot poisoned") = Some(head); + (self.on_head)(); + StreamWriter { + body_tx: self.body_tx, + wake: self.wake, + } } - } } /// The producer side of a streaming body. Each [`write`](Self::write) is one /// chunk; the call blocks when the bounded in-flight budget is full. pub struct StreamWriter { - body_tx: SyncSender>, - wake: WakeSlot, + body_tx: SyncSender>, + wake: WakeSlot, } impl StreamWriter { - /// Enqueue one body chunk, blocking under backpressure. `Err(StreamClosed)` - /// means the renderer cancelled — stop producing. Empty chunks are dropped - /// (they would look like EOF to a naive reader). - pub fn write(&mut self, chunk: Vec) -> Result<(), StreamClosed> { - if chunk.is_empty() { - return Ok(()); + /// Enqueue one body chunk, blocking under backpressure. `Err(StreamClosed)` + /// means the renderer cancelled — stop producing. Empty chunks are dropped + /// (they would look like EOF to a naive reader). + pub fn write(&mut self, chunk: Vec) -> Result<(), StreamClosed> { + if chunk.is_empty() { + return Ok(()); + } + self.body_tx.send(chunk).map_err(|_| StreamClosed)?; + self.take_wake(); + Ok(()) } - self.body_tx.send(chunk).map_err(|_| StreamClosed)?; - self.take_wake(); - Ok(()) - } - - /// End the body. Equivalent to dropping the writer; explicit for clarity. - pub fn finish(self) {} - - fn take_wake(&self) { - // Release the wake-slot lock BEFORE running the waker: the waker - // (request_handler.rs's read glue) re-locks the shared StreamBody to pull - // the now-available chunk, and the CEF read thread parks under the same - // pair of locks in the opposite order. Invoking the waker while still - // holding the wake slot would close that lock cycle into a deadlock. - let wake = self.wake.lock().expect("wake slot poisoned").take(); - if let Some(wake) = wake { - wake(); + + /// End the body. Equivalent to dropping the writer; explicit for clarity. + pub fn finish(self) {} + + fn take_wake(&self) { + // Release the wake-slot lock BEFORE running the waker: the waker + // (request_handler.rs's read glue) re-locks the shared StreamBody to pull + // the now-available chunk, and the CEF read thread parks under the same + // pair of locks in the opposite order. Invoking the waker while still + // holding the wake slot would close that lock cycle into a deadlock. + let wake = self.wake.lock().expect("wake slot poisoned").take(); + if let Some(wake) = wake { + wake(); + } } - } } impl Drop for StreamWriter { - fn drop(&mut self) { - // Dropping body_tx disconnects the channel so the reader observes EOF; - // wake it in case it is parked on an empty-but-open stream. - self.take_wake(); - } + fn drop(&mut self) { + // Dropping body_tx disconnects the channel so the reader observes EOF; + // wake it in case it is parked on an empty-but-open stream. + self.take_wake(); + } } /// Outcome of one [`StreamBody::read`]. #[derive(Debug, PartialEq, Eq)] pub(crate) enum ReadOutcome { - /// `n` bytes copied into the output buffer. - Copied(usize), - /// No bytes available yet; the wake closure was parked and fires on the - /// next write. The CEF caller returns "keep reading" with 0 bytes. - Pending, - /// Producer finished and the buffer is drained. EOF. - Done, + /// `n` bytes copied into the output buffer. + Copied(usize), + /// No bytes available yet; the wake closure was parked and fires on the + /// next write. The CEF caller returns "keep reading" with 0 bytes. + Pending, + /// Producer finished and the buffer is drained. EOF. + Done, } /// The pull side CEF reads through. Holds a leftover cursor for a chunk that /// did not fit the last output buffer. pub(crate) struct StreamBody { - body_rx: Receiver>, - leftover: Vec, - cursor: usize, - wake: WakeSlot, + body_rx: Receiver>, + leftover: Vec, + cursor: usize, + wake: WakeSlot, } impl StreamBody { - /// Copy as much as fits into `out`. When nothing is buffered, park `wake` - /// (fired by the next write) and return [`ReadOutcome::Pending`]; once the - /// producer is gone and nothing is buffered, return [`ReadOutcome::Done`]. - pub(crate) fn read( - &mut self, - out: &mut [u8], - wake: impl FnOnce() + Send + 'static, - ) -> ReadOutcome { - if out.is_empty() { - return ReadOutcome::Pending; - } - if self.cursor >= self.leftover.len() { - match self.body_rx.try_recv() { - Ok(chunk) => { - self.leftover = chunk; - self.cursor = 0; + /// Copy as much as fits into `out`. When nothing is buffered, park `wake` + /// (fired by the next write) and return [`ReadOutcome::Pending`]; once the + /// producer is gone and nothing is buffered, return [`ReadOutcome::Done`]. + pub(crate) fn read( + &mut self, + out: &mut [u8], + wake: impl FnOnce() + Send + 'static, + ) -> ReadOutcome { + if out.is_empty() { + return ReadOutcome::Pending; } - Err(std::sync::mpsc::TryRecvError::Empty) => { - // Park, then re-check once: a write between the failed try_recv and - // storing the waker would otherwise be a lost wakeup. - *self.wake.lock().expect("wake slot poisoned") = Some(Box::new(wake)); - match self.body_rx.try_recv() { - Ok(chunk) => { - self.wake.lock().expect("wake slot poisoned").take(); - self.leftover = chunk; - self.cursor = 0; + if self.cursor >= self.leftover.len() { + match self.body_rx.try_recv() { + Ok(chunk) => { + self.leftover = chunk; + self.cursor = 0; + } + Err(std::sync::mpsc::TryRecvError::Empty) => { + // Park, then re-check once: a write between the failed try_recv and + // storing the waker would otherwise be a lost wakeup. + *self.wake.lock().expect("wake slot poisoned") = Some(Box::new(wake)); + match self.body_rx.try_recv() { + Ok(chunk) => { + self.wake.lock().expect("wake slot poisoned").take(); + self.leftover = chunk; + self.cursor = 0; + } + Err(std::sync::mpsc::TryRecvError::Empty) => return ReadOutcome::Pending, + Err(std::sync::mpsc::TryRecvError::Disconnected) => { + self.wake.lock().expect("wake slot poisoned").take(); + return ReadOutcome::Done; + } + } + } + Err(std::sync::mpsc::TryRecvError::Disconnected) => return ReadOutcome::Done, } - Err(std::sync::mpsc::TryRecvError::Empty) => return ReadOutcome::Pending, - Err(std::sync::mpsc::TryRecvError::Disconnected) => { - self.wake.lock().expect("wake slot poisoned").take(); - return ReadOutcome::Done; - } - } } - Err(std::sync::mpsc::TryRecvError::Disconnected) => return ReadOutcome::Done, - } + let n = (self.leftover.len() - self.cursor).min(out.len()); + out[..n].copy_from_slice(&self.leftover[self.cursor..self.cursor + n]); + self.cursor += n; + ReadOutcome::Copied(n) } - let n = (self.leftover.len() - self.cursor).min(out.len()); - out[..n].copy_from_slice(&self.leftover[self.cursor..self.cursor + n]); - self.cursor += n; - ReadOutcome::Copied(n) - } } /// Build a responder/body pair sharing one bounded channel and wake slot. The /// resource handler stores `head_slot`/`StreamBody`, spawns the handler with /// the `StreamResponder`, and wires `on_head` to `callback.cont()`. pub(crate) fn make_stream( - on_head: Box, + on_head: Box, ) -> ( - StreamResponder, - Arc>>>, - StreamBody, + StreamResponder, + Arc>>>, + StreamBody, ) { - let (body_tx, body_rx) = sync_channel::>(CHANNEL_DEPTH); - let head_slot = Arc::new(Mutex::new(None)); - let wake: WakeSlot = Arc::new(Mutex::new(None)); - let responder = StreamResponder { - head_slot: head_slot.clone(), - on_head, - body_tx, - wake: wake.clone(), - }; - let body = StreamBody { - body_rx, - leftover: Vec::new(), - cursor: 0, - wake, - }; - (responder, head_slot, body) + let (body_tx, body_rx) = sync_channel::>(CHANNEL_DEPTH); + let head_slot = Arc::new(Mutex::new(None)); + let wake: WakeSlot = Arc::new(Mutex::new(None)); + let responder = StreamResponder { + head_slot: head_slot.clone(), + on_head, + body_tx, + wake: wake.clone(), + }; + let body = StreamBody { + body_rx, + leftover: Vec::new(), + cursor: 0, + wake, + }; + (responder, head_slot, body) } #[cfg(test)] mod tests { - use super::*; - use std::sync::atomic::{AtomicUsize, Ordering}; - use std::thread; - use std::time::Duration; - - fn noop_wake() {} - - #[test] - fn drains_two_chunks_across_small_buffers_then_done() { - let (responder, _head, mut body) = make_stream(Box::new(|| {})); - let mut writer = responder.respond(http::Response::new(())); - writer.write(b"hello".to_vec()).unwrap(); - writer.write(b"world".to_vec()).unwrap(); - writer.finish(); - - let mut out = [0u8; 3]; - let mut got = Vec::new(); - loop { - match body.read(&mut out, noop_wake) { - ReadOutcome::Copied(n) => got.extend_from_slice(&out[..n]), - ReadOutcome::Done => break, - ReadOutcome::Pending => panic!("producer already finished, must not park"), - } + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::thread; + use std::time::Duration; + + fn noop_wake() {} + + #[test] + fn drains_two_chunks_across_small_buffers_then_done() { + let (responder, _head, mut body) = make_stream(Box::new(|| {})); + let mut writer = responder.respond(http::Response::new(())); + writer.write(b"hello".to_vec()).unwrap(); + writer.write(b"world".to_vec()).unwrap(); + writer.finish(); + + let mut out = [0u8; 3]; + let mut got = Vec::new(); + loop { + match body.read(&mut out, noop_wake) { + ReadOutcome::Copied(n) => got.extend_from_slice(&out[..n]), + ReadOutcome::Done => break, + ReadOutcome::Pending => panic!("producer already finished, must not park"), + } + } + assert_eq!(got, b"helloworld"); } - assert_eq!(got, b"helloworld"); - } - - #[test] - fn read_on_empty_open_stream_parks_and_a_write_wakes_it() { - let (responder, _head, mut body) = make_stream(Box::new(|| {})); - let mut writer = responder.respond(http::Response::new(())); - - let woke = Arc::new(AtomicUsize::new(0)); - let w = woke.clone(); - let mut out = [0u8; 8]; - // Nothing written yet: must park and register the waker. - assert_eq!( - body.read(&mut out, move || { - w.fetch_add(1, Ordering::SeqCst); - }), - ReadOutcome::Pending - ); - assert_eq!(woke.load(Ordering::SeqCst), 0); - - // The write must fire the parked waker exactly once. - writer.write(b"data".to_vec()).unwrap(); - assert_eq!(woke.load(Ordering::SeqCst), 1); - - match body.read(&mut out, noop_wake) { - ReadOutcome::Copied(n) => assert_eq!(&out[..n], b"data"), - other => panic!("expected Copied, got {other:?}"), + + #[test] + fn read_on_empty_open_stream_parks_and_a_write_wakes_it() { + let (responder, _head, mut body) = make_stream(Box::new(|| {})); + let mut writer = responder.respond(http::Response::new(())); + + let woke = Arc::new(AtomicUsize::new(0)); + let w = woke.clone(); + let mut out = [0u8; 8]; + // Nothing written yet: must park and register the waker. + assert_eq!( + body.read(&mut out, move || { + w.fetch_add(1, Ordering::SeqCst); + }), + ReadOutcome::Pending + ); + assert_eq!(woke.load(Ordering::SeqCst), 0); + + // The write must fire the parked waker exactly once. + writer.write(b"data".to_vec()).unwrap(); + assert_eq!(woke.load(Ordering::SeqCst), 1); + + match body.read(&mut out, noop_wake) { + ReadOutcome::Copied(n) => assert_eq!(&out[..n], b"data"), + other => panic!("expected Copied, got {other:?}"), + } } - } - - #[test] - fn write_after_reader_dropped_returns_stream_closed() { - let (responder, _head, body) = make_stream(Box::new(|| {})); - let mut writer = responder.respond(http::Response::new(())); - drop(body); - assert_eq!(writer.write(b"x".to_vec()), Err(StreamClosed)); - } - - #[test] - fn bounded_channel_blocks_producer_until_reader_drains() { - let (responder, _head, mut body) = make_stream(Box::new(|| {})); - let mut writer = responder.respond(http::Response::new(())); - - let progressed = Arc::new(AtomicUsize::new(0)); - let p = progressed.clone(); - // CHANNEL_DEPTH accepted immediately, then two more must block. - let producer = thread::spawn(move || { - for i in 0..(CHANNEL_DEPTH + 2) { - writer.write(vec![i as u8]).unwrap(); - p.fetch_add(1, Ordering::SeqCst); - } - }); - - // Give the producer time to fill the buffer and block. - thread::sleep(Duration::from_millis(50)); - let filled = progressed.load(Ordering::SeqCst); - assert!( - filled <= CHANNEL_DEPTH + 1, - "producer should have blocked around the channel bound, got {filled}" - ); - - // Drain everything; the producer then completes. - let mut out = [0u8; 1]; - let mut count = 0; - loop { - match body.read(&mut out, noop_wake) { - ReadOutcome::Copied(_) => count += 1, - ReadOutcome::Done => break, - ReadOutcome::Pending => thread::sleep(Duration::from_millis(1)), - } + + #[test] + fn write_after_reader_dropped_returns_stream_closed() { + let (responder, _head, body) = make_stream(Box::new(|| {})); + let mut writer = responder.respond(http::Response::new(())); + drop(body); + assert_eq!(writer.write(b"x".to_vec()), Err(StreamClosed)); + } + + #[test] + fn bounded_channel_blocks_producer_until_reader_drains() { + let (responder, _head, mut body) = make_stream(Box::new(|| {})); + let mut writer = responder.respond(http::Response::new(())); + + let progressed = Arc::new(AtomicUsize::new(0)); + let p = progressed.clone(); + // CHANNEL_DEPTH accepted immediately, then two more must block. + let producer = thread::spawn(move || { + for i in 0..(CHANNEL_DEPTH + 2) { + writer.write(vec![i as u8]).unwrap(); + p.fetch_add(1, Ordering::SeqCst); + } + }); + + // Give the producer time to fill the buffer and block. + thread::sleep(Duration::from_millis(50)); + let filled = progressed.load(Ordering::SeqCst); + assert!( + filled <= CHANNEL_DEPTH + 1, + "producer should have blocked around the channel bound, got {filled}" + ); + + // Drain everything; the producer then completes. + let mut out = [0u8; 1]; + let mut count = 0; + loop { + match body.read(&mut out, noop_wake) { + ReadOutcome::Copied(_) => count += 1, + ReadOutcome::Done => break, + ReadOutcome::Pending => thread::sleep(Duration::from_millis(1)), + } + } + producer.join().unwrap(); + assert_eq!(count, CHANNEL_DEPTH + 2); + assert_eq!(progressed.load(Ordering::SeqCst), CHANNEL_DEPTH + 2); } - producer.join().unwrap(); - assert_eq!(count, CHANNEL_DEPTH + 2); - assert_eq!(progressed.load(Ordering::SeqCst), CHANNEL_DEPTH + 2); - } - - #[test] - fn empty_chunks_are_dropped_not_treated_as_eof() { - let (responder, _head, mut body) = make_stream(Box::new(|| {})); - let mut writer = responder.respond(http::Response::new(())); - writer.write(Vec::new()).unwrap(); - writer.write(b"real".to_vec()).unwrap(); - writer.finish(); - - let mut out = [0u8; 8]; - match body.read(&mut out, noop_wake) { - ReadOutcome::Copied(n) => assert_eq!(&out[..n], b"real"), - other => panic!("empty write must not end the stream, got {other:?}"), + + #[test] + fn empty_chunks_are_dropped_not_treated_as_eof() { + let (responder, _head, mut body) = make_stream(Box::new(|| {})); + let mut writer = responder.respond(http::Response::new(())); + writer.write(Vec::new()).unwrap(); + writer.write(b"real".to_vec()).unwrap(); + writer.finish(); + + let mut out = [0u8; 8]; + match body.read(&mut out, noop_wake) { + ReadOutcome::Copied(n) => assert_eq!(&out[..n], b"real"), + other => panic!("empty write must not end the stream, got {other:?}"), + } + } + + #[test] + fn respond_publishes_head_and_fires_on_head_once() { + let fired = Arc::new(AtomicUsize::new(0)); + let f = fired.clone(); + let (responder, head_slot, _body) = make_stream(Box::new(move || { + f.fetch_add(1, Ordering::SeqCst); + })); + let resp = http::Response::builder() + .status(201) + .header("content-type", "text/event-stream") + .body(()) + .unwrap(); + let _writer = responder.respond(resp); + assert_eq!(fired.load(Ordering::SeqCst), 1); + let stored = head_slot.lock().unwrap(); + let stored = stored.as_ref().expect("head published"); + assert_eq!(stored.status(), 201); + assert_eq!(stored.headers()["content-type"], "text/event-stream"); } - } - - #[test] - fn respond_publishes_head_and_fires_on_head_once() { - let fired = Arc::new(AtomicUsize::new(0)); - let f = fired.clone(); - let (responder, head_slot, _body) = make_stream(Box::new(move || { - f.fetch_add(1, Ordering::SeqCst); - })); - let resp = http::Response::builder() - .status(201) - .header("content-type", "text/event-stream") - .body(()) - .unwrap(); - let _writer = responder.respond(resp); - assert_eq!(fired.load(Ordering::SeqCst), 1); - let stored = head_slot.lock().unwrap(); - let stored = stored.as_ref().expect("head published"); - assert_eq!(stored.status(), 201); - assert_eq!(stored.headers()["content-type"], "text/event-stream"); - } } diff --git a/src/webview.rs b/src/webview.rs index 56c7593..1c35cd0 100644 --- a/src/webview.rs +++ b/src/webview.rs @@ -5,18 +5,18 @@ use std::collections::HashMap; use std::sync::Arc; use std::sync::{ - Mutex, - atomic::{AtomicI32, Ordering}, - mpsc::{self, Receiver, Sender}, + Mutex, + atomic::{AtomicI32, Ordering}, + mpsc::{self, Receiver, Sender}, }; use cef::*; use sha2::{Digest, Sha256}; use tauri_runtime::{ - Cookie, Error, Result, UserEvent, WebviewDispatch, WebviewEventId, - dpi::{PhysicalPosition, PhysicalSize, Position, Rect, Size}, - webview::{DetachedWebview, InitializationScript, PendingWebview, WebviewAttributes}, - window::{WebviewEvent, WindowId}, + Cookie, Error, Result, UserEvent, WebviewDispatch, WebviewEventId, + dpi::{PhysicalPosition, PhysicalSize, Position, Rect, Size}, + webview::{DetachedWebview, InitializationScript, PendingWebview, WebviewAttributes}, + window::{WebviewEvent, WindowId}, }; use tauri_utils::{Theme, config::Color, html::normalize_script_for_csp}; use url::Url; @@ -32,37 +32,37 @@ use crate::window::AppWindow; /// [`tauri_runtime::WebviewDispatch::with_webview`]. #[derive(Clone)] pub struct Webview { - browser: cef::Browser, + browser: cef::Browser, } impl Webview { - pub(crate) fn new(browser: cef::Browser) -> Self { - Self { browser } - } + pub(crate) fn new(browser: cef::Browser) -> Self { + Self { browser } + } - /// Returns the [`cef::Browser`] backing this webview. - /// - /// From the browser you can reach the rest of the CEF API, such as the - /// browser host, the main frame or the native window handle. - pub fn browser(&self) -> cef::Browser { - self.browser.clone() - } + /// Returns the [`cef::Browser`] backing this webview. + /// + /// From the browser you can reach the rest of the CEF API, such as the + /// browser host, the main frame or the native window handle. + pub fn browser(&self) -> cef::Browser { + self.browser.clone() + } } pub fn webview_version() -> tauri_runtime::Result { - Ok(format!( - "{}.{}.{}.{}", - cef::sys::CHROME_VERSION_MAJOR, - cef::sys::CHROME_VERSION_MINOR, - cef::sys::CHROME_VERSION_PATCH, - cef::sys::CHROME_VERSION_BUILD - )) + Ok(format!( + "{}.{}.{}.{}", + cef::sys::CHROME_VERSION_MAJOR, + cef::sys::CHROME_VERSION_MINOR, + cef::sys::CHROME_VERSION_PATCH, + cef::sys::CHROME_VERSION_BUILD + )) } #[inline] fn color_to_argb(color: Color) -> u32 { - let (r, g, b, a) = color.into(); - ((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32) + let (r, g, b, a) = color.into(); + ((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32) } /// Maps the subset of [`WebviewAttributes`] that CEF's `BrowserSettings` @@ -84,39 +84,39 @@ fn color_to_argb(color: Color) -> u32 { /// /// `proxy_url` is handled separately via the request context preference. fn browser_settings_from_webview_attributes( - webview_attributes: &WebviewAttributes, + webview_attributes: &WebviewAttributes, ) -> cef::BrowserSettings { - cef::BrowserSettings { - javascript: cef::State::from(if webview_attributes.javascript_disabled { - cef::sys::cef_state_t::STATE_DISABLED - } else { - cef::sys::cef_state_t::STATE_ENABLED - }), - javascript_access_clipboard: cef::State::from(if webview_attributes.clipboard { - cef::sys::cef_state_t::STATE_ENABLED - } else { - cef::sys::cef_state_t::STATE_DISABLED - }), - background_color: webview_attributes - .background_color - .map(color_to_argb) - .unwrap_or(0), - ..Default::default() - } + cef::BrowserSettings { + javascript: cef::State::from(if webview_attributes.javascript_disabled { + cef::sys::cef_state_t::STATE_DISABLED + } else { + cef::sys::cef_state_t::STATE_ENABLED + }), + javascript_access_clipboard: cef::State::from(if webview_attributes.clipboard { + cef::sys::cef_state_t::STATE_ENABLED + } else { + cef::sys::cef_state_t::STATE_DISABLED + }), + background_color: webview_attributes + .background_color + .map(color_to_argb) + .unwrap_or(0), + ..Default::default() + } } #[derive(Debug, Clone)] pub enum DevToolsProtocol { - Message(Vec), - Event { - method: String, - params: Vec, - }, - MethodResult { - message_id: i32, - success: bool, - result: Vec, - }, + Message(Vec), + Event { + method: String, + params: Vec, + }, + MethodResult { + message_id: i32, + success: bool, + result: Vec, + }, } pub(crate) type DevToolsProtocolHandler = dyn Fn(DevToolsProtocol) + Send + Sync; @@ -124,1089 +124,1094 @@ pub(crate) type WebviewEventHandler = Box; pub(crate) type WebviewEventListeners = Arc>>; pub(crate) enum WebviewMessage { - AddEventListener(WebviewEventId, Box), - EvaluateScript(String), - EvaluateScriptWithCallback(String, Box), - Navigate(Url), - Reload, - GoBack, - CanGoBack(Sender>), - GoForward, - CanGoForward(Sender>), - Print, - Close, - Show, - Hide, - SetPosition(Position), - SetSize(Size), - SetBounds(Rect), - SetFocus, - Reparent(WindowId, Sender>), - SetAutoResize(bool), - SetZoom(f64), - SetBackgroundColor(Option), - ClearAllBrowsingData, - Url(Sender>), - Bounds(Sender>), - Position(Sender>>), - Size(Sender>>), - WithWebview(Box), - CookiesForUrl(Url, Sender>>>), - Cookies(Sender>>>), - SetCookie(Cookie<'static>), - DeleteCookie(Cookie<'static>), - #[cfg(any(debug_assertions, feature = "devtools"))] - OpenDevTools, - #[cfg(any(debug_assertions, feature = "devtools"))] - CloseDevTools, - #[cfg(any(debug_assertions, feature = "devtools"))] - IsDevToolsOpen(Sender), - SendDevToolsMessage(Vec, Sender>), - OnDevToolsProtocol(Arc, Sender>), + AddEventListener(WebviewEventId, Box), + EvaluateScript(String), + EvaluateScriptWithCallback(String, Box), + Navigate(Url), + Reload, + GoBack, + CanGoBack(Sender>), + GoForward, + CanGoForward(Sender>), + Print, + Close, + Show, + Hide, + SetPosition(Position), + SetSize(Size), + SetBounds(Rect), + SetFocus, + Reparent(WindowId, Sender>), + SetAutoResize(bool), + SetZoom(f64), + SetBackgroundColor(Option), + ClearAllBrowsingData, + Url(Sender>), + Bounds(Sender>), + Position(Sender>>), + Size(Sender>>), + WithWebview(Box), + CookiesForUrl(Url, Sender>>>), + Cookies(Sender>>>), + SetCookie(Cookie<'static>), + DeleteCookie(Cookie<'static>), + #[cfg(any(debug_assertions, feature = "devtools"))] + OpenDevTools, + #[cfg(any(debug_assertions, feature = "devtools"))] + CloseDevTools, + #[cfg(any(debug_assertions, feature = "devtools"))] + IsDevToolsOpen(Sender), + SendDevToolsMessage(Vec, Sender>), + OnDevToolsProtocol(Arc, Sender>), } /// A webview's bounds expressed as a fraction of its parent window, used to /// reposition/resize auto-resize webviews when the parent window changes size. #[derive(Clone, Copy)] pub(crate) struct BoundsRate { - pub(crate) x: f32, - pub(crate) y: f32, - pub(crate) width: f32, - pub(crate) height: f32, + pub(crate) x: f32, + pub(crate) y: f32, + pub(crate) width: f32, + pub(crate) height: f32, } impl Default for BoundsRate { - fn default() -> Self { - Self { - x: 0., - y: 0., - width: 1., - height: 1., + fn default() -> Self { + Self { + x: 0., + y: 0., + width: 1., + height: 1., + } } - } } pub(crate) struct AppWebview { - pub(crate) webview_id: u32, - pub(crate) label: String, - pub(crate) browser: cef::Browser, - pub(crate) browser_id: i32, - pub(crate) host: cef::BrowserHost, - pub(crate) uri_scheme_protocols: Arc>>>, - pub(crate) devtools_protocol_handlers: Arc>>>, - /// Keeps the DevTools message observer registered. Dropping this unregisters the observer. - pub(crate) devtools_observer_registration: Arc>>, - pub(crate) listeners: WebviewEventListeners, - pub(crate) bounds_rate: Option, + pub(crate) webview_id: u32, + pub(crate) label: String, + pub(crate) browser: cef::Browser, + pub(crate) browser_id: i32, + pub(crate) host: cef::BrowserHost, + pub(crate) uri_scheme_protocols: Arc>>>, + pub(crate) devtools_protocol_handlers: Arc>>>, + /// Keeps the DevTools message observer registered. Dropping this unregisters the observer. + pub(crate) devtools_observer_registration: Arc>>, + pub(crate) listeners: WebviewEventListeners, + pub(crate) bounds_rate: Option, } impl AppWebview { - pub(crate) fn set_bounds(&mut self, parent_size: PhysicalSize, scale: f64, bounds: Rect) { - let position = bounds.position.to_physical::(scale); - let size = bounds.size.to_physical::(scale); + pub(crate) fn set_bounds(&mut self, parent_size: PhysicalSize, scale: f64, bounds: Rect) { + let position = bounds.position.to_physical::(scale); + let size = bounds.size.to_physical::(scale); + + let x = position.x; + let y = position.y; + let w = size.width as i32; + let h = size.height as i32; + + if self.bounds_rate.is_some() { + let win_w = parent_size.width.max(1) as f32; + let win_h = parent_size.height.max(1) as f32; + self.bounds_rate = Some(BoundsRate { + x: x as f32 / win_w, + y: y as f32 / win_h, + width: w as f32 / win_w, + height: h as f32 / win_h, + }); + } - let x = position.x; - let y = position.y; - let w = size.width as i32; - let h = size.height as i32; - - if self.bounds_rate.is_some() { - let win_w = parent_size.width.max(1) as f32; - let win_h = parent_size.height.max(1) as f32; - self.bounds_rate = Some(BoundsRate { - x: x as f32 / win_w, - y: y as f32 / win_h, - width: w as f32 / win_w, - height: h as f32 / win_h, - }); + self.host.notify_move_or_resize_started(); + self.apply_physical_bounds(scale, x, y, w, h); + self.host.was_resized(); } - self.host.notify_move_or_resize_started(); - self.apply_physical_bounds(scale, x, y, w, h); - self.host.was_resized(); - } - - pub(crate) fn set_visible(&self, visible: bool) { - self.host.was_hidden(if visible { 0 } else { 1 }); - self.apply_visible(visible); - } + pub(crate) fn set_visible(&self, visible: bool) { + self.host.was_hidden(if visible { 0 } else { 1 }); + self.apply_visible(visible); + } - pub fn url(&self) -> Option { - self - .browser - .main_frame() - .map(|frame| cef::CefString::from(&frame.url()).to_string()) - } + pub fn url(&self) -> Option { + self.browser + .main_frame() + .map(|frame| cef::CefString::from(&frame.url()).to_string()) + } } impl WinitCefApp { - pub(crate) fn create_webview( - &mut self, - window_id: WindowId, - webview_id: u32, - pending: PendingWebview>, - ) -> Result<()> { - let Self { - context, - scheme_registry, - state, - .. - } = self; - let Some(appwindow) = state.windows.get_mut(&window_id) else { - return Err(Error::CreateWebview( - format!("window {window_id:?} does not exist").into(), - )); - }; - Self::build_and_attach_webview( - context, - scheme_registry, - &mut state.live_browsers, - appwindow, - webview_id, - browser_client::DragDropEventTarget::Webview, - pending, - ) - } - - /// Builds a webview and attaches it to `appwindow`, bumping `live_browsers` - /// and relaying it out. Works whether `appwindow` already lives in `state` or - /// is still being assembled, so window and child creation share one path. - pub(crate) fn build_and_attach_webview( - context: &RuntimeContext, - scheme_registry: &request_handler::SchemeRegistry, - live_browsers: &mut usize, - appwindow: &mut AppWindow, - webview_id: u32, - drag_drop_event_target: browser_client::DragDropEventTarget, - pending: PendingWebview>, - ) -> Result<()> { - let parent = appwindow.raw_cef_handle(); - let parent_size = appwindow.window.surface_size(); - let scale = appwindow.window.scale_factor(); - let app_wide_theme = *context.app_wide_theme.lock().unwrap(); - let theme = appwindow.resolved_theme(app_wide_theme); - let Some(child) = Self::build_browser_child( - context, - scheme_registry, - appwindow.id, - webview_id, - parent, - parent_size, - scale, - theme, - drag_drop_event_target, - pending, - ) else { - return Err(Error::CreateWebview( - "failed to create CEF browser".to_string().into(), - )); - }; - - *live_browsers += 1; - appwindow.children.push(child); - layout_app_window(appwindow); - Ok(()) - } - - pub(crate) fn build_browser_child( - context: &RuntimeContext, - scheme_registry: &request_handler::SchemeRegistry, - window_id: WindowId, - webview_id: u32, - parent: cef::sys::cef_window_handle_t, - parent_size: PhysicalSize, - scale: f64, - theme: Option, - drag_drop_event_target: browser_client::DragDropEventTarget, - mut pending: PendingWebview>, - ) -> Option { - let bounds_rate = compute_child_bounds_rate( - pending.webview_attributes.bounds.as_ref(), - pending.webview_attributes.auto_resize, - parent_size, - scale, - ); - let initialization_scripts = initialization_scripts(&mut pending.webview_attributes); - let uri_scheme_protocols: Arc> = Arc::new( - pending - .uri_scheme_protocols - .into_iter() - .map(|(scheme, handler)| (scheme, Arc::new(handler))) - .collect(), - ); - let on_page_load_handler = pending.on_page_load_handler.take().map(Arc::from); - let document_title_changed_handler = - pending.document_title_changed_handler.take().map(Arc::from); - // Published PendingWebview has no address-changed channel (feat/cef-only); - // the client plumbing stays for when upstream ships it. - let address_changed_handler: Option> = None; - let devtools_enabled = (cfg!(debug_assertions) || cfg!(feature = "devtools")) - && pending.webview_attributes.devtools.unwrap_or(true); - let drag_drop_handler_enabled = pending.webview_attributes.drag_drop_handler_enabled; - let drag_drop_state = Arc::new(Mutex::new(browser_client::DragDropState::default())); - #[cfg(any(target_os = "macos", target_os = "ios"))] - let web_content_process_terminate_handler = pending - .on_web_content_process_terminate_handler - .take() - .map(|handler| Arc::from(handler) as Arc); - #[cfg(not(any(target_os = "macos", target_os = "ios")))] - let web_content_process_terminate_handler: Option> = None; - let handlers = browser_client::TauriCefBrowserClientHandlers { - ipc_handler: pending.ipc_handler.map(Arc::from), - on_page_load_handler, - document_title_changed_handler, - navigation_handler: pending.navigation_handler.map(Arc::from), - address_changed_handler, - new_window_handler: pending.new_window_handler.map(Arc::from), - download_handler: pending.download_handler.take(), - web_content_process_terminate_handler, - }; - - let mut client = browser_client::TauriCefBrowserClient::new( - context.clone(), - window_id, - webview_id, - pending.label.clone(), - Some(pending.url.as_str().to_string()), - devtools_enabled, - drag_drop_event_target, - drag_drop_handler_enabled, - drag_drop_state, - handlers, - context.proxy.clone(), - context.sender.clone(), - ); - - // If the bounds are not specified, default to the parent window's size and position. - // aka full-window webview. - let bounds = pending.webview_attributes.bounds.unwrap_or_else(|| Rect { - position: PhysicalPosition::new(0, 0).into(), - size: parent_size.into(), - }); - #[cfg(not(target_os = "macos"))] - let bounds = compat::rect_to_physical::(bounds, scale); - #[cfg(target_os = "macos")] - let bounds = compat::rect_to_logical::(bounds, scale); - let bounds = cef::Rect { - x: bounds.position.x, - y: bounds.position.y, - width: bounds.size.width, - height: bounds.size.height, - }; - - // Published PendingWebview has no per-webview platform attribute channel - // (feat/cef-only), so the runtime style is always CEF's default. - let cef_runtime_style = cef::RuntimeStyle::DEFAULT; - - let mut window_info = cef::WindowInfo::default().set_as_child(parent, &bounds); - window_info.runtime_style = cef_runtime_style; - let settings = browser_settings_from_webview_attributes(&pending.webview_attributes); - - let custom_protocol_scheme = if pending.webview_attributes.use_https_scheme { - "https" - } else { - "http" - } - .to_string(); - let custom_scheme_domain_names: Vec = uri_scheme_protocols - .keys() - .map(|scheme| format!("{scheme}.localhost")) - .collect(); - let real_initial_url = pending.url.as_str().to_string(); - let (browser_tx, browser_rx) = mpsc::channel(); - let (init_done, on_initialized) = request_context::deferred_init_continuation({ - let scheme_registry = scheme_registry.clone(); - let uri_scheme_protocols = uri_scheme_protocols.clone(); - let initialization_scripts = initialization_scripts.clone(); - let custom_protocol_scheme = custom_protocol_scheme.clone(); - let custom_scheme_domain_names = custom_scheme_domain_names.clone(); - let label = pending.label.clone(); - move |mut request_context| { - request_context::apply_theme_scheme(request_context.as_ref(), theme); - - // Create with an inert document so the BrowserHost exists before the real - // navigation; the real URL is loaded once the document-start script is set. - let initial_url = CefString::from(INITIAL_LOAD_URL); - let Some(browser) = cef::browser_host_create_browser_sync( - Some(&window_info), - Some(&mut client), - Some(&initial_url), - Some(&settings), - None, - request_context.as_mut(), - ) else { - log::error!("failed to create CEF browser for webview {label:?}"); - return; - }; - let Some(host) = browser.host() else { - log::error!("CEF browser for webview {label:?} has no host"); - return; + pub(crate) fn create_webview( + &mut self, + window_id: WindowId, + webview_id: u32, + pending: PendingWebview>, + ) -> Result<()> { + let Self { + context, + scheme_registry, + state, + .. + } = self; + let Some(appwindow) = state.windows.get_mut(&window_id) else { + return Err(Error::CreateWebview( + format!("window {window_id:?} does not exist").into(), + )); }; - let browser_id = browser.identifier(); - - { - let mut registry = scheme_registry.lock().unwrap(); - for (scheme, handler) in uri_scheme_protocols.iter() { - registry.insert( - (browser_id, scheme.clone()), - ( - label.clone(), - handler.clone(), - initialization_scripts.clone(), - ), - ); - } - } - - let devtools_protocol_handlers = Arc::new(Mutex::new(Vec::new())); - let pending_initial_loads: PendingInitialLoads = Arc::new(Mutex::new(HashMap::new())); - let devtools_observer_registration = Arc::new(Mutex::new(add_dev_tools_observer( - &browser, - devtools_protocol_handlers.clone(), - pending_initial_loads.clone(), - ))); - load_initial_url_after_registering_initialization_scripts( - &browser, - &initialization_scripts, - &custom_protocol_scheme, - &custom_scheme_domain_names, - &real_initial_url, - &pending_initial_loads, - ); - - browser_tx - .send(AppWebview { + Self::build_and_attach_webview( + context, + scheme_registry, + &mut state.live_browsers, + appwindow, webview_id, - label, - browser, - browser_id, - host, - uri_scheme_protocols, - devtools_protocol_handlers, - devtools_observer_registration, - listeners: Default::default(), - bounds_rate, - }) - .expect("failed to send initialized CEF browser"); - } - }); - let request_context = request_context::request_context_from_webview_attributes( - &context.cache_path, - &pending.webview_attributes, - uri_scheme_protocols.keys(), - &custom_protocol_scheme, - scheme_registry.clone(), - on_initialized, - ); - if request_context.is_none() { - init_done.store(true, Ordering::SeqCst); + browser_client::DragDropEventTarget::Webview, + pending, + ) } - request_context::wait_for_deferred_init(&init_done); - // `None` here means browser creation failed (or the request context never - // initialized); the continuation logs the reason. Soft-fail instead of - // taking down the whole process. - browser_rx.recv().ok() - } + /// Builds a webview and attaches it to `appwindow`, bumping `live_browsers` + /// and relaying it out. Works whether `appwindow` already lives in `state` or + /// is still being assembled, so window and child creation share one path. + pub(crate) fn build_and_attach_webview( + context: &RuntimeContext, + scheme_registry: &request_handler::SchemeRegistry, + live_browsers: &mut usize, + appwindow: &mut AppWindow, + webview_id: u32, + drag_drop_event_target: browser_client::DragDropEventTarget, + pending: PendingWebview>, + ) -> Result<()> { + let parent = appwindow.raw_cef_handle(); + let parent_size = appwindow.window.surface_size(); + let scale = appwindow.window.scale_factor(); + let app_wide_theme = *context.app_wide_theme.lock().unwrap(); + let theme = appwindow.resolved_theme(app_wide_theme); + let Some(child) = Self::build_browser_child( + context, + scheme_registry, + appwindow.id, + webview_id, + parent, + parent_size, + scale, + theme, + drag_drop_event_target, + pending, + ) else { + return Err(Error::CreateWebview( + "failed to create CEF browser".to_string().into(), + )); + }; - pub(crate) fn handle_webview_message( - &mut self, - window_id: WindowId, - webview_id: u32, - message: WebviewMessage, - ) { - // If the runtime is exiting, don't process any more messages to avoid macOS crash on exit. - if self.state.exiting { - return; + *live_browsers += 1; + appwindow.children.push(child); + layout_app_window(appwindow); + Ok(()) } - let Some(appwindow) = self.state.windows.get_mut(&window_id) else { - return; - }; - let Some(child) = appwindow - .children - .iter_mut() - .find(|child| child.webview_id == webview_id) - else { - return; - }; - - match message { - WebviewMessage::EvaluateScript(script) => { - if let Some(frame) = child.browser.main_frame() { - let script = cef::CefString::from(script.as_str()); - let url = cef::CefString::from(""); - frame.execute_java_script(Some(&script), Some(&url), 0); - } - } - WebviewMessage::EvaluateScriptWithCallback(script, callback) => { - let host = &child.host; - let message_id = self.context.next_webview_event_id() as i32 + 1; - let message_id = Arc::new(AtomicI32::new(message_id)); - let callback = Arc::new(Mutex::new(Some(callback))); - let registration = Arc::new(Mutex::new(None)); - let mut observer = EvalScriptWithCallbackDevToolsObserver::new( - message_id.clone(), - callback.clone(), - registration.clone(), + pub(crate) fn build_browser_child( + context: &RuntimeContext, + scheme_registry: &request_handler::SchemeRegistry, + window_id: WindowId, + webview_id: u32, + parent: cef::sys::cef_window_handle_t, + parent_size: PhysicalSize, + scale: f64, + theme: Option, + drag_drop_event_target: browser_client::DragDropEventTarget, + mut pending: PendingWebview>, + ) -> Option { + let bounds_rate = compute_child_bounds_rate( + pending.webview_attributes.bounds.as_ref(), + pending.webview_attributes.auto_resize, + parent_size, + scale, + ); + let initialization_scripts = initialization_scripts(&mut pending.webview_attributes); + let uri_scheme_protocols: Arc> = Arc::new( + pending + .uri_scheme_protocols + .into_iter() + .map(|(scheme, handler)| (scheme, Arc::new(handler))) + .collect(), ); + let on_page_load_handler = pending.on_page_load_handler.take().map(Arc::from); + let document_title_changed_handler = + pending.document_title_changed_handler.take().map(Arc::from); + // Published PendingWebview has no address-changed channel (feat/cef-only); + // the client plumbing stays for when upstream ships it. + let address_changed_handler: Option> = None; + let devtools_enabled = (cfg!(debug_assertions) || cfg!(feature = "devtools")) + && pending.webview_attributes.devtools.unwrap_or(true); + let drag_drop_handler_enabled = pending.webview_attributes.drag_drop_handler_enabled; + let drag_drop_state = Arc::new(Mutex::new(browser_client::DragDropState::default())); + #[cfg(any(target_os = "macos", target_os = "ios"))] + let web_content_process_terminate_handler = pending + .on_web_content_process_terminate_handler + .take() + .map(|handler| Arc::from(handler) as Arc); + #[cfg(not(any(target_os = "macos", target_os = "ios")))] + let web_content_process_terminate_handler: Option> = None; + let handlers = browser_client::TauriCefBrowserClientHandlers { + ipc_handler: pending.ipc_handler.map(Arc::from), + on_page_load_handler, + document_title_changed_handler, + navigation_handler: pending.navigation_handler.map(Arc::from), + address_changed_handler, + new_window_handler: pending.new_window_handler.map(Arc::from), + download_handler: pending.download_handler.take(), + web_content_process_terminate_handler, + }; - if let Some(observer_registration) = - host.add_dev_tools_message_observer(Some(&mut observer)) - { - *registration.lock().unwrap() = Some(observer_registration); - - let message = serde_json::json!({ - "id": message_id.load(Ordering::Relaxed), - "method": "Runtime.evaluate", - "params": { - "expression": script, - "returnByValue": true, - } - }) - .to_string(); + let mut client = browser_client::TauriCefBrowserClient::new( + context.clone(), + window_id, + webview_id, + pending.label.clone(), + Some(pending.url.as_str().to_string()), + devtools_enabled, + drag_drop_event_target, + drag_drop_handler_enabled, + drag_drop_state, + handlers, + context.proxy.clone(), + context.sender.clone(), + ); - if host.send_dev_tools_message(Some(message.as_bytes())) != 1 { - let _ = registration.lock().unwrap().take(); - if let Some(callback) = callback.lock().unwrap().take() { - callback(String::new()); - } - } - } else if let Some(callback) = callback.lock().unwrap().take() { - callback(String::new()); - } - } - WebviewMessage::Navigate(url) => { - if let Some(frame) = child.browser.main_frame() { - frame.load_url(Some(&cef::CefString::from(url.as_str()))); - } - } - WebviewMessage::Reload => child.browser.reload(), - WebviewMessage::GoBack => child.browser.go_back(), - WebviewMessage::CanGoBack(tx) => _ = tx.send(Ok(child.browser.can_go_back() == 1)), - WebviewMessage::GoForward => child.browser.go_forward(), - WebviewMessage::CanGoForward(tx) => _ = tx.send(Ok(child.browser.can_go_forward() == 1)), - // Tauri's Webview::close() is an unconditional native lifecycle action, - // not a page-requested window.close(). A non-forced CEF close may leave - // the child browser (and publisher code) alive indefinitely, and its late - // callback can race parent-window bookkeeping. Window/app teardown already - // uses force_close=true; standalone child close needs the same semantics. - WebviewMessage::Close => { - child.host.close_browser(1); - // Windowed CEF browsers are not destroyed by CloseBrowser alone: the - // native child hierarchy must also be torn down before OnBeforeClose - // runs. Leaving it attached leaks the renderer; letting CEF forward a - // close to its top-level parent can close the whole Tauri window. - child.destroy_native(); - } - WebviewMessage::SetBounds(bounds) => { - let parent_size = appwindow.window.surface_size(); - let scale = appwindow.window.scale_factor(); - child.set_bounds(parent_size, scale, bounds); - } - WebviewMessage::SetSize(size) => { - let parent_size = appwindow.window.surface_size(); - let scale = appwindow.window.scale_factor(); - let bounds = child.bounds().unwrap_or_default(); - let new_bounds = Rect { - position: bounds.position, - size, - }; - child.set_bounds(parent_size, scale, new_bounds); - } - WebviewMessage::SetPosition(position) => { - let parent_size = appwindow.window.surface_size(); - let scale = appwindow.window.scale_factor(); - let bounds = child.bounds().unwrap_or_default(); - let new_bounds = Rect { - position, - size: bounds.size, - }; - child.set_bounds(parent_size, scale, new_bounds); - } - WebviewMessage::SetFocus => child.host.set_focus(1), - WebviewMessage::Url(tx) => { - let url = child.url().unwrap_or_default(); - let _ = tx.send(Ok(url)); - } - WebviewMessage::Bounds(tx) => { - let bounds = child.bounds().ok_or(Error::FailedToSendMessage); - let _ = tx.send(bounds); - } - WebviewMessage::Position(tx) => { - let bounds = child.bounds().ok_or(Error::FailedToSendMessage); - let position = bounds.map(|b| b.position); - let position = position.map(|p| p.to_physical::(appwindow.window.scale_factor())); - let _ = tx.send(position); - } - WebviewMessage::Size(tx) => { - let bounds = child.bounds().ok_or(Error::FailedToSendMessage); - let size = bounds.map(|b| b.size.to_physical::(appwindow.window.scale_factor())); - let _ = tx.send(size); - } - WebviewMessage::WithWebview(f) => f(Webview::new(child.browser.clone())), - WebviewMessage::Print => child.host.print(), - WebviewMessage::AddEventListener(event_id, handler) => { - child.listeners.lock().unwrap().insert(event_id, handler); - } - WebviewMessage::Show => child.set_visible(true), - WebviewMessage::Hide => child.set_visible(false), - WebviewMessage::SetZoom(scale_factor) => { - // CEF uses a logarithmic zoom level where percentage = 1.2^level - // (Chromium's kTextSizeMultiplierRatio). Convert from Tauri linear - // scale factor (1.0 = 100%) to CEF's level (0.0 = 100%) - const CEF_ZOOM_BASE: f64 = 1.2; - let zoom_level = if scale_factor > 0.0 { - scale_factor.ln() / CEF_ZOOM_BASE.ln() - } else { - 0.0 + // If the bounds are not specified, default to the parent window's size and position. + // aka full-window webview. + let bounds = pending.webview_attributes.bounds.unwrap_or_else(|| Rect { + position: PhysicalPosition::new(0, 0).into(), + size: parent_size.into(), + }); + #[cfg(not(target_os = "macos"))] + let bounds = compat::rect_to_physical::(bounds, scale); + #[cfg(target_os = "macos")] + let bounds = compat::rect_to_logical::(bounds, scale); + let bounds = cef::Rect { + x: bounds.position.x, + y: bounds.position.y, + width: bounds.size.width, + height: bounds.size.height, }; - child.host.set_zoom_level(zoom_level); - } - WebviewMessage::SetAutoResize(auto_resize) => { - if auto_resize { - let bounds = child.bounds(); - let parent_size = appwindow.window.surface_size(); - let scale = appwindow.window.scale_factor(); - child.bounds_rate = compute_child_bounds_rate(bounds.as_ref(), true, parent_size, scale); - } else { - child.bounds_rate = None; - } - } - WebviewMessage::SetBackgroundColor(color) => child.set_background_color(color), - WebviewMessage::ClearAllBrowsingData => { - if let Some(manager) = child.cookie_manager() { - manager.delete_cookies(None, None, None); - manager.flush_store(None); - } - if let Some(request_context) = child.host.request_context() { - request_context.clear_http_cache(None); - } - } - WebviewMessage::CookiesForUrl(url, tx) => { - if let Some(manager) = child.cookie_manager() { - cookie::visit_url_cookies(manager, url, tx); - } else { - let _ = tx.send(Ok(Vec::new())); - } - } - WebviewMessage::Cookies(tx) => { - if let Some(manager) = child.cookie_manager() { - cookie::visit_all_cookies(manager, tx); + + // Published PendingWebview has no per-webview platform attribute channel + // (feat/cef-only), so the runtime style is always CEF's default. + let cef_runtime_style = cef::RuntimeStyle::DEFAULT; + + let mut window_info = cef::WindowInfo::default().set_as_child(parent, &bounds); + window_info.runtime_style = cef_runtime_style; + let settings = browser_settings_from_webview_attributes(&pending.webview_attributes); + + let custom_protocol_scheme = if pending.webview_attributes.use_https_scheme { + "https" } else { - let _ = tx.send(Ok(Vec::new())); - } - } - WebviewMessage::SetCookie(cookie) => { - if let Some(manager) = child.cookie_manager() { - let url = child.url(); - cookie::set_cookie(manager, url, cookie); + "http" } - } - WebviewMessage::DeleteCookie(cookie) => { - if let Some(manager) = child.cookie_manager() { - let url = child.url(); - cookie::delete_cookie(manager, url, cookie); - } - } - WebviewMessage::Reparent(target_window_id, tx) => { - if window_id == target_window_id { - let _ = tx.send(Ok(())); - return; + .to_string(); + let custom_scheme_domain_names: Vec = uri_scheme_protocols + .keys() + .map(|scheme| format!("{scheme}.localhost")) + .collect(); + let real_initial_url = pending.url.as_str().to_string(); + let (browser_tx, browser_rx) = mpsc::channel(); + let (init_done, on_initialized) = request_context::deferred_init_continuation({ + let scheme_registry = scheme_registry.clone(); + let uri_scheme_protocols = uri_scheme_protocols.clone(); + let initialization_scripts = initialization_scripts.clone(); + let custom_protocol_scheme = custom_protocol_scheme.clone(); + let custom_scheme_domain_names = custom_scheme_domain_names.clone(); + let label = pending.label.clone(); + move |mut request_context| { + request_context::apply_theme_scheme(request_context.as_ref(), theme); + + // Create with an inert document so the BrowserHost exists before the real + // navigation; the real URL is loaded once the document-start script is set. + let initial_url = CefString::from(INITIAL_LOAD_URL); + let Some(browser) = cef::browser_host_create_browser_sync( + Some(&window_info), + Some(&mut client), + Some(&initial_url), + Some(&settings), + None, + request_context.as_mut(), + ) else { + log::error!("failed to create CEF browser for webview {label:?}"); + return; + }; + let Some(host) = browser.host() else { + log::error!("CEF browser for webview {label:?} has no host"); + return; + }; + let browser_id = browser.identifier(); + + { + let mut registry = scheme_registry.lock().unwrap(); + for (scheme, handler) in uri_scheme_protocols.iter() { + registry.insert( + (browser_id, scheme.clone()), + ( + label.clone(), + handler.clone(), + initialization_scripts.clone(), + ), + ); + } + } + + let devtools_protocol_handlers = Arc::new(Mutex::new(Vec::new())); + let pending_initial_loads: PendingInitialLoads = + Arc::new(Mutex::new(HashMap::new())); + let devtools_observer_registration = Arc::new(Mutex::new(add_dev_tools_observer( + &browser, + devtools_protocol_handlers.clone(), + pending_initial_loads.clone(), + ))); + load_initial_url_after_registering_initialization_scripts( + &browser, + &initialization_scripts, + &custom_protocol_scheme, + &custom_scheme_domain_names, + &real_initial_url, + &pending_initial_loads, + ); + + browser_tx + .send(AppWebview { + webview_id, + label, + browser, + browser_id, + host, + uri_scheme_protocols, + devtools_protocol_handlers, + devtools_observer_registration, + listeners: Default::default(), + bounds_rate, + }) + .expect("failed to send initialized CEF browser"); + } + }); + let request_context = request_context::request_context_from_webview_attributes( + &context.cache_path, + &pending.webview_attributes, + uri_scheme_protocols.keys(), + &custom_protocol_scheme, + scheme_registry.clone(), + on_initialized, + ); + if request_context.is_none() { + init_done.store(true, Ordering::SeqCst); } + request_context::wait_for_deferred_init(&init_done); + + // `None` here means browser creation failed (or the request context never + // initialized); the continuation logs the reason. Soft-fail instead of + // taking down the whole process. + browser_rx.recv().ok() + } - if !self.state.windows.contains_key(&target_window_id) { - let _ = tx.send(Err(Error::WindowNotFound)); - return; + pub(crate) fn handle_webview_message( + &mut self, + window_id: WindowId, + webview_id: u32, + message: WebviewMessage, + ) { + // If the runtime is exiting, don't process any more messages to avoid macOS crash on exit. + if self.state.exiting { + return; } - let Some(mut child) = self - .state - .windows - .get_mut(&window_id) - .and_then(|appwindow| { - appwindow - .children - .iter() - .position(|child| child.webview_id == webview_id) - .map(|index| appwindow.children.remove(index)) - }) - else { - let _ = tx.send(Err(Error::WindowNotFound)); - return; + let Some(appwindow) = self.state.windows.get_mut(&window_id) else { + return; }; - - let Some(target_appwindow) = self.state.windows.get_mut(&target_window_id) else { - let _ = tx.send(Err(Error::WindowNotFound)); - return; + let Some(child) = appwindow + .children + .iter_mut() + .find(|child| child.webview_id == webview_id) + else { + return; }; - let bounds = child.bounds().unwrap_or_else(|| Rect { - position: PhysicalPosition::new(0, 0).into(), - size: target_appwindow.window.surface_size().into(), - }); - child.reparent(target_appwindow); - child.set_bounds( - target_appwindow.window.surface_size(), - target_appwindow.window.scale_factor(), - bounds, - ); - - target_appwindow.children.push(child); - let _ = tx.send(Ok(())); - } - #[cfg(any(debug_assertions, feature = "devtools"))] - WebviewMessage::OpenDevTools => child.host.show_dev_tools(None, None, None, None), - #[cfg(any(debug_assertions, feature = "devtools"))] - WebviewMessage::CloseDevTools => child.host.close_dev_tools(), - #[cfg(any(debug_assertions, feature = "devtools"))] - WebviewMessage::IsDevToolsOpen(tx) => _ = tx.send(child.host.has_dev_tools() == 1), - WebviewMessage::SendDevToolsMessage(message, tx) => { - let result = child.host.send_dev_tools_message(Some(&message)); - let _ = tx.send(if result == 1 { - Ok(()) - } else { - Err(Error::FailedToSendMessage) - }); - } - WebviewMessage::OnDevToolsProtocol(handler, tx) => { - child - .devtools_protocol_handlers - .lock() - .unwrap() - .push(handler); - - let needs_devtools_observer = child - .devtools_observer_registration - .lock() - .unwrap() - .is_none(); - if needs_devtools_observer { - if let Some(registration) = add_dev_tools_observer( - &child.browser, - child.devtools_protocol_handlers.clone(), - Arc::new(Mutex::new(HashMap::new())), - ) { - *child.devtools_observer_registration.lock().unwrap() = Some(registration); - let _ = tx.send(Ok(())); - } else { - let _ = tx.send(Err(Error::FailedToSendMessage)); - } - } else { - let _ = tx.send(Ok(())); + match message { + WebviewMessage::EvaluateScript(script) => { + if let Some(frame) = child.browser.main_frame() { + let script = cef::CefString::from(script.as_str()); + let url = cef::CefString::from(""); + frame.execute_java_script(Some(&script), Some(&url), 0); + } + } + WebviewMessage::EvaluateScriptWithCallback(script, callback) => { + let host = &child.host; + let message_id = self.context.next_webview_event_id() as i32 + 1; + let message_id = Arc::new(AtomicI32::new(message_id)); + let callback = Arc::new(Mutex::new(Some(callback))); + let registration = Arc::new(Mutex::new(None)); + let mut observer = EvalScriptWithCallbackDevToolsObserver::new( + message_id.clone(), + callback.clone(), + registration.clone(), + ); + + if let Some(observer_registration) = + host.add_dev_tools_message_observer(Some(&mut observer)) + { + *registration.lock().unwrap() = Some(observer_registration); + + let message = serde_json::json!({ + "id": message_id.load(Ordering::Relaxed), + "method": "Runtime.evaluate", + "params": { + "expression": script, + "returnByValue": true, + } + }) + .to_string(); + + if host.send_dev_tools_message(Some(message.as_bytes())) != 1 { + let _ = registration.lock().unwrap().take(); + if let Some(callback) = callback.lock().unwrap().take() { + callback(String::new()); + } + } + } else if let Some(callback) = callback.lock().unwrap().take() { + callback(String::new()); + } + } + WebviewMessage::Navigate(url) => { + if let Some(frame) = child.browser.main_frame() { + frame.load_url(Some(&cef::CefString::from(url.as_str()))); + } + } + WebviewMessage::Reload => child.browser.reload(), + WebviewMessage::GoBack => child.browser.go_back(), + WebviewMessage::CanGoBack(tx) => _ = tx.send(Ok(child.browser.can_go_back() == 1)), + WebviewMessage::GoForward => child.browser.go_forward(), + WebviewMessage::CanGoForward(tx) => { + _ = tx.send(Ok(child.browser.can_go_forward() == 1)) + } + // Tauri's Webview::close() is an unconditional native lifecycle action, + // not a page-requested window.close(). A non-forced CEF close may leave + // the child browser (and publisher code) alive indefinitely, and its late + // callback can race parent-window bookkeeping. Window/app teardown already + // uses force_close=true; standalone child close needs the same semantics. + WebviewMessage::Close => { + child.host.close_browser(1); + // Windowed CEF browsers are not destroyed by CloseBrowser alone: the + // native child hierarchy must also be torn down before OnBeforeClose + // runs. Leaving it attached leaks the renderer; letting CEF forward a + // close to its top-level parent can close the whole Tauri window. + child.destroy_native(); + } + WebviewMessage::SetBounds(bounds) => { + let parent_size = appwindow.window.surface_size(); + let scale = appwindow.window.scale_factor(); + child.set_bounds(parent_size, scale, bounds); + } + WebviewMessage::SetSize(size) => { + let parent_size = appwindow.window.surface_size(); + let scale = appwindow.window.scale_factor(); + let bounds = child.bounds().unwrap_or_default(); + let new_bounds = Rect { + position: bounds.position, + size, + }; + child.set_bounds(parent_size, scale, new_bounds); + } + WebviewMessage::SetPosition(position) => { + let parent_size = appwindow.window.surface_size(); + let scale = appwindow.window.scale_factor(); + let bounds = child.bounds().unwrap_or_default(); + let new_bounds = Rect { + position, + size: bounds.size, + }; + child.set_bounds(parent_size, scale, new_bounds); + } + WebviewMessage::SetFocus => child.host.set_focus(1), + WebviewMessage::Url(tx) => { + let url = child.url().unwrap_or_default(); + let _ = tx.send(Ok(url)); + } + WebviewMessage::Bounds(tx) => { + let bounds = child.bounds().ok_or(Error::FailedToSendMessage); + let _ = tx.send(bounds); + } + WebviewMessage::Position(tx) => { + let bounds = child.bounds().ok_or(Error::FailedToSendMessage); + let position = bounds.map(|b| b.position); + let position = + position.map(|p| p.to_physical::(appwindow.window.scale_factor())); + let _ = tx.send(position); + } + WebviewMessage::Size(tx) => { + let bounds = child.bounds().ok_or(Error::FailedToSendMessage); + let size = + bounds.map(|b| b.size.to_physical::(appwindow.window.scale_factor())); + let _ = tx.send(size); + } + WebviewMessage::WithWebview(f) => f(Webview::new(child.browser.clone())), + WebviewMessage::Print => child.host.print(), + WebviewMessage::AddEventListener(event_id, handler) => { + child.listeners.lock().unwrap().insert(event_id, handler); + } + WebviewMessage::Show => child.set_visible(true), + WebviewMessage::Hide => child.set_visible(false), + WebviewMessage::SetZoom(scale_factor) => { + // CEF uses a logarithmic zoom level where percentage = 1.2^level + // (Chromium's kTextSizeMultiplierRatio). Convert from Tauri linear + // scale factor (1.0 = 100%) to CEF's level (0.0 = 100%) + const CEF_ZOOM_BASE: f64 = 1.2; + let zoom_level = if scale_factor > 0.0 { + scale_factor.ln() / CEF_ZOOM_BASE.ln() + } else { + 0.0 + }; + child.host.set_zoom_level(zoom_level); + } + WebviewMessage::SetAutoResize(auto_resize) => { + if auto_resize { + let bounds = child.bounds(); + let parent_size = appwindow.window.surface_size(); + let scale = appwindow.window.scale_factor(); + child.bounds_rate = + compute_child_bounds_rate(bounds.as_ref(), true, parent_size, scale); + } else { + child.bounds_rate = None; + } + } + WebviewMessage::SetBackgroundColor(color) => child.set_background_color(color), + WebviewMessage::ClearAllBrowsingData => { + if let Some(manager) = child.cookie_manager() { + manager.delete_cookies(None, None, None); + manager.flush_store(None); + } + if let Some(request_context) = child.host.request_context() { + request_context.clear_http_cache(None); + } + } + WebviewMessage::CookiesForUrl(url, tx) => { + if let Some(manager) = child.cookie_manager() { + cookie::visit_url_cookies(manager, url, tx); + } else { + let _ = tx.send(Ok(Vec::new())); + } + } + WebviewMessage::Cookies(tx) => { + if let Some(manager) = child.cookie_manager() { + cookie::visit_all_cookies(manager, tx); + } else { + let _ = tx.send(Ok(Vec::new())); + } + } + WebviewMessage::SetCookie(cookie) => { + if let Some(manager) = child.cookie_manager() { + let url = child.url(); + cookie::set_cookie(manager, url, cookie); + } + } + WebviewMessage::DeleteCookie(cookie) => { + if let Some(manager) = child.cookie_manager() { + let url = child.url(); + cookie::delete_cookie(manager, url, cookie); + } + } + WebviewMessage::Reparent(target_window_id, tx) => { + if window_id == target_window_id { + let _ = tx.send(Ok(())); + return; + } + + if !self.state.windows.contains_key(&target_window_id) { + let _ = tx.send(Err(Error::WindowNotFound)); + return; + } + + let Some(mut child) = + self.state + .windows + .get_mut(&window_id) + .and_then(|appwindow| { + appwindow + .children + .iter() + .position(|child| child.webview_id == webview_id) + .map(|index| appwindow.children.remove(index)) + }) + else { + let _ = tx.send(Err(Error::WindowNotFound)); + return; + }; + + let Some(target_appwindow) = self.state.windows.get_mut(&target_window_id) else { + let _ = tx.send(Err(Error::WindowNotFound)); + return; + }; + + let bounds = child.bounds().unwrap_or_else(|| Rect { + position: PhysicalPosition::new(0, 0).into(), + size: target_appwindow.window.surface_size().into(), + }); + child.reparent(target_appwindow); + child.set_bounds( + target_appwindow.window.surface_size(), + target_appwindow.window.scale_factor(), + bounds, + ); + + target_appwindow.children.push(child); + let _ = tx.send(Ok(())); + } + #[cfg(any(debug_assertions, feature = "devtools"))] + WebviewMessage::OpenDevTools => child.host.show_dev_tools(None, None, None, None), + #[cfg(any(debug_assertions, feature = "devtools"))] + WebviewMessage::CloseDevTools => child.host.close_dev_tools(), + #[cfg(any(debug_assertions, feature = "devtools"))] + WebviewMessage::IsDevToolsOpen(tx) => _ = tx.send(child.host.has_dev_tools() == 1), + WebviewMessage::SendDevToolsMessage(message, tx) => { + let result = child.host.send_dev_tools_message(Some(&message)); + let _ = tx.send(if result == 1 { + Ok(()) + } else { + Err(Error::FailedToSendMessage) + }); + } + WebviewMessage::OnDevToolsProtocol(handler, tx) => { + child + .devtools_protocol_handlers + .lock() + .unwrap() + .push(handler); + + let needs_devtools_observer = child + .devtools_observer_registration + .lock() + .unwrap() + .is_none(); + if needs_devtools_observer { + if let Some(registration) = add_dev_tools_observer( + &child.browser, + child.devtools_protocol_handlers.clone(), + Arc::new(Mutex::new(HashMap::new())), + ) { + *child.devtools_observer_registration.lock().unwrap() = Some(registration); + let _ = tx.send(Ok(())); + } else { + let _ = tx.send(Err(Error::FailedToSendMessage)); + } + } else { + let _ = tx.send(Ok(())); + } + } } - } } - } } #[derive(Debug, Clone)] pub struct CefInitScript { - pub(crate) script: String, - pub(crate) hash: String, - for_main_frame_only: bool, + pub(crate) script: String, + pub(crate) hash: String, + for_main_frame_only: bool, } impl CefInitScript { - fn new(script: InitializationScript) -> Self { - let mut hasher = Sha256::new(); - hasher.update(normalize_script_for_csp(script.script.as_bytes())); - let hash = format!( - "'sha256-{}'", - base64::Engine::encode( - &base64::engine::general_purpose::STANDARD, - hasher.finalize() - ) - ); - Self { - script: script.script, - hash, - for_main_frame_only: script.for_main_frame_only, + fn new(script: InitializationScript) -> Self { + let mut hasher = Sha256::new(); + hasher.update(normalize_script_for_csp(script.script.as_bytes())); + let hash = format!( + "'sha256-{}'", + base64::Engine::encode( + &base64::engine::general_purpose::STANDARD, + hasher.finalize() + ) + ); + Self { + script: script.script, + hash, + for_main_frame_only: script.for_main_frame_only, + } } - } } pub(crate) fn initialization_scripts(attrs: &mut WebviewAttributes) -> Arc> { - let mut initialization_scripts = Vec::new(); + let mut initialization_scripts = Vec::new(); - if attrs.drag_drop_handler_enabled { - let drag_script = browser_client::drag_drop_initialization_script(); - initialization_scripts.push(CefInitScript::new(drag_script)); - } + if attrs.drag_drop_handler_enabled { + let drag_script = browser_client::drag_drop_initialization_script(); + initialization_scripts.push(CefInitScript::new(drag_script)); + } - initialization_scripts.extend( - std::mem::take(&mut attrs.initialization_scripts) - .into_iter() - .map(CefInitScript::new), - ); + initialization_scripts.extend( + std::mem::take(&mut attrs.initialization_scripts) + .into_iter() + .map(CefInitScript::new), + ); - Arc::new(initialization_scripts) + Arc::new(initialization_scripts) } #[derive(Debug, Clone)] pub struct CefWebviewDispatcher { - pub(crate) window_id: Arc>, - pub(crate) webview_id: u32, - pub(crate) context: RuntimeContext, + pub(crate) window_id: Arc>, + pub(crate) webview_id: u32, + pub(crate) context: RuntimeContext, } impl CefWebviewDispatcher { - pub fn send_dev_tools_message(&self, message: &[u8]) -> Result<()> { - let (tx, rx) = mpsc::channel(); - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::SendDevToolsMessage(message.to_vec(), tx), - })?; - rx.recv().map_err(|_| Error::FailedToReceiveMessage)? - } + pub fn send_dev_tools_message(&self, message: &[u8]) -> Result<()> { + let (tx, rx) = mpsc::channel(); + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::SendDevToolsMessage(message.to_vec(), tx), + })?; + rx.recv().map_err(|_| Error::FailedToReceiveMessage)? + } - pub fn on_dev_tools_protocol( - &self, - f: F, - ) -> Result<()> { - let (tx, rx) = mpsc::channel(); - let handler = - Arc::new(move |protocol: DevToolsProtocol| f(protocol)) as Arc; - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::OnDevToolsProtocol(handler, tx), - })?; - rx.recv().map_err(|_| Error::FailedToReceiveMessage)? - } + pub fn on_dev_tools_protocol( + &self, + f: F, + ) -> Result<()> { + let (tx, rx) = mpsc::channel(); + let handler = + Arc::new(move |protocol: DevToolsProtocol| f(protocol)) as Arc; + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::OnDevToolsProtocol(handler, tx), + })?; + rx.recv().map_err(|_| Error::FailedToReceiveMessage)? + } } pub(crate) fn create_webview_detached( - context: &RuntimeContext, - window_id: WindowId, - pending: PendingWebview>, + context: &RuntimeContext, + window_id: WindowId, + pending: PendingWebview>, ) -> Result>> { - let label = pending.label.clone(); - let webview_id = context.next_webview_id(); - let (result_tx, result_rx) = mpsc::channel(); - context.send_message(Message::CreateWebview { - window_id, - webview_id, - pending: Box::new(pending), - result_tx, - })?; - // Block until the event loop has created the browser so a creation failure - // is surfaced to the caller instead of leaving a detached, dead webview. - result_rx - .recv() - .map_err(|_| Error::FailedToReceiveMessage)??; - Ok(DetachedWebview { - label, - dispatcher: CefWebviewDispatcher { - window_id: Arc::new(Mutex::new(window_id)), - webview_id, - context: context.clone(), - }, - }) + let label = pending.label.clone(); + let webview_id = context.next_webview_id(); + let (result_tx, result_rx) = mpsc::channel(); + context.send_message(Message::CreateWebview { + window_id, + webview_id, + pending: Box::new(pending), + result_tx, + })?; + // Block until the event loop has created the browser so a creation failure + // is surfaced to the caller instead of leaving a detached, dead webview. + result_rx + .recv() + .map_err(|_| Error::FailedToReceiveMessage)??; + Ok(DetachedWebview { + label, + dispatcher: CefWebviewDispatcher { + window_id: Arc::new(Mutex::new(window_id)), + webview_id, + context: context.clone(), + }, + }) } fn getter( - context: &RuntimeContext, - message: Message, - receiver: Receiver>, + context: &RuntimeContext, + message: Message, + receiver: Receiver>, ) -> Result { - context.send_message(message)?; - receiver.recv().map_err(|_| Error::FailedToReceiveMessage)? + context.send_message(message)?; + receiver.recv().map_err(|_| Error::FailedToReceiveMessage)? } macro_rules! webview_getter { - ($self:ident, $variant:ident) => {{ - let (tx, rx) = mpsc::channel(); - getter( - &$self.context, - Message::Webview { - window_id: *$self.window_id.lock().unwrap(), - webview_id: $self.webview_id, - message: WebviewMessage::$variant(tx), - }, - rx, - ) - }}; + ($self:ident, $variant:ident) => {{ + let (tx, rx) = mpsc::channel(); + getter( + &$self.context, + Message::Webview { + window_id: *$self.window_id.lock().unwrap(), + webview_id: $self.webview_id, + message: WebviewMessage::$variant(tx), + }, + rx, + ) + }}; } impl CefWebviewDispatcher { - // History navigation: feat/cef trait methods, not yet part of the published - // `WebviewDispatch` trait — kept as inherent API until upstream releases. - pub fn go_back(&self) -> Result<()> { - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::GoBack, - }) - } + // History navigation: feat/cef trait methods, not yet part of the published + // `WebviewDispatch` trait — kept as inherent API until upstream releases. + pub fn go_back(&self) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::GoBack, + }) + } - pub fn can_go_back(&self) -> Result { - webview_getter!(self, CanGoBack) - } + pub fn can_go_back(&self) -> Result { + webview_getter!(self, CanGoBack) + } - pub fn go_forward(&self) -> Result<()> { - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::GoForward, - }) - } + pub fn go_forward(&self) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::GoForward, + }) + } - pub fn can_go_forward(&self) -> Result { - webview_getter!(self, CanGoForward) - } + pub fn can_go_forward(&self) -> Result { + webview_getter!(self, CanGoForward) + } } impl WebviewDispatch for CefWebviewDispatcher { - type Runtime = CefRuntime; + type Runtime = CefRuntime; - fn run_on_main_thread(&self, f: F) -> Result<()> { - self.context.run_on_main_thread(f) - } + fn run_on_main_thread(&self, f: F) -> Result<()> { + self.context.run_on_main_thread(f) + } - fn on_webview_event(&self, f: F) -> WebviewEventId { - let id = self.context.next_webview_event_id(); - let _ = self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::AddEventListener(id, Box::new(f)), - }); - id - } + fn on_webview_event(&self, f: F) -> WebviewEventId { + let id = self.context.next_webview_event_id(); + let _ = self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::AddEventListener(id, Box::new(f)), + }); + id + } - fn with_webview) + Send + 'static>(&self, f: F) -> Result<()> { - // Published tauri erases the runtime webview type; downcast the boxed - // `Any` back to [`Webview`] to reach the underlying `cef::Browser`. - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::WithWebview(Box::new(move |webview: Webview| { - f(Box::new(webview)) - })), - }) - } + fn with_webview) + Send + 'static>(&self, f: F) -> Result<()> { + // Published tauri erases the runtime webview type; downcast the boxed + // `Any` back to [`Webview`] to reach the underlying `cef::Browser`. + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::WithWebview(Box::new(move |webview: Webview| { + f(Box::new(webview)) + })), + }) + } - #[cfg(any(debug_assertions, feature = "devtools"))] - fn open_devtools(&self) { - let _ = self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::OpenDevTools, - }); - } + #[cfg(any(debug_assertions, feature = "devtools"))] + fn open_devtools(&self) { + let _ = self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::OpenDevTools, + }); + } - #[cfg(any(debug_assertions, feature = "devtools"))] - fn close_devtools(&self) { - let _ = self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::CloseDevTools, - }); - } + #[cfg(any(debug_assertions, feature = "devtools"))] + fn close_devtools(&self) { + let _ = self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::CloseDevTools, + }); + } - #[cfg(any(debug_assertions, feature = "devtools"))] - fn is_devtools_open(&self) -> Result { - let (tx, rx) = mpsc::channel(); - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::IsDevToolsOpen(tx), - })?; - rx.recv().map_err(|_| Error::FailedToReceiveMessage) - } + #[cfg(any(debug_assertions, feature = "devtools"))] + fn is_devtools_open(&self) -> Result { + let (tx, rx) = mpsc::channel(); + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::IsDevToolsOpen(tx), + })?; + rx.recv().map_err(|_| Error::FailedToReceiveMessage) + } - fn url(&self) -> Result { - webview_getter!(self, Url) - } + fn url(&self) -> Result { + webview_getter!(self, Url) + } - fn bounds(&self) -> Result { - webview_getter!(self, Bounds) - } + fn bounds(&self) -> Result { + webview_getter!(self, Bounds) + } - fn position(&self) -> Result> { - webview_getter!(self, Position) - } + fn position(&self) -> Result> { + webview_getter!(self, Position) + } - fn size(&self) -> Result> { - webview_getter!(self, Size) - } + fn size(&self) -> Result> { + webview_getter!(self, Size) + } - fn navigate(&self, url: Url) -> Result<()> { - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::Navigate(url), - }) - } + fn navigate(&self, url: Url) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::Navigate(url), + }) + } - fn reload(&self) -> Result<()> { - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::Reload, - }) - } + fn reload(&self) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::Reload, + }) + } - fn print(&self) -> Result<()> { - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::Print, - }) - } + fn print(&self) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::Print, + }) + } - fn close(&self) -> Result<()> { - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::Close, - }) - } + fn close(&self) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::Close, + }) + } - fn set_bounds(&self, bounds: Rect) -> Result<()> { - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::SetBounds(bounds), - }) - } + fn set_bounds(&self, bounds: Rect) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::SetBounds(bounds), + }) + } - fn set_size(&self, size: Size) -> Result<()> { - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::SetSize(size), - }) - } + fn set_size(&self, size: Size) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::SetSize(size), + }) + } - fn set_position(&self, position: Position) -> Result<()> { - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::SetPosition(position), - }) - } + fn set_position(&self, position: Position) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::SetPosition(position), + }) + } - fn set_focus(&self) -> Result<()> { - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::SetFocus, - }) - } + fn set_focus(&self) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::SetFocus, + }) + } - fn hide(&self) -> Result<()> { - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::Hide, - }) - } + fn hide(&self) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::Hide, + }) + } - fn show(&self) -> Result<()> { - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::Show, - }) - } + fn show(&self) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::Show, + }) + } - fn eval_script>(&self, script: S) -> Result<()> { - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::EvaluateScript(script.into()), - }) - } + fn eval_script>(&self, script: S) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::EvaluateScript(script.into()), + }) + } - fn eval_script_with_callback>( - &self, - script: S, - callback: impl Fn(String) + Send + 'static, - ) -> Result<()> { - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::EvaluateScriptWithCallback(script.into(), Box::new(callback)), - }) - } + fn eval_script_with_callback>( + &self, + script: S, + callback: impl Fn(String) + Send + 'static, + ) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::EvaluateScriptWithCallback(script.into(), Box::new(callback)), + }) + } - fn reparent(&self, window_id: WindowId) -> Result<()> { - let (tx, rx) = mpsc::channel(); - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::Reparent(window_id, tx), - })?; - let result = rx.recv().map_err(|_| Error::FailedToReceiveMessage)?; - if result.is_ok() { - *self.window_id.lock().unwrap() = window_id; + fn reparent(&self, window_id: WindowId) -> Result<()> { + let (tx, rx) = mpsc::channel(); + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::Reparent(window_id, tx), + })?; + let result = rx.recv().map_err(|_| Error::FailedToReceiveMessage)?; + if result.is_ok() { + *self.window_id.lock().unwrap() = window_id; + } + result } - result - } - fn cookies_for_url(&self, url: Url) -> Result>> { - let (tx, rx) = mpsc::channel(); - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::CookiesForUrl(url, tx), - })?; - rx.recv().map_err(|_| Error::FailedToReceiveMessage)? - } + fn cookies_for_url(&self, url: Url) -> Result>> { + let (tx, rx) = mpsc::channel(); + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::CookiesForUrl(url, tx), + })?; + rx.recv().map_err(|_| Error::FailedToReceiveMessage)? + } - fn cookies(&self) -> Result>> { - webview_getter!(self, Cookies) - } + fn cookies(&self) -> Result>> { + webview_getter!(self, Cookies) + } - fn set_cookie(&self, cookie: Cookie<'_>) -> Result<()> { - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::SetCookie(cookie.into_owned()), - }) - } + fn set_cookie(&self, cookie: Cookie<'_>) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::SetCookie(cookie.into_owned()), + }) + } - fn delete_cookie(&self, cookie: Cookie<'_>) -> Result<()> { - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::DeleteCookie(cookie.into_owned()), - }) - } + fn delete_cookie(&self, cookie: Cookie<'_>) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::DeleteCookie(cookie.into_owned()), + }) + } - fn set_auto_resize(&self, auto_resize: bool) -> Result<()> { - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::SetAutoResize(auto_resize), - }) - } + fn set_auto_resize(&self, auto_resize: bool) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::SetAutoResize(auto_resize), + }) + } - fn set_zoom(&self, scale_factor: f64) -> Result<()> { - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::SetZoom(scale_factor), - }) - } + fn set_zoom(&self, scale_factor: f64) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::SetZoom(scale_factor), + }) + } - fn set_background_color(&self, color: Option) -> Result<()> { - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::SetBackgroundColor(color), - }) - } + fn set_background_color(&self, color: Option) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::SetBackgroundColor(color), + }) + } - fn clear_all_browsing_data(&self) -> Result<()> { - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::ClearAllBrowsingData, - }) - } + fn clear_all_browsing_data(&self) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::ClearAllBrowsingData, + }) + } } /// Reposition every child webview to follow the parent window size. @@ -1215,22 +1220,22 @@ impl WebviewDispatch for CefWebviewDispatcher { /// from the current window size; children with fixed bounds keep whatever bounds /// they were last given. pub(crate) fn layout_app_window(appwindow: &AppWindow) { - let parent_size = appwindow.window.surface_size(); - let win_w = parent_size.width as f32; - let win_h = parent_size.height as f32; - let scale = appwindow.window.scale_factor(); - for child in &appwindow.children { - let Some(rate) = child.bounds_rate else { - continue; - }; - let x = (rate.x * win_w).round() as i32; - let y = (rate.y * win_h).round() as i32; - let w = (rate.width * win_w).round() as i32; - let h = (rate.height * win_h).round() as i32; - child.host.notify_move_or_resize_started(); - child.apply_physical_bounds(scale, x, y, w, h); - child.host.was_resized(); - } + let parent_size = appwindow.window.surface_size(); + let win_w = parent_size.width as f32; + let win_h = parent_size.height as f32; + let scale = appwindow.window.scale_factor(); + for child in &appwindow.children { + let Some(rate) = child.bounds_rate else { + continue; + }; + let x = (rate.x * win_w).round() as i32; + let y = (rate.y * win_h).round() as i32; + let w = (rate.width * win_w).round() as i32; + let h = (rate.height * win_h).round() as i32; + child.host.notify_move_or_resize_started(); + child.apply_physical_bounds(scale, x, y, w, h); + child.host.was_resized(); + } } /// Compute the bounds rate of a child webview relative to its parent window. @@ -1238,50 +1243,50 @@ pub(crate) fn layout_app_window(appwindow: &AppWindow) { /// For webiews filling the window, default rate is used, otherwise the rate is computed from the current bounds and parent size /// if auto_resize is enabled, otherwise None is returned. pub(crate) fn compute_child_bounds_rate( - bounds: Option<&Rect>, - auto_resize: bool, - parent_size: PhysicalSize, - scale: f64, + bounds: Option<&Rect>, + auto_resize: bool, + parent_size: PhysicalSize, + scale: f64, ) -> Option { - let Some(bounds) = bounds else { - return Some(BoundsRate::default()); - }; + let Some(bounds) = bounds else { + return Some(BoundsRate::default()); + }; - if !auto_resize { - return None; - } + if !auto_resize { + return None; + } - let min_w = parent_size.width.max(1) as i32; - let min_h = parent_size.height.max(1) as i32; + let min_w = parent_size.width.max(1) as i32; + let min_h = parent_size.height.max(1) as i32; - let pos = bounds.position.to_physical::(scale); - let size = bounds.size.to_physical::(scale); + let pos = bounds.position.to_physical::(scale); + let size = bounds.size.to_physical::(scale); - let x = pos.x; - let y = pos.y; - let w = size.width; - let h = size.height; + let x = pos.x; + let y = pos.y; + let w = size.width; + let h = size.height; - Some(BoundsRate { - x: x as f32 / min_w as f32, - y: y as f32 / min_h as f32, - width: w as f32 / min_w as f32, - height: h as f32 / min_h as f32, - }) + Some(BoundsRate { + x: x as f32 / min_w as f32, + y: y as f32 / min_h as f32, + width: w as f32 / min_w as f32, + height: h as f32 / min_h as f32, + }) } pub(crate) const INITIAL_LOAD_URL: &str = concat!( - "data:text/html;charset=utf-8,", - "%3C!doctype%20html%3E", - "%3Chtml%20data-tauri-cef-internal%3D%22initial-load%22%3E", - "%3Chead%3E", - "%3Cmeta%20charset%3D%22utf-8%22%3E", - "%3Ctitle%3ETauri%20CEF%20Initial%20Load%3C%2Ftitle%3E", - "%3C%2Fhead%3E", - "%3Cbody%20data-tauri-cef-internal%3D%22initial-load%22%3E", - "%3C!--%20Tauri%20CEF%20internal%20initial%20load%20placeholder%20--%3E", - "%3C%2Fbody%3E", - "%3C%2Fhtml%3E", + "data:text/html;charset=utf-8,", + "%3C!doctype%20html%3E", + "%3Chtml%20data-tauri-cef-internal%3D%22initial-load%22%3E", + "%3Chead%3E", + "%3Cmeta%20charset%3D%22utf-8%22%3E", + "%3Ctitle%3ETauri%20CEF%20Initial%20Load%3C%2Ftitle%3E", + "%3C%2Fhead%3E", + "%3Cbody%20data-tauri-cef-internal%3D%22initial-load%22%3E", + "%3C!--%20Tauri%20CEF%20internal%20initial%20load%20placeholder%20--%3E", + "%3C%2Fbody%3E", + "%3C%2Fhtml%3E", ); static NEXT_INIT_SCRIPT_DEVTOOLS_MESSAGE_ID: AtomicI32 = AtomicI32::new(1_000_000); @@ -1363,22 +1368,22 @@ cef::wrap_dev_tools_message_observer! { } fn runtime_evaluate_result_to_json(result: Option<&[u8]>) -> String { - let Some(result) = result else { - return String::new(); - }; - let Ok(result) = serde_json::from_slice::(result) else { - return String::new(); - }; - - if result.get("exceptionDetails").is_some() { - return String::new(); - } + let Some(result) = result else { + return String::new(); + }; + let Ok(result) = serde_json::from_slice::(result) else { + return String::new(); + }; - let remote_object = result.get("result").unwrap_or(&result); - remote_object - .get("value") - .and_then(|value| serde_json::to_string(value).ok()) - .unwrap_or_default() + if result.get("exceptionDetails").is_some() { + return String::new(); + } + + let remote_object = result.get("result").unwrap_or(&result); + remote_object + .get("value") + .and_then(|value| serde_json::to_string(value).ok()) + .unwrap_or_default() } type EvalScriptCallback = Box; @@ -1422,29 +1427,29 @@ cef::wrap_dev_tools_message_observer! { /// kept alive for the observer to stay registered. The observer is unregistered when /// the Registration is dropped. pub(crate) fn add_dev_tools_observer( - browser: &Browser, - handlers: Arc>>>, - pending_initial_loads: PendingInitialLoads, + browser: &Browser, + handlers: Arc>>>, + pending_initial_loads: PendingInitialLoads, ) -> Option { - browser.host().and_then(|host| { - let mut observer = TauriDevToolsProtocolObserver::new(handlers, pending_initial_loads); - host.add_dev_tools_message_observer(Some(&mut observer)) - }) + browser.host().and_then(|host| { + let mut observer = TauriDevToolsProtocolObserver::new(handlers, pending_initial_loads); + host.add_dev_tools_message_observer(Some(&mut observer)) + }) } fn devtools_initialization_script_source( - initialization_scripts: &[CefInitScript], - custom_protocol_scheme: &str, - custom_scheme_domain_names: &[String], + initialization_scripts: &[CefInitScript], + custom_protocol_scheme: &str, + custom_scheme_domain_names: &[String], ) -> Option { - if initialization_scripts.is_empty() { - return None; - } + if initialization_scripts.is_empty() { + return None; + } - let custom_protocol = serde_json::to_string(&format!("{custom_protocol_scheme}:")).ok()?; - let custom_domains = serde_json::to_string(custom_scheme_domain_names).ok()?; - let mut source = format!( - r#"{{ + let custom_protocol = serde_json::to_string(&format!("{custom_protocol_scheme}:")).ok()?; + let custom_domains = serde_json::to_string(custom_scheme_domain_names).ok()?; + let mut source = format!( + r#"{{ const __TAURI_CEF_INIT_CUSTOM_PROTOCOL__ = {custom_protocol}; const __TAURI_CEF_INIT_CUSTOM_DOMAINS__ = new Set({custom_domains}); const __TAURI_CEF_INIT_IS_CUSTOM_PROTOCOL__ = @@ -1458,70 +1463,71 @@ fn devtools_initialization_script_source( }} }})(); "# - ); + ); - for init_script in initialization_scripts { - source.push_str(" if (!__TAURI_CEF_INIT_IS_CUSTOM_PROTOCOL__"); - if init_script.for_main_frame_only { - source.push_str(" && __TAURI_CEF_INIT_IS_MAIN_FRAME__"); + for init_script in initialization_scripts { + source.push_str(" if (!__TAURI_CEF_INIT_IS_CUSTOM_PROTOCOL__"); + if init_script.for_main_frame_only { + source.push_str(" && __TAURI_CEF_INIT_IS_MAIN_FRAME__"); + } + source.push_str(") {\n"); + source.push_str(init_script.script.as_str()); + source.push_str("\n }\n"); } - source.push_str(") {\n"); - source.push_str(init_script.script.as_str()); - source.push_str("\n }\n"); - } - source.push_str("}\n"); - Some(source) + source.push_str("}\n"); + Some(source) } fn register_initialization_scripts( - browser: &Browser, - initialization_scripts: &[CefInitScript], - custom_protocol_scheme: &str, - custom_scheme_domain_names: &[String], - initial_url: String, - pending_initial_loads: &PendingInitialLoads, + browser: &Browser, + initialization_scripts: &[CefInitScript], + custom_protocol_scheme: &str, + custom_scheme_domain_names: &[String], + initial_url: String, + pending_initial_loads: &PendingInitialLoads, ) -> bool { - let Some(source) = devtools_initialization_script_source( - initialization_scripts, - custom_protocol_scheme, - custom_scheme_domain_names, - ) else { - return false; - }; - let Some(host) = browser.host() else { - return false; - }; - - let page_enable_message_id = NEXT_INIT_SCRIPT_DEVTOOLS_MESSAGE_ID.fetch_add(1, Ordering::Relaxed); - let page_enable_message = serde_json::json!({ - "id": page_enable_message_id, - "method": "Page.enable", - "params": {} - }) - .to_string(); - let _ = host.send_dev_tools_message(Some(page_enable_message.as_bytes())); - - let message_id = NEXT_INIT_SCRIPT_DEVTOOLS_MESSAGE_ID.fetch_add(1, Ordering::Relaxed); - let message = serde_json::json!({ - "id": message_id, - "method": "Page.addScriptToEvaluateOnNewDocument", - "params": { - "source": source, + let Some(source) = devtools_initialization_script_source( + initialization_scripts, + custom_protocol_scheme, + custom_scheme_domain_names, + ) else { + return false; + }; + let Some(host) = browser.host() else { + return false; + }; + + let page_enable_message_id = + NEXT_INIT_SCRIPT_DEVTOOLS_MESSAGE_ID.fetch_add(1, Ordering::Relaxed); + let page_enable_message = serde_json::json!({ + "id": page_enable_message_id, + "method": "Page.enable", + "params": {} + }) + .to_string(); + let _ = host.send_dev_tools_message(Some(page_enable_message.as_bytes())); + + let message_id = NEXT_INIT_SCRIPT_DEVTOOLS_MESSAGE_ID.fetch_add(1, Ordering::Relaxed); + let message = serde_json::json!({ + "id": message_id, + "method": "Page.addScriptToEvaluateOnNewDocument", + "params": { + "source": source, + } + }) + .to_string(); + + pending_initial_loads + .lock() + .unwrap() + .insert(message_id, (browser.clone(), initial_url)); + if host.send_dev_tools_message(Some(message.as_bytes())) == 1 { + true + } else { + pending_initial_loads.lock().unwrap().remove(&message_id); + false } - }) - .to_string(); - - pending_initial_loads - .lock() - .unwrap() - .insert(message_id, (browser.clone(), initial_url)); - if host.send_dev_tools_message(Some(message.as_bytes())) == 1 { - true - } else { - pending_initial_loads.lock().unwrap().remove(&message_id); - false - } } wrap_task! { @@ -1538,8 +1544,8 @@ wrap_task! { } fn post_load_initial_url(browser: Browser, initial_url: String) { - let mut task = LoadInitialUrlTask::new(browser, initial_url); - cef::post_task(sys::cef_thread_id_t::TID_UI.into(), Some(&mut task)); + let mut task = LoadInitialUrlTask::new(browser, initial_url); + cef::post_task(sys::cef_thread_id_t::TID_UI.into(), Some(&mut task)); } // Browsers are created with an inert internal document so the BrowserHost exists @@ -1551,31 +1557,31 @@ fn post_load_initial_url(browser: Browser, initial_url: String) { // The real load is posted as a CEF UI task instead of performed inline. This // keeps the browser creation/CDP setup stack from re-entering navigation. pub(crate) fn load_initial_url_after_registering_initialization_scripts( - browser: &Browser, - initialization_scripts: &[CefInitScript], - custom_protocol_scheme: &str, - custom_scheme_domain_names: &[String], - initial_url: &str, - pending_initial_loads: &PendingInitialLoads, + browser: &Browser, + initialization_scripts: &[CefInitScript], + custom_protocol_scheme: &str, + custom_scheme_domain_names: &[String], + initial_url: &str, + pending_initial_loads: &PendingInitialLoads, ) { - let browser_for_callback = browser.clone(); - let initial_url = initial_url.to_string(); - let is_waiting_for_initialization_scripts = register_initialization_scripts( - browser, - initialization_scripts, - custom_protocol_scheme, - custom_scheme_domain_names, - initial_url.clone(), - pending_initial_loads, - ); - - if !is_waiting_for_initialization_scripts { - post_load_initial_url(browser_for_callback, initial_url); - } + let browser_for_callback = browser.clone(); + let initial_url = initial_url.to_string(); + let is_waiting_for_initialization_scripts = register_initialization_scripts( + browser, + initialization_scripts, + custom_protocol_scheme, + custom_scheme_domain_names, + initial_url.clone(), + pending_initial_loads, + ); + + if !is_waiting_for_initialization_scripts { + post_load_initial_url(browser_for_callback, initial_url); + } } fn load_initial_url(browser: &Browser, initial_url: &str) { - if let Some(frame) = browser.main_frame() { - frame.load_url(Some(&CefString::from(initial_url))); - } + if let Some(frame) = browser.main_frame() { + frame.load_url(Some(&CefString::from(initial_url))); + } } diff --git a/src/window.rs b/src/window.rs index f15a75d..b14de91 100644 --- a/src/window.rs +++ b/src/window.rs @@ -3,31 +3,31 @@ // SPDX-License-Identifier: MIT use std::{ - collections::HashMap, - sync::{ - Arc, Mutex, - mpsc::{self, Receiver, Sender}, - }, + collections::HashMap, + sync::{ + Arc, Mutex, + mpsc::{self, Receiver, Sender}, + }, }; use cef::ImplBrowserHost; use raw_window_handle::HasWindowHandle; use tauri_runtime::{ - Error, Icon, ProgressBarState, Result, UserAttentionType, UserEvent, WindowDispatch, - WindowEventId, - dpi::{PhysicalPosition, PhysicalSize, Position, Size}, - monitor::Monitor, - webview::{DetachedWebview, PendingWebview}, - window::{ - CursorIcon, DetachedWindow, DetachedWindowWebview, PendingWindow, RawWindow, WindowEvent, - WindowId, WindowSizeConstraints, - }, + Error, Icon, ProgressBarState, Result, UserAttentionType, UserEvent, WindowDispatch, + WindowEventId, + dpi::{PhysicalPosition, PhysicalSize, Position, Size}, + monitor::Monitor, + webview::{DetachedWebview, PendingWebview}, + window::{ + CursorIcon, DetachedWindow, DetachedWindowWebview, PendingWindow, RawWindow, WindowEvent, + WindowId, WindowSizeConstraints, + }, }; use tauri_utils::{Theme, config::Color}; use winit::{ - event_loop::ActiveEventLoop, - monitor::{Fullscreen, MonitorHandle}, - window::{Window as WinitWindow, WindowAttributes, WindowLevel}, + event_loop::ActiveEventLoop, + monitor::{Fullscreen, MonitorHandle}, + window::{Window as WinitWindow, WindowAttributes, WindowLevel}, }; #[cfg(target_os = "macos")] @@ -45,1399 +45,1408 @@ use winit::platform::windows::WindowExtWindows; #[cfg(windows)] use crate::window_handle::SoftbufferWindowHandle; use crate::{ - cef_impl::{client as browser_client, request_context}, - runtime::{AfterWindowCreationCallback, CefRuntime, Message, RuntimeContext, WinitCefApp}, - webview::{AppWebview, CefWebviewDispatcher, create_webview_detached}, - window_builder::WindowBuilderWrapper, - window_handle::SendRawWindowHandle, + cef_impl::{client as browser_client, request_context}, + runtime::{AfterWindowCreationCallback, CefRuntime, Message, RuntimeContext, WinitCefApp}, + webview::{AppWebview, CefWebviewDispatcher, create_webview_detached}, + window_builder::WindowBuilderWrapper, + window_handle::SendRawWindowHandle, }; type WindowEventListener = Box; type WindowEventListeners = Arc>>; pub(crate) fn tauri_theme_to_winit_theme(theme: Option) -> Option { - theme.map(|theme| match theme { - Theme::Light => winit::window::Theme::Light, - Theme::Dark => winit::window::Theme::Dark, - _ => winit::window::Theme::Light, - }) + theme.map(|theme| match theme { + Theme::Light => winit::window::Theme::Light, + Theme::Dark => winit::window::Theme::Dark, + _ => winit::window::Theme::Light, + }) } pub(crate) fn winit_theme_to_tauri_theme(theme: winit::window::Theme) -> Theme { - match theme { - winit::window::Theme::Light => Theme::Light, - winit::window::Theme::Dark => Theme::Dark, - } + match theme { + winit::window::Theme::Light => Theme::Light, + winit::window::Theme::Dark => Theme::Dark, + } } fn tauri_resize_direction_to_winit( - direction: tauri_runtime::ResizeDirection, + direction: tauri_runtime::ResizeDirection, ) -> winit::window::ResizeDirection { - match direction { - tauri_runtime::ResizeDirection::East => winit::window::ResizeDirection::East, - tauri_runtime::ResizeDirection::North => winit::window::ResizeDirection::North, - tauri_runtime::ResizeDirection::NorthEast => winit::window::ResizeDirection::NorthEast, - tauri_runtime::ResizeDirection::NorthWest => winit::window::ResizeDirection::NorthWest, - tauri_runtime::ResizeDirection::South => winit::window::ResizeDirection::South, - tauri_runtime::ResizeDirection::SouthEast => winit::window::ResizeDirection::SouthEast, - tauri_runtime::ResizeDirection::SouthWest => winit::window::ResizeDirection::SouthWest, - tauri_runtime::ResizeDirection::West => winit::window::ResizeDirection::West, - } + match direction { + tauri_runtime::ResizeDirection::East => winit::window::ResizeDirection::East, + tauri_runtime::ResizeDirection::North => winit::window::ResizeDirection::North, + tauri_runtime::ResizeDirection::NorthEast => winit::window::ResizeDirection::NorthEast, + tauri_runtime::ResizeDirection::NorthWest => winit::window::ResizeDirection::NorthWest, + tauri_runtime::ResizeDirection::South => winit::window::ResizeDirection::South, + tauri_runtime::ResizeDirection::SouthEast => winit::window::ResizeDirection::SouthEast, + tauri_runtime::ResizeDirection::SouthWest => winit::window::ResizeDirection::SouthWest, + tauri_runtime::ResizeDirection::West => winit::window::ResizeDirection::West, + } } fn calculate_window_center_position( - window_size: PhysicalSize, - monitor: &MonitorHandle, + window_size: PhysicalSize, + monitor: &MonitorHandle, ) -> PhysicalPosition { - let work_area = monitor.work_area(); - PhysicalPosition::new( - work_area.position.x + ((work_area.size.width as i32 - window_size.width as i32).max(0) / 2), - work_area.position.y + ((work_area.size.height as i32 - window_size.height as i32).max(0) / 2), - ) + let work_area = monitor.work_area(); + PhysicalPosition::new( + work_area.position.x + + ((work_area.size.width as i32 - window_size.width as i32).max(0) / 2), + work_area.position.y + + ((work_area.size.height as i32 - window_size.height as i32).max(0) / 2), + ) } fn find_monitor_for_position( - monitors: impl Iterator, - position: Position, + monitors: impl Iterator, + position: Position, ) -> Option { - monitors.into_iter().find(|monitor| { - let Some(monitor_position) = monitor.position() else { - return false; - }; - let Some(video_mode) = monitor.current_video_mode() else { - return false; - }; + monitors.into_iter().find(|monitor| { + let Some(monitor_position) = monitor.position() else { + return false; + }; + let Some(video_mode) = monitor.current_video_mode() else { + return false; + }; - let monitor_size = video_mode.size(); - let position = position.to_physical::(monitor.scale_factor()); + let monitor_size = video_mode.size(); + let position = position.to_physical::(monitor.scale_factor()); - monitor_position.x <= position.x - && position.x < monitor_position.x + monitor_size.width as i32 - && monitor_position.y <= position.y - && position.y < monitor_position.y + monitor_size.height as i32 - }) + monitor_position.x <= position.x + && position.x < monitor_position.x + monitor_size.width as i32 + && monitor_position.y <= position.y + && position.y < monitor_position.y + monitor_size.height as i32 + }) } fn clamp_surface_size(attrs: &WindowAttributes, scale_factor: f64) -> PhysicalSize { - let mut size = attrs - .surface_size - .unwrap_or_else(|| PhysicalSize::new(800, 600).into()) - .to_physical::(scale_factor); - - if let Some(min_size) = attrs.min_surface_size { - let min_size = min_size.to_physical::(scale_factor); - size.width = size.width.max(min_size.width); - size.height = size.height.max(min_size.height); - } - - if let Some(max_size) = attrs.max_surface_size { - let max_size = max_size.to_physical::(scale_factor); - size.width = size.width.min(max_size.width); - size.height = size.height.min(max_size.height); - } - - size + let mut size = attrs + .surface_size + .unwrap_or_else(|| PhysicalSize::new(800, 600).into()) + .to_physical::(scale_factor); + + if let Some(min_size) = attrs.min_surface_size { + let min_size = min_size.to_physical::(scale_factor); + size.width = size.width.max(min_size.width); + size.height = size.height.max(min_size.height); + } + + if let Some(max_size) = attrs.max_surface_size { + let max_size = max_size.to_physical::(scale_factor); + size.width = size.width.min(max_size.width); + size.height = size.height.min(max_size.height); + } + + size } fn apply_prevent_overflow( - attrs: &mut WindowAttributes, - window_size: &mut PhysicalSize, - monitor: &MonitorHandle, - margin: Size, + attrs: &mut WindowAttributes, + window_size: &mut PhysicalSize, + monitor: &MonitorHandle, + margin: Size, ) { - let work_area = monitor.work_area(); - let margin = margin.to_physical::(monitor.scale_factor()); - let constraint = PhysicalSize::new( - work_area.size.width.saturating_sub(margin.width), - work_area.size.height.saturating_sub(margin.height), - ); - - if window_size.width > constraint.width || window_size.height > constraint.height { - window_size.width = window_size.width.min(constraint.width); - window_size.height = window_size.height.min(constraint.height); - attrs.surface_size = Some((*window_size).into()); - } + let work_area = monitor.work_area(); + let margin = margin.to_physical::(monitor.scale_factor()); + let constraint = PhysicalSize::new( + work_area.size.width.saturating_sub(margin.width), + work_area.size.height.saturating_sub(margin.height), + ); + + if window_size.width > constraint.width || window_size.height > constraint.height { + window_size.width = window_size.width.min(constraint.width); + window_size.height = window_size.height.min(constraint.height); + attrs.surface_size = Some((*window_size).into()); + } } fn prepare_window_attributes(event_loop: &dyn ActiveEventLoop, attrs: &mut AppWindowAttrs) { - if !attrs.center && attrs.prevent_overflow.is_none() { - return; - } - - let monitor = attrs - .inner - .position - .and_then(|position| { - let monitors = event_loop.available_monitors(); - find_monitor_for_position(monitors, position) - }) - .or_else(|| event_loop.primary_monitor()); - - let Some(monitor) = monitor else { - return; - }; - - // `clamp_surface_size` is the requested client/surface size; the window - // winit creates will be larger by the non-client frame (title bar + borders). - // To center the *visible* window we have to account for that frame. - let mut window_size = clamp_surface_size(&attrs.inner, monitor.scale_factor()); - - // Left and right borders count toward the outer width (and so toward the - // centered x position) but not toward the surface size. The title bar adds to - // the visible height. Mirrors `tauri-runtime-wry`'s creation-time centering. - #[allow(unused_mut)] - let mut shadow_width: u32 = 0; - #[cfg(windows)] - if attrs.inner.decorations { - use windows::Win32::{ - Foundation::RECT, - UI::WindowsAndMessaging::{AdjustWindowRect, WS_OVERLAPPEDWINDOW}, + if !attrs.center && attrs.prevent_overflow.is_none() { + return; + } + + let monitor = attrs + .inner + .position + .and_then(|position| { + let monitors = event_loop.available_monitors(); + find_monitor_for_position(monitors, position) + }) + .or_else(|| event_loop.primary_monitor()); + + let Some(monitor) = monitor else { + return; }; - let mut rect = RECT::default(); - if unsafe { AdjustWindowRect(&mut rect, WS_OVERLAPPEDWINDOW, false) }.is_ok() { - shadow_width = (rect.right - rect.left) as u32; - // `rect.top` is negative (the title bar above the client area); - // `rect.bottom` is the bottom shadow, which we intentionally ignore. - window_size.height += (-rect.top) as u32; + // `clamp_surface_size` is the requested client/surface size; the window + // winit creates will be larger by the non-client frame (title bar + borders). + // To center the *visible* window we have to account for that frame. + let mut window_size = clamp_surface_size(&attrs.inner, monitor.scale_factor()); + + // Left and right borders count toward the outer width (and so toward the + // centered x position) but not toward the surface size. The title bar adds to + // the visible height. Mirrors `tauri-runtime-wry`'s creation-time centering. + #[allow(unused_mut)] + let mut shadow_width: u32 = 0; + #[cfg(windows)] + if attrs.inner.decorations { + use windows::Win32::{ + Foundation::RECT, + UI::WindowsAndMessaging::{AdjustWindowRect, WS_OVERLAPPEDWINDOW}, + }; + + let mut rect = RECT::default(); + if unsafe { AdjustWindowRect(&mut rect, WS_OVERLAPPEDWINDOW, false) }.is_ok() { + shadow_width = (rect.right - rect.left) as u32; + // `rect.top` is negative (the title bar above the client area); + // `rect.bottom` is the bottom shadow, which we intentionally ignore. + window_size.height += (-rect.top) as u32; + } } - } - if let Some(margin) = attrs.prevent_overflow { - apply_prevent_overflow(&mut attrs.inner, &mut window_size, &monitor, margin); - } + if let Some(margin) = attrs.prevent_overflow { + apply_prevent_overflow(&mut attrs.inner, &mut window_size, &monitor, margin); + } - if attrs.center { - window_size.width += shadow_width; - let position = calculate_window_center_position(window_size, &monitor); - attrs.inner.position = Some(position.into()); - } + if attrs.center { + window_size.width += shadow_width; + let position = calculate_window_center_position(window_size, &monitor); + attrs.inner.position = Some(position.into()); + } } pub(crate) fn paired_size_constraint( - width: Option, - height: Option, + width: Option, + height: Option, ) -> Option { - match (width, height) { - ( - Some(tauri_runtime::dpi::PixelUnit::Logical(width)), - Some(tauri_runtime::dpi::PixelUnit::Logical(height)), - ) => Some(Size::Logical(tauri_runtime::dpi::LogicalSize::new( - width.into(), - height.into(), - ))), - ( - Some(tauri_runtime::dpi::PixelUnit::Physical(width)), - Some(tauri_runtime::dpi::PixelUnit::Physical(height)), - ) => Some(Size::Physical(PhysicalSize::new( - width.into(), - height.into(), - ))), - _ => None, - } + match (width, height) { + ( + Some(tauri_runtime::dpi::PixelUnit::Logical(width)), + Some(tauri_runtime::dpi::PixelUnit::Logical(height)), + ) => Some(Size::Logical(tauri_runtime::dpi::LogicalSize::new( + width.into(), + height.into(), + ))), + ( + Some(tauri_runtime::dpi::PixelUnit::Physical(width)), + Some(tauri_runtime::dpi::PixelUnit::Physical(height)), + ) => Some(Size::Physical(PhysicalSize::new( + width.into(), + height.into(), + ))), + _ => None, + } } pub(crate) enum WindowMessage { - AddEventListener(WindowEventId, WindowEventListener), - Close, - Destroy, - ScaleFactor(Sender>), - InnerPosition(Sender>>), - OuterPosition(Sender>>), - InnerSize(Sender>>), - OuterSize(Sender>>), - IsFullscreen(Sender>), - IsMinimized(Sender>), - IsMaximized(Sender>), - IsFocused(Sender>), - IsDecorated(Sender>), - IsResizable(Sender>), - IsMaximizable(Sender>), - IsMinimizable(Sender>), - IsClosable(Sender>), - IsVisible(Sender>), - IsEnabled(Sender>), - IsAlwaysOnTop(Sender>), - Title(Sender>), - CurrentMonitor(Sender>>), - PrimaryMonitor(Sender>>), - MonitorFromPoint(Sender>>, f64, f64), - AvailableMonitors(Sender>>), - RawWindowHandle(Sender>), - Theme(Sender>), - Center, - RequestUserAttention(Option), - SetEnabled(bool), - SetResizable(bool), - SetMaximizable(bool), - SetMinimizable(bool), - SetClosable(bool), - SetTitle(String), - Maximize, - Unmaximize, - Minimize, - Unminimize, - Show, - Hide, - SetDecorations(bool), - SetShadow(bool), - SetAlwaysOnBottom(bool), - SetAlwaysOnTop(bool), - SetVisibleOnAllWorkspaces(bool), - SetContentProtected(bool), - SetSize(Size), - SetMinSize(Option), - SetMaxSize(Option), - SetSizeConstraints(WindowSizeConstraints), - SetPosition(Position), - SetFullscreen(bool), - #[cfg(target_os = "macos")] - SetSimpleFullscreen(bool), - SetFocus, - // TODO: Implement SetFocusable, winit currently doesn't expose an API for it - #[allow(unused)] - SetFocusable(bool), - SetIcon(Icon<'static>), - SetSkipTaskbar(bool), - SetCursorGrab(bool), - SetCursorVisible(bool), - SetCursorIcon(CursorIcon), - SetCursorPosition(Position), - SetIgnoreCursorEvents(bool), - SetProgressBar(ProgressBarState), - SetBadgeCount(Option, Option), - SetBadgeLabel(Option), - SetOverlayIcon(Option>), - SetTitleBarStyle(tauri_utils::TitleBarStyle), - SetTrafficLightPosition(Position), - SetTheme(Option), - SetBackgroundColor(Option), - StartDragging, - StartResizeDragging(tauri_runtime::ResizeDirection), + AddEventListener(WindowEventId, WindowEventListener), + Close, + Destroy, + ScaleFactor(Sender>), + InnerPosition(Sender>>), + OuterPosition(Sender>>), + InnerSize(Sender>>), + OuterSize(Sender>>), + IsFullscreen(Sender>), + IsMinimized(Sender>), + IsMaximized(Sender>), + IsFocused(Sender>), + IsDecorated(Sender>), + IsResizable(Sender>), + IsMaximizable(Sender>), + IsMinimizable(Sender>), + IsClosable(Sender>), + IsVisible(Sender>), + IsEnabled(Sender>), + IsAlwaysOnTop(Sender>), + Title(Sender>), + CurrentMonitor(Sender>>), + PrimaryMonitor(Sender>>), + MonitorFromPoint(Sender>>, f64, f64), + AvailableMonitors(Sender>>), + RawWindowHandle(Sender>), + Theme(Sender>), + Center, + RequestUserAttention(Option), + SetEnabled(bool), + SetResizable(bool), + SetMaximizable(bool), + SetMinimizable(bool), + SetClosable(bool), + SetTitle(String), + Maximize, + Unmaximize, + Minimize, + Unminimize, + Show, + Hide, + SetDecorations(bool), + SetShadow(bool), + SetAlwaysOnBottom(bool), + SetAlwaysOnTop(bool), + SetVisibleOnAllWorkspaces(bool), + SetContentProtected(bool), + SetSize(Size), + SetMinSize(Option), + SetMaxSize(Option), + SetSizeConstraints(WindowSizeConstraints), + SetPosition(Position), + SetFullscreen(bool), + #[cfg(target_os = "macos")] + SetSimpleFullscreen(bool), + SetFocus, + // TODO: Implement SetFocusable, winit currently doesn't expose an API for it + #[allow(unused)] + SetFocusable(bool), + SetIcon(Icon<'static>), + SetSkipTaskbar(bool), + SetCursorGrab(bool), + SetCursorVisible(bool), + SetCursorIcon(CursorIcon), + SetCursorPosition(Position), + SetIgnoreCursorEvents(bool), + SetProgressBar(ProgressBarState), + SetBadgeCount(Option, Option), + SetBadgeLabel(Option), + SetOverlayIcon(Option>), + SetTitleBarStyle(tauri_utils::TitleBarStyle), + SetTrafficLightPosition(Position), + SetTheme(Option), + SetBackgroundColor(Option), + StartDragging, + StartResizeDragging(tauri_runtime::ResizeDirection), } #[cfg(windows)] type SoftbufferSurface = softbuffer::Surface; pub(crate) struct AppWindow { - #[allow(unused)] - pub(crate) id: WindowId, - pub(crate) label: String, - #[cfg(windows)] - pub(crate) background_surface: Option, - pub(crate) window: Box, - pub(crate) attrs: AppWindowAttrs, - pub(crate) children: Vec, - pub(crate) listeners: WindowEventListeners, - #[cfg(target_os = "macos")] - pub(crate) appkit_state: Arc>, + #[allow(unused)] + pub(crate) id: WindowId, + pub(crate) label: String, + #[cfg(windows)] + pub(crate) background_surface: Option, + pub(crate) window: Box, + pub(crate) attrs: AppWindowAttrs, + pub(crate) children: Vec, + pub(crate) listeners: WindowEventListeners, + #[cfg(target_os = "macos")] + pub(crate) appkit_state: Arc>, } #[derive(Clone, Debug, Default)] pub(crate) struct AppWindowAttrs { - pub(crate) inner: WindowAttributes, - pub(crate) center: bool, - pub(crate) background_color: Option, - pub(crate) prevent_overflow: Option, - #[cfg(target_os = "macos")] - pub(crate) traffic_light_position: Option, - #[cfg(any( - target_os = "macos", - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - pub(crate) visible_on_all_workspaces: bool, - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - pub(crate) skip_taskbar: bool, + pub(crate) inner: WindowAttributes, + pub(crate) center: bool, + pub(crate) background_color: Option, + pub(crate) prevent_overflow: Option, + #[cfg(target_os = "macos")] + pub(crate) traffic_light_position: Option, + #[cfg(any( + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub(crate) visible_on_all_workspaces: bool, + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub(crate) skip_taskbar: bool, } impl AppWindow { - pub(crate) fn center(&self) { - let monitor = self.window.current_monitor(); - let monitor = monitor.or_else(|| self.window.primary_monitor()); - let Some(monitor) = monitor else { - return; - }; - - #[allow(unused_mut)] - let mut window_size = self.window.outer_size(); - - // On Windows `outer_size` includes the invisible resize/shadow border, so - // centering by it pushes the visible window down. Substitute the visible - // frame height reported by DWM. Mirrors `tauri-runtime-wry`'s `center`. - #[cfg(windows)] - if self.window.is_decorated() - && let Some(visible_height) = self.dwm_visible_frame_height() - { - window_size.height = visible_height; - } - - let position = calculate_window_center_position(window_size, &monitor); - self.window.set_outer_position(Position::Physical(position)); - } - - pub(crate) fn preferred_theme(&self) -> Option { - self - .attrs - .inner - .preferred_theme - .map(winit_theme_to_tauri_theme) - } - - pub(crate) fn resolved_theme(&self, app_wide_theme: Option) -> Option { - self.preferred_theme().or(app_wide_theme) - } - - pub(crate) fn set_theme(&mut self, theme: Option) { - self.attrs.inner.preferred_theme = tauri_theme_to_winit_theme(theme); - self.window.set_theme(tauri_theme_to_winit_theme(theme)); - self.apply_cef_theme(theme); - } + pub(crate) fn center(&self) { + let monitor = self.window.current_monitor(); + let monitor = monitor.or_else(|| self.window.primary_monitor()); + let Some(monitor) = monitor else { + return; + }; - fn apply_cef_theme(&self, theme: Option) { - for child in &self.children { - let request_context = child.host.request_context(); - request_context::apply_theme_scheme(request_context.as_ref(), theme); - } - } -} + #[allow(unused_mut)] + let mut window_size = self.window.outer_size(); -impl WinitCefApp { - pub(crate) fn create_window( - &mut self, - event_loop: &dyn ActiveEventLoop, - window_id: WindowId, - webview_id: Option, - pending: Box>>, - _after_window_creation: Option, - ) -> Result<()> { - let mut attrs = pending.window_builder.attrs.clone(); - if attrs.inner.preferred_theme.is_none() { - attrs.inner.preferred_theme = - tauri_theme_to_winit_theme(*self.context.app_wide_theme.lock().unwrap()); - } - prepare_window_attributes(event_loop, &mut attrs); - - let window = event_loop - .create_window(attrs.inner.clone()) - .map_err(|_| Error::CreateWindow)?; - - let winit_id = window.id(); - let mut appwindow = AppWindow { - id: window_id, - label: pending.label.clone(), - #[cfg(windows)] - background_surface: None, - window, - attrs, - children: Vec::new(), - listeners: Default::default(), - #[cfg(target_os = "macos")] - appkit_state: Arc::new(RwLock::new(AppkitState::default())), - }; + // On Windows `outer_size` includes the invisible resize/shadow border, so + // centering by it pushes the visible window down. Substitute the visible + // frame height reported by DWM. Mirrors `tauri-runtime-wry`'s `center`. + #[cfg(windows)] + if self.window.is_decorated() + && let Some(visible_height) = self.dwm_visible_frame_height() + { + window_size.height = visible_height; + } - #[cfg(target_os = "macos")] - { - appwindow.associate_appkit_state(); - appwindow.set_visible_on_all_workspaces(appwindow.attrs.visible_on_all_workspaces); - if let Some(position) = &appwindow.attrs.traffic_light_position { - appwindow.set_traffic_light_position(position); - } + let position = calculate_window_center_position(window_size, &monitor); + self.window.set_outer_position(Position::Physical(position)); } - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - { - appwindow.set_visible_on_all_workspaces(appwindow.attrs.visible_on_all_workspaces); - appwindow.set_skip_taskbar(appwindow.attrs.skip_taskbar); + pub(crate) fn preferred_theme(&self) -> Option { + self.attrs + .inner + .preferred_theme + .map(winit_theme_to_tauri_theme) } - #[cfg(windows)] - if appwindow.attrs.inner.transparent || appwindow.attrs.background_color.is_some() { - appwindow.draw_background_surface(); + pub(crate) fn resolved_theme(&self, app_wide_theme: Option) -> Option { + self.preferred_theme().or(app_wide_theme) } - #[cfg(not(windows))] - if appwindow.attrs.background_color.is_some() { - appwindow.set_background_color(appwindow.attrs.background_color); + pub(crate) fn set_theme(&mut self, theme: Option) { + self.attrs.inner.preferred_theme = tauri_theme_to_winit_theme(theme); + self.window.set_theme(tauri_theme_to_winit_theme(theme)); + self.apply_cef_theme(theme); } - #[cfg(any(windows, target_os = "macos"))] - if let Some(after_window_creation) = _after_window_creation { - after_window_creation(RawWindow { - #[cfg(windows)] - hwnd: appwindow.hwnd().0 as isize, - _marker: &PhantomData, - }); - } - - // Build the initial webview against the not-yet-registered window so a - // creation failure surfaces to the caller without leaving the window in - // state to roll back. - if let (Some(webview_id), Some(webview)) = (webview_id, pending.webview) { - Self::build_and_attach_webview( - &self.context, - &self.scheme_registry, - &mut self.state.live_browsers, - &mut appwindow, - webview_id, - browser_client::DragDropEventTarget::Window, - webview, - )?; - } - - self - .state - .winid_id_to_window_id_map - .insert(winit_id, window_id); - self.state.windows.insert(window_id, appwindow); - - Ok(()) - } - - pub(crate) fn handle_window_message( - &mut self, - event_loop: &dyn ActiveEventLoop, - window_id: WindowId, - message: WindowMessage, - ) { - // Handle Close and Destroy messages first to avoid borrowing issues with the window. - match message { - WindowMessage::Close => { - self.request_window_close(window_id, event_loop); - return; - } - WindowMessage::Destroy => { - self.close_window(window_id, event_loop); - return; - } - _ => {} + fn apply_cef_theme(&self, theme: Option) { + for child in &self.children { + let request_context = child.host.request_context(); + request_context::apply_theme_scheme(request_context.as_ref(), theme); + } } +} - let Some(appwindow) = self.state.windows.get_mut(&window_id) else { - return; - }; - let window = &appwindow.window; - - match message { - WindowMessage::AddEventListener(id, listener) => { - appwindow.listeners.lock().unwrap().insert(id, listener); - } - WindowMessage::Close | WindowMessage::Destroy => unreachable!("handled before borrowing"), - WindowMessage::ScaleFactor(tx) => _ = tx.send(Ok(window.scale_factor())), - WindowMessage::InnerSize(tx) => _ = tx.send(Ok(window.surface_size())), - WindowMessage::OuterSize(tx) => _ = tx.send(Ok(window.outer_size())), - WindowMessage::IsFullscreen(tx) => _ = tx.send(Ok(window.fullscreen().is_some())), - WindowMessage::IsMinimized(tx) => _ = tx.send(Ok(window.is_minimized().unwrap_or(false))), - WindowMessage::IsMaximized(tx) => _ = tx.send(Ok(window.is_maximized())), - WindowMessage::IsFocused(tx) => _ = tx.send(Ok(window.has_focus())), - WindowMessage::IsDecorated(tx) => _ = tx.send(Ok(window.is_decorated())), - WindowMessage::IsResizable(tx) => _ = tx.send(Ok(window.is_resizable())), - WindowMessage::IsMaximizable(tx) => { - let is_maximizable = window - .enabled_buttons() - .contains(winit::window::WindowButtons::MAXIMIZE); - let _ = tx.send(Ok(is_maximizable)); - } - WindowMessage::IsMinimizable(tx) => { - let is_minimizable = window - .enabled_buttons() - .contains(winit::window::WindowButtons::MINIMIZE); - let _ = tx.send(Ok(is_minimizable)); - } - WindowMessage::IsClosable(tx) => { - let is_closable = window - .enabled_buttons() - .contains(winit::window::WindowButtons::CLOSE); - let _ = tx.send(Ok(is_closable)); - } - WindowMessage::IsVisible(tx) => _ = tx.send(Ok(window.is_visible().unwrap_or(true))), - WindowMessage::IsEnabled(tx) => _ = tx.send(Ok(appwindow.is_enabled())), - WindowMessage::IsAlwaysOnTop(tx) => { - let is_on_top = appwindow.attrs.inner.window_level == WindowLevel::AlwaysOnTop; - let _ = tx.send(Ok(is_on_top)); - } - WindowMessage::Title(tx) => _ = tx.send(Ok(window.title())), - WindowMessage::InnerPosition(tx) => _ = tx.send(Ok(window.surface_position())), - WindowMessage::OuterPosition(tx) => { - let pos = window - .outer_position() - .map_err(|_| Error::FailedToGetMonitor); - let _ = tx.send(pos); - } - WindowMessage::CurrentMonitor(tx) => { - let current = window.current_monitor(); - let current = current.map(|m| winit_monitor_to_tauri_monitor(&m)); - let _ = tx.send(Ok(current)); - } - WindowMessage::PrimaryMonitor(tx) => { - let primary = window.primary_monitor(); - let primary = primary.map(|m| winit_monitor_to_tauri_monitor(&m)); - let _ = tx.send(Ok(primary)); - } - WindowMessage::MonitorFromPoint(tx, x, y) => { - let mut available_monitors = window.available_monitors(); - let monitor = available_monitors - .find(|m| { - let pos = m.position().unwrap_or_default(); - let vm = m.current_video_mode(); - let size = vm.map(|v| v.size()).unwrap_or_default(); - x >= pos.x as f64 - && x <= pos.x as f64 + size.width as f64 - && y >= pos.y as f64 - && y <= pos.y as f64 + size.height as f64 - }) - .map(|m| winit_monitor_to_tauri_monitor(&m)); - let _ = tx.send(Ok(monitor)); - } - WindowMessage::AvailableMonitors(tx) => { - let monitors = window - .available_monitors() - .map(|m| winit_monitor_to_tauri_monitor(&m)) - .collect(); - let _ = tx.send(Ok(monitors)); - } - WindowMessage::RawWindowHandle(tx) => { - let handle = window.window_handle(); - let send_handle = handle - .map(|h| SendRawWindowHandle(h.as_raw())) - .map_err(|_| Error::FailedToSendMessage); - let _ = tx.send(send_handle); - } - WindowMessage::Theme(tx) => { - let theme = window.theme(); - let theme = theme.map(winit_theme_to_tauri_theme); - let theme = theme.unwrap_or(Theme::Light); - let _ = tx.send(Ok(theme)); - } - WindowMessage::Center => { - appwindow.center(); - } - WindowMessage::RequestUserAttention(attention) => { - window.request_user_attention(match attention { - Some(UserAttentionType::Critical) => Some(winit::window::UserAttentionType::Critical), - Some(UserAttentionType::Informational) => { - Some(winit::window::UserAttentionType::Informational) - } - None => None, - }) - } - WindowMessage::SetEnabled(value) => appwindow.set_enabled(value), - WindowMessage::SetResizable(value) => window.set_resizable(value), - WindowMessage::SetTitle(title) => window.set_title(&title), - WindowMessage::Maximize => window.set_maximized(true), - WindowMessage::Unmaximize => window.set_maximized(false), - WindowMessage::Minimize => window.set_minimized(true), - WindowMessage::Unminimize => window.set_minimized(false), - WindowMessage::Show => window.set_visible(true), - WindowMessage::Hide => window.set_visible(false), - WindowMessage::SetDecorations(value) => window.set_decorations(value), - WindowMessage::SetSize(size) => _ = window.request_surface_size(size), - WindowMessage::SetPosition(position) => window.set_outer_position(position), - WindowMessage::SetFullscreen(value) => { - window.set_fullscreen(value.then_some(Fullscreen::Borderless(None))) - } - #[cfg(target_os = "macos")] - WindowMessage::SetSimpleFullscreen(value) => { - window.set_simple_fullscreen(value); - } - WindowMessage::SetFocus => window.focus_window(), - WindowMessage::SetMinSize(min_size) => window.set_min_surface_size(min_size), - WindowMessage::SetMaxSize(max_size) => window.set_max_surface_size(max_size), - WindowMessage::SetMaximizable(value) => { - let mut buttons = window.enabled_buttons(); - buttons.set(winit::window::WindowButtons::MAXIMIZE, value); - window.set_enabled_buttons(buttons); - } - WindowMessage::SetMinimizable(value) => { - let mut buttons = window.enabled_buttons(); - buttons.set(winit::window::WindowButtons::MINIMIZE, value); - window.set_enabled_buttons(buttons); - } - WindowMessage::SetClosable(value) => { - let mut buttons = window.enabled_buttons(); - buttons.set(winit::window::WindowButtons::CLOSE, value); - window.set_enabled_buttons(buttons); - } - WindowMessage::SetAlwaysOnBottom(value) => { - let level = match value { - true => WindowLevel::AlwaysOnBottom, - false => WindowLevel::Normal, - }; - appwindow.attrs.inner.window_level = level; - window.set_window_level(level); - } - WindowMessage::SetAlwaysOnTop(value) => { - let level = match value { - true => WindowLevel::AlwaysOnTop, - false => WindowLevel::Normal, +impl WinitCefApp { + pub(crate) fn create_window( + &mut self, + event_loop: &dyn ActiveEventLoop, + window_id: WindowId, + webview_id: Option, + pending: Box>>, + _after_window_creation: Option, + ) -> Result<()> { + let mut attrs = pending.window_builder.attrs.clone(); + if attrs.inner.preferred_theme.is_none() { + attrs.inner.preferred_theme = + tauri_theme_to_winit_theme(*self.context.app_wide_theme.lock().unwrap()); + } + prepare_window_attributes(event_loop, &mut attrs); + + let window = event_loop + .create_window(attrs.inner.clone()) + .map_err(|_| Error::CreateWindow)?; + + let winit_id = window.id(); + let mut appwindow = AppWindow { + id: window_id, + label: pending.label.clone(), + #[cfg(windows)] + background_surface: None, + window, + attrs, + children: Vec::new(), + listeners: Default::default(), + #[cfg(target_os = "macos")] + appkit_state: Arc::new(RwLock::new(AppkitState::default())), }; - appwindow.attrs.inner.window_level = level; - window.set_window_level(level); - } - WindowMessage::SetVisibleOnAllWorkspaces(_value) => { + #[cfg(target_os = "macos")] { - appwindow.attrs.visible_on_all_workspaces = _value; - appwindow.set_visible_on_all_workspaces(_value); + appwindow.associate_appkit_state(); + appwindow.set_visible_on_all_workspaces(appwindow.attrs.visible_on_all_workspaces); + if let Some(position) = &appwindow.attrs.traffic_light_position { + appwindow.set_traffic_light_position(position); + } } + #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" ))] { - appwindow.attrs.visible_on_all_workspaces = _value; - appwindow.set_visible_on_all_workspaces(_value); - } - } - WindowMessage::SetContentProtected(value) => window.set_content_protected(value), - WindowMessage::SetIcon(icon) => { - if let Ok(icon) = super::window::tauri_icon_to_winit_icon(icon) { - window.set_window_icon(Some(icon)) + appwindow.set_visible_on_all_workspaces(appwindow.attrs.visible_on_all_workspaces); + appwindow.set_skip_taskbar(appwindow.attrs.skip_taskbar); } - } - WindowMessage::SetSkipTaskbar(_value) => { - #[cfg(windows)] - window.set_skip_taskbar(_value); - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - appwindow.set_skip_taskbar(_value); - } - WindowMessage::SetShadow(_value) => { + #[cfg(windows)] - window.set_undecorated_shadow(_value); - #[cfg(target_os = "macos")] - window.set_has_shadow(_value); - } - WindowMessage::SetCursorGrab(value) => { - let _ = window.set_cursor_grab(match value { - true => winit::window::CursorGrabMode::Confined, - false => winit::window::CursorGrabMode::None, - }); - } - WindowMessage::SetCursorVisible(value) => window.set_cursor_visible(value), - WindowMessage::SetCursorIcon(value) => { - let cursor_icon = tauri_cursor_to_winit_cursor(value); - window.set_cursor(cursor_icon.into()) - } - WindowMessage::SetCursorPosition(value) => _ = window.set_cursor_position(value), - WindowMessage::SetIgnoreCursorEvents(value) => _ = window.set_cursor_hittest(!value), - - WindowMessage::SetTrafficLightPosition(_position) => { - #[cfg(target_os = "macos")] - { - appwindow.attrs.traffic_light_position = Some(_position.clone()); - appwindow.set_traffic_light_position(&_position); + if appwindow.attrs.inner.transparent || appwindow.attrs.background_color.is_some() { + appwindow.draw_background_surface(); + } + + #[cfg(not(windows))] + if appwindow.attrs.background_color.is_some() { + appwindow.set_background_color(appwindow.attrs.background_color); + } + + #[cfg(any(windows, target_os = "macos"))] + if let Some(after_window_creation) = _after_window_creation { + after_window_creation(RawWindow { + #[cfg(windows)] + hwnd: appwindow.hwnd().0 as isize, + _marker: &PhantomData, + }); + } + + // Build the initial webview against the not-yet-registered window so a + // creation failure surfaces to the caller without leaving the window in + // state to roll back. + if let (Some(webview_id), Some(webview)) = (webview_id, pending.webview) { + Self::build_and_attach_webview( + &self.context, + &self.scheme_registry, + &mut self.state.live_browsers, + &mut appwindow, + webview_id, + browser_client::DragDropEventTarget::Window, + webview, + )?; + } + + self.state + .winid_id_to_window_id_map + .insert(winit_id, window_id); + self.state.windows.insert(window_id, appwindow); + + Ok(()) + } + + pub(crate) fn handle_window_message( + &mut self, + event_loop: &dyn ActiveEventLoop, + window_id: WindowId, + message: WindowMessage, + ) { + // Handle Close and Destroy messages first to avoid borrowing issues with the window. + match message { + WindowMessage::Close => { + self.request_window_close(window_id, event_loop); + return; + } + WindowMessage::Destroy => { + self.close_window(window_id, event_loop); + return; + } + _ => {} + } + + let Some(appwindow) = self.state.windows.get_mut(&window_id) else { + return; + }; + let window = &appwindow.window; + + match message { + WindowMessage::AddEventListener(id, listener) => { + appwindow.listeners.lock().unwrap().insert(id, listener); + } + WindowMessage::Close | WindowMessage::Destroy => { + unreachable!("handled before borrowing") + } + WindowMessage::ScaleFactor(tx) => _ = tx.send(Ok(window.scale_factor())), + WindowMessage::InnerSize(tx) => _ = tx.send(Ok(window.surface_size())), + WindowMessage::OuterSize(tx) => _ = tx.send(Ok(window.outer_size())), + WindowMessage::IsFullscreen(tx) => _ = tx.send(Ok(window.fullscreen().is_some())), + WindowMessage::IsMinimized(tx) => { + _ = tx.send(Ok(window.is_minimized().unwrap_or(false))) + } + WindowMessage::IsMaximized(tx) => _ = tx.send(Ok(window.is_maximized())), + WindowMessage::IsFocused(tx) => _ = tx.send(Ok(window.has_focus())), + WindowMessage::IsDecorated(tx) => _ = tx.send(Ok(window.is_decorated())), + WindowMessage::IsResizable(tx) => _ = tx.send(Ok(window.is_resizable())), + WindowMessage::IsMaximizable(tx) => { + let is_maximizable = window + .enabled_buttons() + .contains(winit::window::WindowButtons::MAXIMIZE); + let _ = tx.send(Ok(is_maximizable)); + } + WindowMessage::IsMinimizable(tx) => { + let is_minimizable = window + .enabled_buttons() + .contains(winit::window::WindowButtons::MINIMIZE); + let _ = tx.send(Ok(is_minimizable)); + } + WindowMessage::IsClosable(tx) => { + let is_closable = window + .enabled_buttons() + .contains(winit::window::WindowButtons::CLOSE); + let _ = tx.send(Ok(is_closable)); + } + WindowMessage::IsVisible(tx) => _ = tx.send(Ok(window.is_visible().unwrap_or(true))), + WindowMessage::IsEnabled(tx) => _ = tx.send(Ok(appwindow.is_enabled())), + WindowMessage::IsAlwaysOnTop(tx) => { + let is_on_top = appwindow.attrs.inner.window_level == WindowLevel::AlwaysOnTop; + let _ = tx.send(Ok(is_on_top)); + } + WindowMessage::Title(tx) => _ = tx.send(Ok(window.title())), + WindowMessage::InnerPosition(tx) => _ = tx.send(Ok(window.surface_position())), + WindowMessage::OuterPosition(tx) => { + let pos = window + .outer_position() + .map_err(|_| Error::FailedToGetMonitor); + let _ = tx.send(pos); + } + WindowMessage::CurrentMonitor(tx) => { + let current = window.current_monitor(); + let current = current.map(|m| winit_monitor_to_tauri_monitor(&m)); + let _ = tx.send(Ok(current)); + } + WindowMessage::PrimaryMonitor(tx) => { + let primary = window.primary_monitor(); + let primary = primary.map(|m| winit_monitor_to_tauri_monitor(&m)); + let _ = tx.send(Ok(primary)); + } + WindowMessage::MonitorFromPoint(tx, x, y) => { + let mut available_monitors = window.available_monitors(); + let monitor = available_monitors + .find(|m| { + let pos = m.position().unwrap_or_default(); + let vm = m.current_video_mode(); + let size = vm.map(|v| v.size()).unwrap_or_default(); + x >= pos.x as f64 + && x <= pos.x as f64 + size.width as f64 + && y >= pos.y as f64 + && y <= pos.y as f64 + size.height as f64 + }) + .map(|m| winit_monitor_to_tauri_monitor(&m)); + let _ = tx.send(Ok(monitor)); + } + WindowMessage::AvailableMonitors(tx) => { + let monitors = window + .available_monitors() + .map(|m| winit_monitor_to_tauri_monitor(&m)) + .collect(); + let _ = tx.send(Ok(monitors)); + } + WindowMessage::RawWindowHandle(tx) => { + let handle = window.window_handle(); + let send_handle = handle + .map(|h| SendRawWindowHandle(h.as_raw())) + .map_err(|_| Error::FailedToSendMessage); + let _ = tx.send(send_handle); + } + WindowMessage::Theme(tx) => { + let theme = window.theme(); + let theme = theme.map(winit_theme_to_tauri_theme); + let theme = theme.unwrap_or(Theme::Light); + let _ = tx.send(Ok(theme)); + } + WindowMessage::Center => { + appwindow.center(); + } + WindowMessage::RequestUserAttention(attention) => { + window.request_user_attention(match attention { + Some(UserAttentionType::Critical) => { + Some(winit::window::UserAttentionType::Critical) + } + Some(UserAttentionType::Informational) => { + Some(winit::window::UserAttentionType::Informational) + } + None => None, + }) + } + WindowMessage::SetEnabled(value) => appwindow.set_enabled(value), + WindowMessage::SetResizable(value) => window.set_resizable(value), + WindowMessage::SetTitle(title) => window.set_title(&title), + WindowMessage::Maximize => window.set_maximized(true), + WindowMessage::Unmaximize => window.set_maximized(false), + WindowMessage::Minimize => window.set_minimized(true), + WindowMessage::Unminimize => window.set_minimized(false), + WindowMessage::Show => window.set_visible(true), + WindowMessage::Hide => window.set_visible(false), + WindowMessage::SetDecorations(value) => window.set_decorations(value), + WindowMessage::SetSize(size) => _ = window.request_surface_size(size), + WindowMessage::SetPosition(position) => window.set_outer_position(position), + WindowMessage::SetFullscreen(value) => { + window.set_fullscreen(value.then_some(Fullscreen::Borderless(None))) + } + #[cfg(target_os = "macos")] + WindowMessage::SetSimpleFullscreen(value) => { + window.set_simple_fullscreen(value); + } + WindowMessage::SetFocus => window.focus_window(), + WindowMessage::SetMinSize(min_size) => window.set_min_surface_size(min_size), + WindowMessage::SetMaxSize(max_size) => window.set_max_surface_size(max_size), + WindowMessage::SetMaximizable(value) => { + let mut buttons = window.enabled_buttons(); + buttons.set(winit::window::WindowButtons::MAXIMIZE, value); + window.set_enabled_buttons(buttons); + } + WindowMessage::SetMinimizable(value) => { + let mut buttons = window.enabled_buttons(); + buttons.set(winit::window::WindowButtons::MINIMIZE, value); + window.set_enabled_buttons(buttons); + } + WindowMessage::SetClosable(value) => { + let mut buttons = window.enabled_buttons(); + buttons.set(winit::window::WindowButtons::CLOSE, value); + window.set_enabled_buttons(buttons); + } + WindowMessage::SetAlwaysOnBottom(value) => { + let level = match value { + true => WindowLevel::AlwaysOnBottom, + false => WindowLevel::Normal, + }; + appwindow.attrs.inner.window_level = level; + window.set_window_level(level); + } + WindowMessage::SetAlwaysOnTop(value) => { + let level = match value { + true => WindowLevel::AlwaysOnTop, + false => WindowLevel::Normal, + }; + appwindow.attrs.inner.window_level = level; + window.set_window_level(level); + } + WindowMessage::SetVisibleOnAllWorkspaces(_value) => { + #[cfg(target_os = "macos")] + { + appwindow.attrs.visible_on_all_workspaces = _value; + appwindow.set_visible_on_all_workspaces(_value); + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + appwindow.attrs.visible_on_all_workspaces = _value; + appwindow.set_visible_on_all_workspaces(_value); + } + } + WindowMessage::SetContentProtected(value) => window.set_content_protected(value), + WindowMessage::SetIcon(icon) => { + if let Ok(icon) = super::window::tauri_icon_to_winit_icon(icon) { + window.set_window_icon(Some(icon)) + } + } + WindowMessage::SetSkipTaskbar(_value) => { + #[cfg(windows)] + window.set_skip_taskbar(_value); + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + appwindow.set_skip_taskbar(_value); + } + WindowMessage::SetShadow(_value) => { + #[cfg(windows)] + window.set_undecorated_shadow(_value); + #[cfg(target_os = "macos")] + window.set_has_shadow(_value); + } + WindowMessage::SetCursorGrab(value) => { + let _ = window.set_cursor_grab(match value { + true => winit::window::CursorGrabMode::Confined, + false => winit::window::CursorGrabMode::None, + }); + } + WindowMessage::SetCursorVisible(value) => window.set_cursor_visible(value), + WindowMessage::SetCursorIcon(value) => { + let cursor_icon = tauri_cursor_to_winit_cursor(value); + window.set_cursor(cursor_icon.into()) + } + WindowMessage::SetCursorPosition(value) => _ = window.set_cursor_position(value), + WindowMessage::SetIgnoreCursorEvents(value) => _ = window.set_cursor_hittest(!value), + + WindowMessage::SetTrafficLightPosition(_position) => { + #[cfg(target_os = "macos")] + { + appwindow.attrs.traffic_light_position = Some(_position.clone()); + appwindow.set_traffic_light_position(&_position); + } + } + WindowMessage::SetTitleBarStyle(_style) => { + #[cfg(target_os = "macos")] + appwindow.set_title_bar_style(_style); + } + WindowMessage::SetBackgroundColor(color) => { + appwindow.attrs.background_color = color; + appwindow.set_background_color(color); + } + WindowMessage::SetTheme(theme) => appwindow.set_theme(theme), + WindowMessage::SetBadgeCount(count, desktop_filename) => { + event_loop.set_badge_count(count, desktop_filename) + } + WindowMessage::SetBadgeLabel(label) => event_loop.set_badge_label(label), + WindowMessage::SetOverlayIcon(_icon) => { + #[cfg(windows)] + appwindow.set_overlay_icon(_icon); + } + WindowMessage::StartDragging => _ = window.drag_window(), + WindowMessage::StartResizeDragging(direction) => { + let _ = window.drag_resize_window(tauri_resize_direction_to_winit(direction)); + } + WindowMessage::SetSizeConstraints(constraints) => { + // TODO: upstream individual width/height size constraints to winit. + let min_size = + paired_size_constraint(constraints.min_width, constraints.min_height); + let max_size = + paired_size_constraint(constraints.max_width, constraints.max_height); + window.set_min_surface_size(min_size); + window.set_max_surface_size(max_size); + } + + WindowMessage::SetFocusable(_) => { + // TODO + } + WindowMessage::SetProgressBar(state) => { + #[cfg(target_os = "macos")] + event_loop.set_progress_bar(state); + #[cfg(not(target_os = "macos"))] + appwindow.set_progress_bar(state); + } } - } - WindowMessage::SetTitleBarStyle(_style) => { - #[cfg(target_os = "macos")] - appwindow.set_title_bar_style(_style); - } - WindowMessage::SetBackgroundColor(color) => { - appwindow.attrs.background_color = color; - appwindow.set_background_color(color); - } - WindowMessage::SetTheme(theme) => appwindow.set_theme(theme), - WindowMessage::SetBadgeCount(count, desktop_filename) => { - event_loop.set_badge_count(count, desktop_filename) - } - WindowMessage::SetBadgeLabel(label) => event_loop.set_badge_label(label), - WindowMessage::SetOverlayIcon(_icon) => { - #[cfg(windows)] - appwindow.set_overlay_icon(_icon); - } - WindowMessage::StartDragging => _ = window.drag_window(), - WindowMessage::StartResizeDragging(direction) => { - let _ = window.drag_resize_window(tauri_resize_direction_to_winit(direction)); - } - WindowMessage::SetSizeConstraints(constraints) => { - // TODO: upstream individual width/height size constraints to winit. - let min_size = paired_size_constraint(constraints.min_width, constraints.min_height); - let max_size = paired_size_constraint(constraints.max_width, constraints.max_height); - window.set_min_surface_size(min_size); - window.set_max_surface_size(max_size); - } - - WindowMessage::SetFocusable(_) => { - // TODO - } - WindowMessage::SetProgressBar(state) => { - #[cfg(target_os = "macos")] - event_loop.set_progress_bar(state); - #[cfg(not(target_os = "macos"))] - appwindow.set_progress_bar(state); - } } - } } #[derive(Debug, Clone)] pub struct CefWindowDispatcher { - pub(crate) window_id: WindowId, - pub(crate) context: RuntimeContext, + pub(crate) window_id: WindowId, + pub(crate) context: RuntimeContext, } fn getter( - context: &RuntimeContext, - message: Message, - receiver: Receiver>, + context: &RuntimeContext, + message: Message, + receiver: Receiver>, ) -> Result { - context.send_message(message)?; - receiver.recv().map_err(|_| Error::FailedToReceiveMessage)? + context.send_message(message)?; + receiver.recv().map_err(|_| Error::FailedToReceiveMessage)? } macro_rules! window_getter { - ($self:ident, $variant:ident) => {{ - let (tx, rx) = mpsc::channel(); - getter( - &$self.context, - Message::Window { - window_id: $self.window_id, - message: WindowMessage::$variant(tx), - }, - rx, - ) - }}; + ($self:ident, $variant:ident) => {{ + let (tx, rx) = mpsc::channel(); + getter( + &$self.context, + Message::Window { + window_id: $self.window_id, + message: WindowMessage::$variant(tx), + }, + rx, + ) + }}; } impl WindowDispatch for CefWindowDispatcher { - type Runtime = CefRuntime; - type WindowBuilder = WindowBuilderWrapper; - - fn run_on_main_thread(&self, f: F) -> Result<()> { - self.context.run_on_main_thread(f) - } - - fn on_window_event(&self, f: F) -> WindowEventId { - let id = self.context.next_window_event_id(); - let _ = self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::AddEventListener(id, Box::new(f)), - }); - id - } - - fn scale_factor(&self) -> Result { - window_getter!(self, ScaleFactor) - } - - fn inner_position(&self) -> Result> { - window_getter!(self, InnerPosition) - } - - fn outer_position(&self) -> Result> { - window_getter!(self, OuterPosition) - } - - fn inner_size(&self) -> Result> { - window_getter!(self, InnerSize) - } - - fn outer_size(&self) -> Result> { - window_getter!(self, OuterSize) - } - - fn is_fullscreen(&self) -> Result { - window_getter!(self, IsFullscreen) - } - - fn is_minimized(&self) -> Result { - window_getter!(self, IsMinimized) - } - - fn is_maximized(&self) -> Result { - window_getter!(self, IsMaximized) - } - - fn is_focused(&self) -> Result { - window_getter!(self, IsFocused) - } - - fn is_decorated(&self) -> Result { - window_getter!(self, IsDecorated) - } - - fn is_resizable(&self) -> Result { - window_getter!(self, IsResizable) - } - - fn is_maximizable(&self) -> Result { - window_getter!(self, IsMaximizable) - } - - fn is_minimizable(&self) -> Result { - window_getter!(self, IsMinimizable) - } - - fn is_closable(&self) -> Result { - window_getter!(self, IsClosable) - } - - fn is_visible(&self) -> Result { - window_getter!(self, IsVisible) - } - - fn is_enabled(&self) -> Result { - window_getter!(self, IsEnabled) - } - - fn is_always_on_top(&self) -> Result { - window_getter!(self, IsAlwaysOnTop) - } - - fn title(&self) -> Result { - window_getter!(self, Title) - } - - fn current_monitor(&self) -> Result> { - window_getter!(self, CurrentMonitor) - } - - fn primary_monitor(&self) -> Result> { - window_getter!(self, PrimaryMonitor) - } - - fn monitor_from_point(&self, x: f64, y: f64) -> Result> { - let (tx, rx) = mpsc::channel(); - getter( - &self.context, - Message::Window { - window_id: self.window_id, - message: WindowMessage::MonitorFromPoint(tx, x, y), - }, - rx, - ) - } - - fn available_monitors(&self) -> Result> { - window_getter!(self, AvailableMonitors) - } - - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - fn gtk_window(&self) -> Result { - Err(Error::FailedToSendMessage) - } - - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - fn default_vbox(&self) -> Result { - Err(Error::FailedToSendMessage) - } - - fn window_handle( - &self, - ) -> std::result::Result, raw_window_handle::HandleError> { - let handle: Result = window_getter!(self, RawWindowHandle); - let handle = handle.map_err(|_| raw_window_handle::HandleError::Unavailable)?; - Ok(unsafe { raw_window_handle::WindowHandle::borrow_raw(handle.0) }) - } - - fn theme(&self) -> Result { - window_getter!(self, Theme) - } - - fn center(&self) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Center, - }) - } + type Runtime = CefRuntime; + type WindowBuilder = WindowBuilderWrapper; - fn request_user_attention(&self, request_type: Option) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::RequestUserAttention(request_type), - }) - } + fn run_on_main_thread(&self, f: F) -> Result<()> { + self.context.run_on_main_thread(f) + } - fn create_window) + Send + 'static>( - &mut self, - pending: PendingWindow, - after_window_creation: Option, - ) -> Result> { - create_window_detached(&self.context, pending, after_window_creation) - } - - fn create_webview( - &mut self, - pending: PendingWebview, - ) -> Result> { - create_webview_detached(&self.context, self.window_id, pending) - } - - fn set_resizable(&self, resizable: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetResizable(resizable), - }) - } + fn on_window_event(&self, f: F) -> WindowEventId { + let id = self.context.next_window_event_id(); + let _ = self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::AddEventListener(id, Box::new(f)), + }); + id + } - fn set_enabled(&self, enabled: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetEnabled(enabled), - }) - } + fn scale_factor(&self) -> Result { + window_getter!(self, ScaleFactor) + } - fn set_maximizable(&self, maximizable: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetMaximizable(maximizable), - }) - } + fn inner_position(&self) -> Result> { + window_getter!(self, InnerPosition) + } - fn set_minimizable(&self, minimizable: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetMinimizable(minimizable), - }) - } + fn outer_position(&self) -> Result> { + window_getter!(self, OuterPosition) + } - fn set_closable(&self, closable: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetClosable(closable), - }) - } + fn inner_size(&self) -> Result> { + window_getter!(self, InnerSize) + } - fn set_title>(&self, title: S) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetTitle(title.into()), - }) - } + fn outer_size(&self) -> Result> { + window_getter!(self, OuterSize) + } - fn maximize(&self) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Maximize, - }) - } + fn is_fullscreen(&self) -> Result { + window_getter!(self, IsFullscreen) + } - fn unmaximize(&self) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Unmaximize, - }) - } + fn is_minimized(&self) -> Result { + window_getter!(self, IsMinimized) + } - fn minimize(&self) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Minimize, - }) - } + fn is_maximized(&self) -> Result { + window_getter!(self, IsMaximized) + } - fn unminimize(&self) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Unminimize, - }) - } + fn is_focused(&self) -> Result { + window_getter!(self, IsFocused) + } - fn show(&self) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Show, - }) - } + fn is_decorated(&self) -> Result { + window_getter!(self, IsDecorated) + } - fn hide(&self) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Hide, - }) - } + fn is_resizable(&self) -> Result { + window_getter!(self, IsResizable) + } - fn close(&self) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Close, - }) - } + fn is_maximizable(&self) -> Result { + window_getter!(self, IsMaximizable) + } - fn destroy(&self) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Destroy, - }) - } + fn is_minimizable(&self) -> Result { + window_getter!(self, IsMinimizable) + } - fn set_decorations(&self, decorations: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetDecorations(decorations), - }) - } + fn is_closable(&self) -> Result { + window_getter!(self, IsClosable) + } - fn set_shadow(&self, enable: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetShadow(enable), - }) - } + fn is_visible(&self) -> Result { + window_getter!(self, IsVisible) + } - fn set_always_on_bottom(&self, value: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetAlwaysOnBottom(value), - }) - } + fn is_enabled(&self) -> Result { + window_getter!(self, IsEnabled) + } - fn set_always_on_top(&self, value: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetAlwaysOnTop(value), - }) - } + fn is_always_on_top(&self) -> Result { + window_getter!(self, IsAlwaysOnTop) + } - fn set_visible_on_all_workspaces(&self, value: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetVisibleOnAllWorkspaces(value), - }) - } + fn title(&self) -> Result { + window_getter!(self, Title) + } - fn set_content_protected(&self, protected: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetContentProtected(protected), - }) - } + fn current_monitor(&self) -> Result> { + window_getter!(self, CurrentMonitor) + } - fn set_size(&self, size: Size) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetSize(size), - }) - } + fn primary_monitor(&self) -> Result> { + window_getter!(self, PrimaryMonitor) + } - fn set_min_size(&self, size: Option) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetMinSize(size), - }) - } + fn monitor_from_point(&self, x: f64, y: f64) -> Result> { + let (tx, rx) = mpsc::channel(); + getter( + &self.context, + Message::Window { + window_id: self.window_id, + message: WindowMessage::MonitorFromPoint(tx, x, y), + }, + rx, + ) + } - fn set_max_size(&self, size: Option) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetMaxSize(size), - }) - } + fn available_monitors(&self) -> Result> { + window_getter!(self, AvailableMonitors) + } - fn set_size_constraints(&self, constraints: WindowSizeConstraints) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetSizeConstraints(constraints), - }) - } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn gtk_window(&self) -> Result { + Err(Error::FailedToSendMessage) + } - fn set_position(&self, position: Position) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetPosition(position), - }) - } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn default_vbox(&self) -> Result { + Err(Error::FailedToSendMessage) + } - fn set_fullscreen(&self, fullscreen: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetFullscreen(fullscreen), - }) - } + fn window_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> + { + let handle: Result = window_getter!(self, RawWindowHandle); + let handle = handle.map_err(|_| raw_window_handle::HandleError::Unavailable)?; + Ok(unsafe { raw_window_handle::WindowHandle::borrow_raw(handle.0) }) + } - #[cfg(target_os = "macos")] - fn set_simple_fullscreen(&self, enable: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetSimpleFullscreen(enable), - }) - } + fn theme(&self) -> Result { + window_getter!(self, Theme) + } - fn set_focus(&self) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetFocus, - }) - } + fn center(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Center, + }) + } - fn set_focusable(&self, focusable: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetFocusable(focusable), - }) - } + fn request_user_attention(&self, request_type: Option) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::RequestUserAttention(request_type), + }) + } - fn set_icon(&self, icon: Icon) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetIcon(crate::compat::icon_into_owned(icon)), - }) - } + fn create_window) + Send + 'static>( + &mut self, + pending: PendingWindow, + after_window_creation: Option, + ) -> Result> { + create_window_detached(&self.context, pending, after_window_creation) + } - fn set_skip_taskbar(&self, skip: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetSkipTaskbar(skip), - }) - } + fn create_webview( + &mut self, + pending: PendingWebview, + ) -> Result> { + create_webview_detached(&self.context, self.window_id, pending) + } - fn set_cursor_grab(&self, grab: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetCursorGrab(grab), - }) - } + fn set_resizable(&self, resizable: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetResizable(resizable), + }) + } - fn set_cursor_visible(&self, visible: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetCursorVisible(visible), - }) - } + fn set_enabled(&self, enabled: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetEnabled(enabled), + }) + } - fn set_cursor_icon(&self, icon: CursorIcon) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetCursorIcon(icon), - }) - } + fn set_maximizable(&self, maximizable: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetMaximizable(maximizable), + }) + } - fn set_cursor_position>(&self, position: Pos) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetCursorPosition(position.into()), - }) - } + fn set_minimizable(&self, minimizable: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetMinimizable(minimizable), + }) + } - fn set_ignore_cursor_events(&self, ignore: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetIgnoreCursorEvents(ignore), - }) - } + fn set_closable(&self, closable: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetClosable(closable), + }) + } - fn start_dragging(&self) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::StartDragging, - }) - } + fn set_title>(&self, title: S) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetTitle(title.into()), + }) + } - fn start_resize_dragging(&self, direction: tauri_runtime::ResizeDirection) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::StartResizeDragging(direction), - }) - } + fn maximize(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Maximize, + }) + } - fn set_badge_count(&self, count: Option, desktop_filename: Option) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetBadgeCount(count, desktop_filename), - }) - } + fn unmaximize(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Unmaximize, + }) + } - fn set_badge_label(&self, label: Option) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetBadgeLabel(label), - }) - } + fn minimize(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Minimize, + }) + } - fn set_overlay_icon(&self, icon: Option) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetOverlayIcon(icon.map(crate::compat::icon_into_owned)), - }) - } + fn unminimize(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Unminimize, + }) + } - fn set_progress_bar(&self, progress_state: ProgressBarState) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetProgressBar(progress_state), - }) - } + fn show(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Show, + }) + } - fn set_title_bar_style(&self, style: tauri_utils::TitleBarStyle) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetTitleBarStyle(style), - }) - } + fn hide(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Hide, + }) + } - fn set_traffic_light_position(&self, position: Position) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetTrafficLightPosition(position), - }) - } + fn close(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Close, + }) + } - fn set_theme(&self, theme: Option) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetTheme(theme), - }) - } + fn destroy(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Destroy, + }) + } - fn set_background_color(&self, color: Option) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetBackgroundColor(color), - }) - } + fn set_decorations(&self, decorations: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetDecorations(decorations), + }) + } + + fn set_shadow(&self, enable: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetShadow(enable), + }) + } + + fn set_always_on_bottom(&self, value: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetAlwaysOnBottom(value), + }) + } + + fn set_always_on_top(&self, value: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetAlwaysOnTop(value), + }) + } + + fn set_visible_on_all_workspaces(&self, value: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetVisibleOnAllWorkspaces(value), + }) + } + + fn set_content_protected(&self, protected: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetContentProtected(protected), + }) + } + + fn set_size(&self, size: Size) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetSize(size), + }) + } + + fn set_min_size(&self, size: Option) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetMinSize(size), + }) + } + + fn set_max_size(&self, size: Option) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetMaxSize(size), + }) + } + + fn set_size_constraints(&self, constraints: WindowSizeConstraints) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetSizeConstraints(constraints), + }) + } + + fn set_position(&self, position: Position) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetPosition(position), + }) + } + + fn set_fullscreen(&self, fullscreen: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetFullscreen(fullscreen), + }) + } + + #[cfg(target_os = "macos")] + fn set_simple_fullscreen(&self, enable: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetSimpleFullscreen(enable), + }) + } + + fn set_focus(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetFocus, + }) + } + + fn set_focusable(&self, focusable: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetFocusable(focusable), + }) + } + + fn set_icon(&self, icon: Icon) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetIcon(crate::compat::icon_into_owned(icon)), + }) + } + + fn set_skip_taskbar(&self, skip: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetSkipTaskbar(skip), + }) + } + + fn set_cursor_grab(&self, grab: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetCursorGrab(grab), + }) + } + + fn set_cursor_visible(&self, visible: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetCursorVisible(visible), + }) + } + + fn set_cursor_icon(&self, icon: CursorIcon) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetCursorIcon(icon), + }) + } + + fn set_cursor_position>(&self, position: Pos) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetCursorPosition(position.into()), + }) + } + + fn set_ignore_cursor_events(&self, ignore: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetIgnoreCursorEvents(ignore), + }) + } + + fn start_dragging(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::StartDragging, + }) + } + + fn start_resize_dragging(&self, direction: tauri_runtime::ResizeDirection) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::StartResizeDragging(direction), + }) + } + + fn set_badge_count(&self, count: Option, desktop_filename: Option) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetBadgeCount(count, desktop_filename), + }) + } + + fn set_badge_label(&self, label: Option) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetBadgeLabel(label), + }) + } + + fn set_overlay_icon(&self, icon: Option) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetOverlayIcon(icon.map(crate::compat::icon_into_owned)), + }) + } + + fn set_progress_bar(&self, progress_state: ProgressBarState) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetProgressBar(progress_state), + }) + } + + fn set_title_bar_style(&self, style: tauri_utils::TitleBarStyle) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetTitleBarStyle(style), + }) + } + + fn set_traffic_light_position(&self, position: Position) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetTrafficLightPosition(position), + }) + } + + fn set_theme(&self, theme: Option) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetTheme(theme), + }) + } + + fn set_background_color(&self, color: Option) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetBackgroundColor(color), + }) + } } pub(crate) fn create_window_detached( - context: &RuntimeContext, - pending: PendingWindow>, - after_window_creation: Option, + context: &RuntimeContext, + pending: PendingWindow>, + after_window_creation: Option, ) -> Result>> where - T: UserEvent, - F: Fn(RawWindow<'_>) + Send + 'static, + T: UserEvent, + F: Fn(RawWindow<'_>) + Send + 'static, { - let label = pending.label.clone(); - let window_id = context.next_window_id(); - let (webview_id, use_https_scheme) = pending - .webview - .as_ref() - .map(|w| { - ( - Some(context.next_webview_id()), - w.webview_attributes.use_https_scheme, - ) - }) - .unwrap_or((None, false)); - - let (result_tx, result_rx) = mpsc::channel(); - context.send_message(Message::CreateWindow { - window_id, - webview_id, - pending: Box::new(pending), - after_window_creation: after_window_creation.map(|f| Box::new(f) as _), - result_tx, - })?; - // Block until the event loop has created the window so a creation failure is - // surfaced to the caller instead of leaving a detached, dead window. - result_rx - .recv() - .map_err(|_| Error::FailedToReceiveMessage)??; - - let webview = webview_id.map(|webview_id| DetachedWindowWebview { - webview: DetachedWebview { - label: label.clone(), - dispatcher: CefWebviewDispatcher { - window_id: Arc::new(Mutex::new(window_id)), + let label = pending.label.clone(); + let window_id = context.next_window_id(); + let (webview_id, use_https_scheme) = pending + .webview + .as_ref() + .map(|w| { + ( + Some(context.next_webview_id()), + w.webview_attributes.use_https_scheme, + ) + }) + .unwrap_or((None, false)); + + let (result_tx, result_rx) = mpsc::channel(); + context.send_message(Message::CreateWindow { + window_id, webview_id, - context: context.clone(), - }, - }, - use_https_scheme, - }); - - Ok(DetachedWindow { - id: window_id, - label, - dispatcher: CefWindowDispatcher { - window_id, - context: context.clone(), - }, - webview, - }) + pending: Box::new(pending), + after_window_creation: after_window_creation.map(|f| Box::new(f) as _), + result_tx, + })?; + // Block until the event loop has created the window so a creation failure is + // surfaced to the caller instead of leaving a detached, dead window. + result_rx + .recv() + .map_err(|_| Error::FailedToReceiveMessage)??; + + let webview = webview_id.map(|webview_id| DetachedWindowWebview { + webview: DetachedWebview { + label: label.clone(), + dispatcher: CefWebviewDispatcher { + window_id: Arc::new(Mutex::new(window_id)), + webview_id, + context: context.clone(), + }, + }, + use_https_scheme, + }); + + Ok(DetachedWindow { + id: window_id, + label, + dispatcher: CefWindowDispatcher { + window_id, + context: context.clone(), + }, + webview, + }) } pub(crate) fn winit_monitor_to_tauri_monitor(monitor: &winit::monitor::MonitorHandle) -> Monitor { - Monitor { - name: monitor.name().map(|s| s.to_string()), - scale_factor: monitor.scale_factor(), - position: monitor.position().unwrap_or_default(), - size: monitor - .current_video_mode() - .map(|v| v.size()) - .unwrap_or_default(), - work_area: monitor.work_area(), - } + Monitor { + name: monitor.name().map(|s| s.to_string()), + scale_factor: monitor.scale_factor(), + position: monitor.position().unwrap_or_default(), + size: monitor + .current_video_mode() + .map(|v| v.size()) + .unwrap_or_default(), + work_area: monitor.work_area(), + } } pub(crate) fn tauri_icon_to_winit_icon(icon: Icon) -> Result { - winit::icon::RgbaIcon::new(icon.rgba.into_owned(), icon.width, icon.height) - .map(Into::into) - .map_err(|e| tauri_runtime::Error::InvalidIcon(e.into())) + winit::icon::RgbaIcon::new(icon.rgba.into_owned(), icon.width, icon.height) + .map(Into::into) + .map_err(|e| tauri_runtime::Error::InvalidIcon(e.into())) } fn tauri_cursor_to_winit_cursor(cursor: CursorIcon) -> winit::cursor::CursorIcon { - match cursor { - CursorIcon::Default => winit::cursor::CursorIcon::Default, - CursorIcon::Crosshair => winit::cursor::CursorIcon::Crosshair, - CursorIcon::Hand => winit::cursor::CursorIcon::Grab, - CursorIcon::Arrow => winit::cursor::CursorIcon::Default, - CursorIcon::Move => winit::cursor::CursorIcon::Move, - CursorIcon::Text => winit::cursor::CursorIcon::Text, - CursorIcon::Wait => winit::cursor::CursorIcon::Wait, - CursorIcon::Help => winit::cursor::CursorIcon::Help, - CursorIcon::Progress => winit::cursor::CursorIcon::Progress, - CursorIcon::NotAllowed => winit::cursor::CursorIcon::NotAllowed, - CursorIcon::ContextMenu => winit::cursor::CursorIcon::ContextMenu, - CursorIcon::Cell => winit::cursor::CursorIcon::Cell, - CursorIcon::VerticalText => winit::cursor::CursorIcon::VerticalText, - CursorIcon::Alias => winit::cursor::CursorIcon::Alias, - CursorIcon::Copy => winit::cursor::CursorIcon::Copy, - CursorIcon::NoDrop => winit::cursor::CursorIcon::NoDrop, - CursorIcon::Grab => winit::cursor::CursorIcon::Grab, - CursorIcon::Grabbing => winit::cursor::CursorIcon::Grabbing, - CursorIcon::AllScroll => winit::cursor::CursorIcon::AllScroll, - CursorIcon::ZoomIn => winit::cursor::CursorIcon::ZoomIn, - CursorIcon::ZoomOut => winit::cursor::CursorIcon::ZoomOut, - CursorIcon::EResize => winit::cursor::CursorIcon::EResize, - CursorIcon::NResize => winit::cursor::CursorIcon::NResize, - CursorIcon::NeResize => winit::cursor::CursorIcon::NeResize, - CursorIcon::NwResize => winit::cursor::CursorIcon::NwResize, - CursorIcon::SResize => winit::cursor::CursorIcon::SResize, - CursorIcon::SeResize => winit::cursor::CursorIcon::SeResize, - CursorIcon::SwResize => winit::cursor::CursorIcon::SwResize, - CursorIcon::WResize => winit::cursor::CursorIcon::WResize, - CursorIcon::EwResize => winit::cursor::CursorIcon::EwResize, - CursorIcon::NsResize => winit::cursor::CursorIcon::NsResize, - CursorIcon::NeswResize => winit::cursor::CursorIcon::NeswResize, - CursorIcon::NwseResize => winit::cursor::CursorIcon::NwseResize, - CursorIcon::ColResize => winit::cursor::CursorIcon::ColResize, - CursorIcon::RowResize => winit::cursor::CursorIcon::RowResize, - _ => winit::cursor::CursorIcon::Default, - } + match cursor { + CursorIcon::Default => winit::cursor::CursorIcon::Default, + CursorIcon::Crosshair => winit::cursor::CursorIcon::Crosshair, + CursorIcon::Hand => winit::cursor::CursorIcon::Grab, + CursorIcon::Arrow => winit::cursor::CursorIcon::Default, + CursorIcon::Move => winit::cursor::CursorIcon::Move, + CursorIcon::Text => winit::cursor::CursorIcon::Text, + CursorIcon::Wait => winit::cursor::CursorIcon::Wait, + CursorIcon::Help => winit::cursor::CursorIcon::Help, + CursorIcon::Progress => winit::cursor::CursorIcon::Progress, + CursorIcon::NotAllowed => winit::cursor::CursorIcon::NotAllowed, + CursorIcon::ContextMenu => winit::cursor::CursorIcon::ContextMenu, + CursorIcon::Cell => winit::cursor::CursorIcon::Cell, + CursorIcon::VerticalText => winit::cursor::CursorIcon::VerticalText, + CursorIcon::Alias => winit::cursor::CursorIcon::Alias, + CursorIcon::Copy => winit::cursor::CursorIcon::Copy, + CursorIcon::NoDrop => winit::cursor::CursorIcon::NoDrop, + CursorIcon::Grab => winit::cursor::CursorIcon::Grab, + CursorIcon::Grabbing => winit::cursor::CursorIcon::Grabbing, + CursorIcon::AllScroll => winit::cursor::CursorIcon::AllScroll, + CursorIcon::ZoomIn => winit::cursor::CursorIcon::ZoomIn, + CursorIcon::ZoomOut => winit::cursor::CursorIcon::ZoomOut, + CursorIcon::EResize => winit::cursor::CursorIcon::EResize, + CursorIcon::NResize => winit::cursor::CursorIcon::NResize, + CursorIcon::NeResize => winit::cursor::CursorIcon::NeResize, + CursorIcon::NwResize => winit::cursor::CursorIcon::NwResize, + CursorIcon::SResize => winit::cursor::CursorIcon::SResize, + CursorIcon::SeResize => winit::cursor::CursorIcon::SeResize, + CursorIcon::SwResize => winit::cursor::CursorIcon::SwResize, + CursorIcon::WResize => winit::cursor::CursorIcon::WResize, + CursorIcon::EwResize => winit::cursor::CursorIcon::EwResize, + CursorIcon::NsResize => winit::cursor::CursorIcon::NsResize, + CursorIcon::NeswResize => winit::cursor::CursorIcon::NeswResize, + CursorIcon::NwseResize => winit::cursor::CursorIcon::NwseResize, + CursorIcon::ColResize => winit::cursor::CursorIcon::ColResize, + CursorIcon::RowResize => winit::cursor::CursorIcon::RowResize, + _ => winit::cursor::CursorIcon::Default, + } } diff --git a/src/window_builder.rs b/src/window_builder.rs index 9164cf0..3a2ee83 100644 --- a/src/window_builder.rs +++ b/src/window_builder.rs @@ -3,22 +3,22 @@ // SPDX-License-Identifier: MIT use tauri_runtime::{ - Icon, Result, - dpi::{PhysicalSize, Size}, - window::{WindowBuilder, WindowBuilderBase, WindowSizeConstraints}, + Icon, Result, + dpi::{PhysicalSize, Size}, + window::{WindowBuilder, WindowBuilderBase, WindowSizeConstraints}, }; use tauri_utils::{ - Theme, - config::{Color, PreventOverflowConfig, WindowConfig}, + Theme, + config::{Color, PreventOverflowConfig, WindowConfig}, }; use winit::{ - dpi::{LogicalPosition, LogicalSize}, - monitor::Fullscreen, - window::{WindowAttributes, WindowButtons}, + dpi::{LogicalPosition, LogicalSize}, + monitor::Fullscreen, + window::{WindowAttributes, WindowButtons}, }; use crate::window::{ - AppWindowAttrs, paired_size_constraint, tauri_theme_to_winit_theme, winit_theme_to_tauri_theme, + AppWindowAttrs, paired_size_constraint, tauri_theme_to_winit_theme, winit_theme_to_tauri_theme, }; #[cfg(any(windows, target_os = "macos"))] @@ -38,7 +38,7 @@ use windows::Win32::Foundation::HWND; #[derive(Clone, Default, Debug)] pub struct WindowBuilderWrapper { - pub(crate) attrs: AppWindowAttrs, + pub(crate) attrs: AppWindowAttrs, } unsafe impl Send for WindowBuilderWrapper {} @@ -46,469 +46,467 @@ unsafe impl Send for WindowBuilderWrapper {} impl WindowBuilderBase for WindowBuilderWrapper {} impl WindowBuilder for WindowBuilderWrapper { - fn new() -> Self { - #[allow(unused_mut)] - let mut builder = Self { - attrs: AppWindowAttrs { - inner: WindowAttributes::default() - .with_title("Tauri App") - .with_visible(true), - ..Default::default() - }, + fn new() -> Self { + #[allow(unused_mut)] + let mut builder = Self { + attrs: AppWindowAttrs { + inner: WindowAttributes::default() + .with_title("Tauri App") + .with_visible(true), + ..Default::default() + }, + } + .focused(true); + + #[cfg(windows)] + { + builder = builder.window_classname("Tauri Window"); + } + + builder } - .focused(true); - #[cfg(windows)] - { - builder = builder.window_classname("Tauri Window"); - } - - builder - } - - fn with_config(config: &WindowConfig) -> Self { - let mut builder = Self::new() - .title(config.title.to_string()) - .inner_size(config.width, config.height) - .resizable(config.resizable) - .fullscreen(config.fullscreen) - .focused(config.focus) - .focusable(config.focusable) - .visible(config.visible) - .decorations(config.decorations) - .maximized(config.maximized) - .content_protected(config.content_protected) - .closable(config.closable) - .maximizable(config.maximizable) - .minimizable(config.minimizable) - .skip_taskbar(config.skip_taskbar) - .shadow(config.shadow) - .visible_on_all_workspaces(config.visible_on_all_workspaces) - .theme(config.theme); - if config.always_on_bottom { - builder = builder.always_on_bottom(true); - } else if config.always_on_top { - builder = builder.always_on_top(true); + fn with_config(config: &WindowConfig) -> Self { + let mut builder = Self::new() + .title(config.title.to_string()) + .inner_size(config.width, config.height) + .resizable(config.resizable) + .fullscreen(config.fullscreen) + .focused(config.focus) + .focusable(config.focusable) + .visible(config.visible) + .decorations(config.decorations) + .maximized(config.maximized) + .content_protected(config.content_protected) + .closable(config.closable) + .maximizable(config.maximizable) + .minimizable(config.minimizable) + .skip_taskbar(config.skip_taskbar) + .shadow(config.shadow) + .visible_on_all_workspaces(config.visible_on_all_workspaces) + .theme(config.theme); + if config.always_on_bottom { + builder = builder.always_on_bottom(true); + } else if config.always_on_top { + builder = builder.always_on_top(true); + } + #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] + { + builder = builder.transparent(config.transparent); + } + if let (Some(min_width), Some(min_height)) = (config.min_width, config.min_height) { + builder = builder.min_inner_size(min_width, min_height); + } + if let (Some(max_width), Some(max_height)) = (config.max_width, config.max_height) { + builder = builder.max_inner_size(max_width, max_height); + } + if let Some(color) = config.background_color { + builder = builder.background_color(color); + } + if let (Some(x), Some(y)) = (config.x, config.y) { + builder = builder.position(x, y); + } + #[cfg(target_os = "macos")] + { + builder = builder + .hidden_title(config.hidden_title) + .title_bar_style(config.title_bar_style); + if let Some(position) = &config.traffic_light_position { + builder = + builder.traffic_light_position(LogicalPosition::new(position.x, position.y)); + } + if let Some(identifier) = &config.tabbing_identifier { + builder = builder.tabbing_identifier(identifier); + } + let pl_attrs = (*platform_attrs(&mut builder.attrs.inner)) + .with_accepts_first_mouse(config.accept_first_mouse); + + builder.attrs.inner = builder + .attrs + .inner + .with_platform_attributes(Box::new(pl_attrs)); + } + if config.center { + builder = builder.center(); + } + if let Some(window_classname) = &config.window_classname { + builder = builder.window_classname(window_classname); + } + if let Some(prevent_overflow) = &config.prevent_overflow { + builder = match prevent_overflow { + PreventOverflowConfig::Enable(true) => builder.prevent_overflow(), + PreventOverflowConfig::Margin(margin) => { + let margin = PhysicalSize::new(margin.width, margin.height); + builder.prevent_overflow_with_margin(margin.into()) + } + _ => builder, + }; + } + builder + } + + fn center(mut self) -> Self { + self.attrs.center = true; + self } + + fn position(mut self, x: f64, y: f64) -> Self { + self.attrs.inner = self.attrs.inner.with_position(LogicalPosition::new(x, y)); + self + } + + fn inner_size(mut self, width: f64, height: f64) -> Self { + self.attrs.inner = self + .attrs + .inner + .with_surface_size(LogicalSize::new(width, height)); + self + } + + fn min_inner_size(mut self, min_width: f64, min_height: f64) -> Self { + self.attrs.inner = self + .attrs + .inner + .with_min_surface_size(LogicalSize::new(min_width, min_height)); + self + } + + fn max_inner_size(mut self, max_width: f64, max_height: f64) -> Self { + self.attrs.inner = self + .attrs + .inner + .with_max_surface_size(LogicalSize::new(max_width, max_height)); + self + } + + fn inner_size_constraints(mut self, constraints: WindowSizeConstraints) -> Self { + // TODO: upstream individual width/height size constraints to winit. + self.attrs.inner.min_surface_size = + paired_size_constraint(constraints.min_width, constraints.min_height); + self.attrs.inner.max_surface_size = + paired_size_constraint(constraints.max_width, constraints.max_height); + self + } + + fn prevent_overflow(mut self) -> Self { + self.attrs.prevent_overflow = Some(PhysicalSize::new(0, 0).into()); + self + } + + fn prevent_overflow_with_margin(mut self, margin: Size) -> Self { + self.attrs.prevent_overflow = Some(margin); + self + } + + fn resizable(mut self, resizable: bool) -> Self { + self.attrs.inner = self.attrs.inner.with_resizable(resizable); + self + } + + fn maximizable(mut self, maximizable: bool) -> Self { + self.attrs + .inner + .enabled_buttons + .set(WindowButtons::MAXIMIZE, maximizable); + self + } + + fn minimizable(mut self, minimizable: bool) -> Self { + self.attrs + .inner + .enabled_buttons + .set(WindowButtons::MINIMIZE, minimizable); + self + } + + fn closable(mut self, closable: bool) -> Self { + self.attrs + .inner + .enabled_buttons + .set(WindowButtons::CLOSE, closable); + self + } + + fn title>(mut self, title: S) -> Self { + self.attrs.inner = self.attrs.inner.with_title(title); + self + } + + fn fullscreen(mut self, fullscreen: bool) -> Self { + self.attrs.inner = self + .attrs + .inner + .with_fullscreen(fullscreen.then_some(Fullscreen::Borderless(None))); + self + } + + fn focused(mut self, focused: bool) -> Self { + self.attrs.inner = self.attrs.inner.with_active(focused); + self + } + + fn focusable(self, _focusable: bool) -> Self { + // TODO + self + } + + fn maximized(mut self, maximized: bool) -> Self { + self.attrs.inner = self.attrs.inner.with_maximized(maximized); + self + } + + fn visible(mut self, visible: bool) -> Self { + self.attrs.inner = self.attrs.inner.with_visible(visible); + self + } + #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] - { - builder = builder.transparent(config.transparent); + fn transparent(mut self, transparent: bool) -> Self { + self.attrs.inner = self.attrs.inner.with_transparent(transparent); + self } - if let (Some(min_width), Some(min_height)) = (config.min_width, config.min_height) { - builder = builder.min_inner_size(min_width, min_height); + + fn decorations(mut self, decorations: bool) -> Self { + self.attrs.inner = self.attrs.inner.with_decorations(decorations); + self } - if let (Some(max_width), Some(max_height)) = (config.max_width, config.max_height) { - builder = builder.max_inner_size(max_width, max_height); + + fn always_on_bottom(mut self, always_on_bottom: bool) -> Self { + self.attrs.inner = self.attrs.inner.with_window_level(if always_on_bottom { + winit::window::WindowLevel::AlwaysOnBottom + } else { + winit::window::WindowLevel::Normal + }); + self } - if let Some(color) = config.background_color { - builder = builder.background_color(color); + + fn always_on_top(mut self, always_on_top: bool) -> Self { + self.attrs.inner = self.attrs.inner.with_window_level(if always_on_top { + winit::window::WindowLevel::AlwaysOnTop + } else { + winit::window::WindowLevel::Normal + }); + self } - if let (Some(x), Some(y)) = (config.x, config.y) { - builder = builder.position(x, y); + + #[cfg_attr(windows, allow(unused))] + fn visible_on_all_workspaces(mut self, visible_on_all_workspaces: bool) -> Self { + #[cfg(any( + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + self.attrs.visible_on_all_workspaces = visible_on_all_workspaces; + } + + self } - #[cfg(target_os = "macos")] - { - builder = builder - .hidden_title(config.hidden_title) - .title_bar_style(config.title_bar_style); - if let Some(position) = &config.traffic_light_position { - builder = builder.traffic_light_position(LogicalPosition::new(position.x, position.y)); - } - if let Some(identifier) = &config.tabbing_identifier { - builder = builder.tabbing_identifier(identifier); - } - let pl_attrs = (*platform_attrs(&mut builder.attrs.inner)) - .with_accepts_first_mouse(config.accept_first_mouse); - - builder.attrs.inner = builder - .attrs - .inner - .with_platform_attributes(Box::new(pl_attrs)); - } - if config.center { - builder = builder.center(); - } - if let Some(window_classname) = &config.window_classname { - builder = builder.window_classname(window_classname); - } - if let Some(prevent_overflow) = &config.prevent_overflow { - builder = match prevent_overflow { - PreventOverflowConfig::Enable(true) => builder.prevent_overflow(), - PreventOverflowConfig::Margin(margin) => { - let margin = PhysicalSize::new(margin.width, margin.height); - builder.prevent_overflow_with_margin(margin.into()) + + fn content_protected(mut self, protected: bool) -> Self { + self.attrs.inner = self.attrs.inner.with_content_protected(protected); + self + } + + fn icon(mut self, icon: Icon) -> Result { + let icon = super::window::tauri_icon_to_winit_icon(icon)?; + self.attrs.inner = self.attrs.inner.with_window_icon(Some(icon)); + Ok(self) + } + + #[allow(unused_mut)] + fn skip_taskbar(mut self, skip: bool) -> Self { + #[cfg(windows)] + { + let pl_attrs = platform_attrs(&mut self.attrs.inner).with_skip_taskbar(skip); + self.attrs.inner = self + .attrs + .inner + .with_platform_attributes(Box::new(pl_attrs)); } - _ => builder, - }; - } - builder - } - - fn center(mut self) -> Self { - self.attrs.center = true; - self - } - - fn position(mut self, x: f64, y: f64) -> Self { - self.attrs.inner = self.attrs.inner.with_position(LogicalPosition::new(x, y)); - self - } - - fn inner_size(mut self, width: f64, height: f64) -> Self { - self.attrs.inner = self - .attrs - .inner - .with_surface_size(LogicalSize::new(width, height)); - self - } - - fn min_inner_size(mut self, min_width: f64, min_height: f64) -> Self { - self.attrs.inner = self - .attrs - .inner - .with_min_surface_size(LogicalSize::new(min_width, min_height)); - self - } - - fn max_inner_size(mut self, max_width: f64, max_height: f64) -> Self { - self.attrs.inner = self - .attrs - .inner - .with_max_surface_size(LogicalSize::new(max_width, max_height)); - self - } - - fn inner_size_constraints(mut self, constraints: WindowSizeConstraints) -> Self { - // TODO: upstream individual width/height size constraints to winit. - self.attrs.inner.min_surface_size = - paired_size_constraint(constraints.min_width, constraints.min_height); - self.attrs.inner.max_surface_size = - paired_size_constraint(constraints.max_width, constraints.max_height); - self - } - - fn prevent_overflow(mut self) -> Self { - self.attrs.prevent_overflow = Some(PhysicalSize::new(0, 0).into()); - self - } - - fn prevent_overflow_with_margin(mut self, margin: Size) -> Self { - self.attrs.prevent_overflow = Some(margin); - self - } - - fn resizable(mut self, resizable: bool) -> Self { - self.attrs.inner = self.attrs.inner.with_resizable(resizable); - self - } - - fn maximizable(mut self, maximizable: bool) -> Self { - self - .attrs - .inner - .enabled_buttons - .set(WindowButtons::MAXIMIZE, maximizable); - self - } - - fn minimizable(mut self, minimizable: bool) -> Self { - self - .attrs - .inner - .enabled_buttons - .set(WindowButtons::MINIMIZE, minimizable); - self - } - - fn closable(mut self, closable: bool) -> Self { - self - .attrs - .inner - .enabled_buttons - .set(WindowButtons::CLOSE, closable); - self - } - - fn title>(mut self, title: S) -> Self { - self.attrs.inner = self.attrs.inner.with_title(title); - self - } - - fn fullscreen(mut self, fullscreen: bool) -> Self { - self.attrs.inner = self - .attrs - .inner - .with_fullscreen(fullscreen.then_some(Fullscreen::Borderless(None))); - self - } - - fn focused(mut self, focused: bool) -> Self { - self.attrs.inner = self.attrs.inner.with_active(focused); - self - } - - fn focusable(self, _focusable: bool) -> Self { - // TODO - self - } - - fn maximized(mut self, maximized: bool) -> Self { - self.attrs.inner = self.attrs.inner.with_maximized(maximized); - self - } - - fn visible(mut self, visible: bool) -> Self { - self.attrs.inner = self.attrs.inner.with_visible(visible); - self - } - - #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] - fn transparent(mut self, transparent: bool) -> Self { - self.attrs.inner = self.attrs.inner.with_transparent(transparent); - self - } - - fn decorations(mut self, decorations: bool) -> Self { - self.attrs.inner = self.attrs.inner.with_decorations(decorations); - self - } - - fn always_on_bottom(mut self, always_on_bottom: bool) -> Self { - self.attrs.inner = self.attrs.inner.with_window_level(if always_on_bottom { - winit::window::WindowLevel::AlwaysOnBottom - } else { - winit::window::WindowLevel::Normal - }); - self - } - - fn always_on_top(mut self, always_on_top: bool) -> Self { - self.attrs.inner = self.attrs.inner.with_window_level(if always_on_top { - winit::window::WindowLevel::AlwaysOnTop - } else { - winit::window::WindowLevel::Normal - }); - self - } - - #[cfg_attr(windows, allow(unused))] - fn visible_on_all_workspaces(mut self, visible_on_all_workspaces: bool) -> Self { - #[cfg(any( - target_os = "macos", - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - { - self.attrs.visible_on_all_workspaces = visible_on_all_workspaces; + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + self.attrs.skip_taskbar = skip; + } + + self } - self - } + fn background_color(mut self, color: Color) -> Self { + self.attrs.background_color = Some(color); + self + } - fn content_protected(mut self, protected: bool) -> Self { - self.attrs.inner = self.attrs.inner.with_content_protected(protected); - self - } + #[cfg_attr( + not(any(windows, target_os = "macos")), + allow(unused_mut, unused_variables) + )] + fn shadow(mut self, enable: bool) -> Self { + #[cfg(windows)] + { + let pl_attrs = platform_attrs(&mut self.attrs.inner).with_undecorated_shadow(enable); + self.attrs.inner = self + .attrs + .inner + .with_platform_attributes(Box::new(pl_attrs)); + } - fn icon(mut self, icon: Icon) -> Result { - let icon = super::window::tauri_icon_to_winit_icon(icon)?; - self.attrs.inner = self.attrs.inner.with_window_icon(Some(icon)); - Ok(self) - } + #[cfg(target_os = "macos")] + { + let pl_attrs = platform_attrs(&mut self.attrs.inner).with_has_shadow(enable); + self.attrs.inner = self + .attrs + .inner + .with_platform_attributes(Box::new(pl_attrs)); + } + + self + } + + #[cfg(windows)] + fn owner(mut self, owner: HWND) -> Self { + let pl_attrs = platform_attrs(&mut self.attrs.inner).with_owner_window(owner.0); + self.attrs.inner = self + .attrs + .inner + .with_platform_attributes(Box::new(pl_attrs)); + self + } - #[allow(unused_mut)] - fn skip_taskbar(mut self, skip: bool) -> Self { #[cfg(windows)] - { - let pl_attrs = platform_attrs(&mut self.attrs.inner).with_skip_taskbar(skip); - self.attrs.inner = self - .attrs - .inner - .with_platform_attributes(Box::new(pl_attrs)); + fn parent(mut self, parent: HWND) -> Self { + if let Some(hwnd) = NonZeroIsize::new(parent.0 as isize) { + let handle = + RawWindowHandle::Win32(winit::raw_window_handle::Win32WindowHandle::new(hwnd)); + // SAFETY: Tauri passes a live parent HWND owned by the application. + self.attrs.inner = unsafe { self.attrs.inner.with_parent_window(Some(handle)) }; + } + self + } + + #[cfg(windows)] + fn drag_and_drop(mut self, enabled: bool) -> Self { + let pl_attrs = platform_attrs(&mut self.attrs.inner).with_drag_and_drop(enabled); + self.attrs.inner = self + .attrs + .inner + .with_platform_attributes(Box::new(pl_attrs)); + self + } + + #[cfg(target_os = "macos")] + fn parent(mut self, parent: *mut std::ffi::c_void) -> Self { + if let Some(ns_view) = NonNull::new(parent) { + let handle = + RawWindowHandle::AppKit(winit::raw_window_handle::AppKitWindowHandle::new(ns_view)); + // SAFETY: Tauri passes a live parent NSView owned by the application. + self.attrs.inner = unsafe { self.attrs.inner.with_parent_window(Some(handle)) }; + } + self } #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" ))] - { - self.attrs.skip_taskbar = skip; + fn transient_for(self, _parent: &impl gtk::glib::IsA) -> Self { + self } - self - } + #[cfg(target_os = "macos")] + fn title_bar_style(mut self, style: TitleBarStyle) -> Self { + let pl_attrs = *platform_attrs(&mut self.attrs.inner); + let pl_attrs = match style { + TitleBarStyle::Visible => pl_attrs + .with_titlebar_transparent(false) + .with_fullsize_content_view(false), + TitleBarStyle::Transparent => pl_attrs + .with_titlebar_transparent(true) + .with_fullsize_content_view(false), + TitleBarStyle::Overlay => pl_attrs + .with_titlebar_transparent(true) + .with_fullsize_content_view(true), + _ => pl_attrs, + }; + self.attrs.inner = self + .attrs + .inner + .with_platform_attributes(Box::new(pl_attrs)); + self + } - fn background_color(mut self, color: Color) -> Self { - self.attrs.background_color = Some(color); - self - } + #[cfg(target_os = "macos")] + fn traffic_light_position>(mut self, position: P) -> Self { + self.attrs.traffic_light_position = Some(position.into()); + self + } - #[cfg_attr( - not(any(windows, target_os = "macos")), - allow(unused_mut, unused_variables) - )] - fn shadow(mut self, enable: bool) -> Self { - #[cfg(windows)] - { - let pl_attrs = platform_attrs(&mut self.attrs.inner).with_undecorated_shadow(enable); - self.attrs.inner = self - .attrs - .inner - .with_platform_attributes(Box::new(pl_attrs)); + #[cfg(target_os = "macos")] + fn hidden_title(mut self, hidden: bool) -> Self { + let pl_attrs = platform_attrs(&mut self.attrs.inner).with_title_hidden(hidden); + self.attrs.inner = self + .attrs + .inner + .with_platform_attributes(Box::new(pl_attrs)); + self } #[cfg(target_os = "macos")] - { - let pl_attrs = platform_attrs(&mut self.attrs.inner).with_has_shadow(enable); - self.attrs.inner = self - .attrs - .inner - .with_platform_attributes(Box::new(pl_attrs)); - } - - self - } - - #[cfg(windows)] - fn owner(mut self, owner: HWND) -> Self { - let pl_attrs = platform_attrs(&mut self.attrs.inner).with_owner_window(owner.0); - self.attrs.inner = self - .attrs - .inner - .with_platform_attributes(Box::new(pl_attrs)); - self - } - - #[cfg(windows)] - fn parent(mut self, parent: HWND) -> Self { - if let Some(hwnd) = NonZeroIsize::new(parent.0 as isize) { - let handle = RawWindowHandle::Win32(winit::raw_window_handle::Win32WindowHandle::new(hwnd)); - // SAFETY: Tauri passes a live parent HWND owned by the application. - self.attrs.inner = unsafe { self.attrs.inner.with_parent_window(Some(handle)) }; - } - self - } - - #[cfg(windows)] - fn drag_and_drop(mut self, enabled: bool) -> Self { - let pl_attrs = platform_attrs(&mut self.attrs.inner).with_drag_and_drop(enabled); - self.attrs.inner = self - .attrs - .inner - .with_platform_attributes(Box::new(pl_attrs)); - self - } - - #[cfg(target_os = "macos")] - fn parent(mut self, parent: *mut std::ffi::c_void) -> Self { - if let Some(ns_view) = NonNull::new(parent) { - let handle = - RawWindowHandle::AppKit(winit::raw_window_handle::AppKitWindowHandle::new(ns_view)); - // SAFETY: Tauri passes a live parent NSView owned by the application. - self.attrs.inner = unsafe { self.attrs.inner.with_parent_window(Some(handle)) }; - } - self - } - - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - fn transient_for(self, _parent: &impl gtk::glib::IsA) -> Self { - self - } - - #[cfg(target_os = "macos")] - fn title_bar_style(mut self, style: TitleBarStyle) -> Self { - let pl_attrs = *platform_attrs(&mut self.attrs.inner); - let pl_attrs = match style { - TitleBarStyle::Visible => pl_attrs - .with_titlebar_transparent(false) - .with_fullsize_content_view(false), - TitleBarStyle::Transparent => pl_attrs - .with_titlebar_transparent(true) - .with_fullsize_content_view(false), - TitleBarStyle::Overlay => pl_attrs - .with_titlebar_transparent(true) - .with_fullsize_content_view(true), - _ => pl_attrs, - }; - self.attrs.inner = self - .attrs - .inner - .with_platform_attributes(Box::new(pl_attrs)); - self - } - - #[cfg(target_os = "macos")] - fn traffic_light_position>(mut self, position: P) -> Self { - self.attrs.traffic_light_position = Some(position.into()); - self - } - - #[cfg(target_os = "macos")] - fn hidden_title(mut self, hidden: bool) -> Self { - let pl_attrs = platform_attrs(&mut self.attrs.inner).with_title_hidden(hidden); - self.attrs.inner = self - .attrs - .inner - .with_platform_attributes(Box::new(pl_attrs)); - self - } - - #[cfg(target_os = "macos")] - fn tabbing_identifier(mut self, identifier: &str) -> Self { - let pl_attrs = platform_attrs(&mut self.attrs.inner).with_tabbing_identifier(identifier); - self.attrs.inner = self - .attrs - .inner - .with_platform_attributes(Box::new(pl_attrs)); - self - } - - fn theme(mut self, theme: Option) -> Self { - self.attrs.inner = self - .attrs - .inner - .with_theme(tauri_theme_to_winit_theme(theme)); - self - } - - #[allow(unused_mut)] - fn window_classname>(mut self, _window_classname: S) -> Self { - #[cfg(windows)] - { - let pl_attrs = - platform_attrs(&mut self.attrs.inner).with_class_name(_window_classname.into()); - self.attrs.inner = self - .attrs - .inner - .with_platform_attributes(Box::new(pl_attrs)); - } - - self - } - - fn has_icon(&self) -> bool { - self.attrs.inner.window_icon.is_some() - } - - fn get_theme(&self) -> Option { - self - .attrs - .inner - .preferred_theme - .map(winit_theme_to_tauri_theme) - } + fn tabbing_identifier(mut self, identifier: &str) -> Self { + let pl_attrs = platform_attrs(&mut self.attrs.inner).with_tabbing_identifier(identifier); + self.attrs.inner = self + .attrs + .inner + .with_platform_attributes(Box::new(pl_attrs)); + self + } + + fn theme(mut self, theme: Option) -> Self { + self.attrs.inner = self + .attrs + .inner + .with_theme(tauri_theme_to_winit_theme(theme)); + self + } + + #[allow(unused_mut)] + fn window_classname>(mut self, _window_classname: S) -> Self { + #[cfg(windows)] + { + let pl_attrs = + platform_attrs(&mut self.attrs.inner).with_class_name(_window_classname.into()); + self.attrs.inner = self + .attrs + .inner + .with_platform_attributes(Box::new(pl_attrs)); + } + + self + } + + fn has_icon(&self) -> bool { + self.attrs.inner.window_icon.is_some() + } + + fn get_theme(&self) -> Option { + self.attrs + .inner + .preferred_theme + .map(winit_theme_to_tauri_theme) + } } #[cfg(windows)] @@ -518,9 +516,9 @@ type PlatformAttributes = winit::platform::macos::WindowAttributesMacOS; #[cfg(any(windows, target_os = "macos"))] fn platform_attrs(attrs: &mut WindowAttributes) -> Box { - attrs - .platform - .take() - .and_then(|attrs| attrs.cast::().ok()) - .unwrap_or_default() + attrs + .platform + .take() + .and_then(|attrs| attrs.cast::().ok()) + .unwrap_or_default() } diff --git a/src/window_handle.rs b/src/window_handle.rs index 3d0ae80..6edff6a 100644 --- a/src/window_handle.rs +++ b/src/window_handle.rs @@ -16,34 +16,36 @@ unsafe impl Send for SendRawDisplayHandle {} #[cfg(windows)] #[derive(Clone, Copy, Debug)] pub(crate) struct SoftbufferWindowHandle { - display: RawDisplayHandle, - window: RawWindowHandle, + display: RawDisplayHandle, + window: RawWindowHandle, } #[cfg(windows)] impl SoftbufferWindowHandle { - pub(crate) fn new(window: &dyn WinitWindow) -> Option { - Some(Self { - display: window.display_handle().ok()?.as_raw(), - window: window.window_handle().ok()?.as_raw(), - }) - } + pub(crate) fn new(window: &dyn WinitWindow) -> Option { + Some(Self { + display: window.display_handle().ok()?.as_raw(), + window: window.window_handle().ok()?.as_raw(), + }) + } } #[cfg(windows)] impl HasDisplayHandle for SoftbufferWindowHandle { - fn display_handle( - &self, - ) -> std::result::Result, raw_window_handle::HandleError> { - Ok(unsafe { raw_window_handle::DisplayHandle::borrow_raw(self.display) }) - } + fn display_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> + { + Ok(unsafe { raw_window_handle::DisplayHandle::borrow_raw(self.display) }) + } } #[cfg(windows)] impl HasWindowHandle for SoftbufferWindowHandle { - fn window_handle( - &self, - ) -> std::result::Result, raw_window_handle::HandleError> { - Ok(unsafe { raw_window_handle::WindowHandle::borrow_raw(self.window) }) - } + fn window_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> + { + Ok(unsafe { raw_window_handle::WindowHandle::borrow_raw(self.window) }) + } }