From 124e71db80a577e1abac9abceecfe6bbc13d27b1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 07:02:12 +0000 Subject: [PATCH] chore: sync from tauri-apps/tauri feat/cef --- Cargo.toml | 27 +- src/cef_impl/client/display.rs | 4 +- src/cef_impl/client/download.rs | 2 +- src/cef_impl/client/drag.rs | 142 +- src/cef_impl/client/life_span.rs | 116 +- src/cef_impl/client/load.rs | 2 +- src/cef_impl/client/mod.rs | 54 +- src/cef_impl/client/permission.rs | 81 +- src/cef_impl/client/process.rs | 4 +- src/cef_impl/cookie.rs | 145 +- src/cef_impl/ipc.rs | 167 +- src/cef_impl/request_context.rs | 384 ++-- src/cef_impl/request_handler.rs | 627 +++---- src/external_message_pump/linux.rs | 400 ++-- src/external_message_pump/macos.rs | 119 +- src/external_message_pump/mod.rs | 263 +-- src/external_message_pump/windows.rs | 234 +-- src/lib.rs | 13 - src/platform/linux/event_loop.rs | 70 +- src/platform/linux/monitor.rs | 200 +- src/platform/linux/taskbar.rs | 267 ++- src/platform/linux/utils.rs | 138 +- src/platform/linux/webview.rs | 231 ++- src/platform/linux/window.rs | 106 +- src/platform/macos/appkit_state.rs | 26 +- src/platform/macos/application.rs | 125 +- src/platform/macos/dock.rs | 124 +- src/platform/macos/event_loop.rs | 166 +- src/platform/macos/monitor.rs | 36 +- src/platform/macos/progress.rs | 126 +- src/platform/macos/utils.rs | 90 +- src/platform/macos/webview.rs | 172 +- src/platform/macos/window.rs | 385 ++-- src/platform/mod.rs | 58 +- src/platform/windows/event_loop.rs | 22 +- src/platform/windows/icon.rs | 46 +- src/platform/windows/monitor.rs | 34 +- src/platform/windows/webview.rs | 206 +- src/platform/windows/window.rs | 269 ++- src/runtime.rs | 2601 +++++++++++++------------- src/webview.rs | 2398 ++++++++++++------------ src/window.rs | 2517 +++++++++++++------------ src/window_builder.rs | 922 ++++----- src/window_handle.rs | 38 +- 44 files changed, 7002 insertions(+), 7155 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1fccfbf..33eb526 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,21 +1,19 @@ [package] name = "tauri-runtime-cef" version = "0.1.0" -description = "CEF runtime for Tauri, ported from tauri feat/cef branch onto published crates." -authors = ["Tauri Programme within The Commons Conservancy", "byeongsu-hong"] -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" +authors.workspace = true +homepage.workspace = true +repository.workspace = true +categories.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true [dependencies] base64 = "0.22" +cef = { version = "=150.0.0", features = ["build-util", "linux-x11"] } # Not actually used directly, just locking it. -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 } +cef-dll-sys = { version = "=150.0.0", default-features = false } dirs = "6" dioxus-debug-cell = "0.1" http = "1" @@ -26,8 +24,8 @@ raw-window-handle = "0.6" serde = { version = "1", features = ["derive"] } serde_json = "1" sha2 = "0.10" -tauri-runtime = "2.11.2" -tauri-utils = { version = "2.9.2", features = [ +tauri-runtime = { version = "2.11.2", path = "../tauri-runtime" } +tauri-utils = { version = "2.9.2", path = "../tauri-utils", features = [ "html-manipulation", ] } url = "2" @@ -98,6 +96,3 @@ 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/src/cef_impl/client/display.rs b/src/cef_impl/client/display.rs index 0e4aa7c..d80eecf 100644 --- a/src/cef_impl/client/display.rs +++ b/src/cef_impl/client/display.rs @@ -10,8 +10,8 @@ use crate::webview::INITIAL_LOAD_URL; wrap_display_handler! { pub struct TauriCefDisplayHandler { - document_title_changed_handler: Option>, - address_changed_handler: Option>, + document_title_changed_handler: Option>, + address_changed_handler: Option>, } impl DisplayHandler { diff --git a/src/cef_impl/client/download.rs b/src/cef_impl/client/download.rs index 2275533..b6f9489 100644 --- a/src/cef_impl/client/download.rs +++ b/src/cef_impl/client/download.rs @@ -8,7 +8,7 @@ use cef::*; wrap_download_handler! { pub struct TauriCefDownloadHandler { - download_handler: Arc, + download_handler: Arc, } impl DownloadHandler { diff --git a/src/cef_impl/client/drag.rs b/src/cef_impl/client/drag.rs index af7713a..fd493a2 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; - } + 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 { - return 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, + 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, + } } wrap_resource_request_handler! { diff --git a/src/cef_impl/client/life_span.rs b/src/cef_impl/client/life_span.rs index 007e85d..dfae5fa 100644 --- a/src/cef_impl/client/life_span.rs +++ b/src/cef_impl/client/life_span.rs @@ -5,41 +5,45 @@ use std::sync::{Arc, mpsc::Sender}; use cef::*; -use tauri_runtime::{UserEvent, window::WindowId}; +use tauri_runtime::{ + UserEvent, + dpi::{LogicalPosition, LogicalSize}, + window::WindowId, +}; use winit::event_loop::EventLoopProxy as WinitEventLoopProxy; -use crate::runtime::{Message, RuntimeContext}; +use crate::runtime::{CefRuntime, Message, NewWindowOpener, RuntimeContext}; // There is some race condition on CEF that causes the app loading to fail // when there is a network service crash: // "[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; - } - } - std::thread::sleep(check_interval); + 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); + } + }); } wrap_life_span_handler! { @@ -48,9 +52,8 @@ wrap_life_span_handler! { proxy: WinitEventLoopProxy, window_id: WindowId, webview_id: u32, - webview_label: String, context: RuntimeContext, - new_window_handler: Option>, + new_window_handler: Option>>>, initial_url: Option, } @@ -72,41 +75,60 @@ wrap_life_span_handler! { _target_frame_name: Option<&CefString>, _target_disposition: WindowOpenDisposition, _user_gesture: std::os::raw::c_int, - _popup_features: Option<&PopupFeatures>, + popup_features: Option<&PopupFeatures>, _window_info: Option<&mut WindowInfo>, _client: Option<&mut Option>, _settings: Option<&mut BrowserSettings>, _extra_info: Option<&mut Option>, _no_javascript_access: Option<&mut i32>, ) -> std::os::raw::c_int { - // Return value: 0 = allow the popup, 1 = cancel it. - // A crate-level popup policy (set_popup_policy) decides per URL/label - // when installed. - let url = target_url.map(|u| u.to_string()).unwrap_or_default(); - if let Some(allow) = crate::policy::popup_allowed(&crate::policy::PopupRequest { - webview_label: &self.webview_label, - url: &url, - }) { - return i32::from(!allow); + let Some(handler) = &self.new_window_handler else { + return 0; + }; + + let Some(target_url) = target_url else { + return 1; + }; + + let url_str = target_url.to_string(); + let Ok(url) = url::Url::parse(&url_str) else { + return 1; + }; + + // window.open() features are CSS pixels, which map to Tauri's logical units. + let size = popup_features.and_then(|features| { + (features.width_set != 0 && features.height_set != 0) + .then(|| LogicalSize::new(features.width as f64, features.height as f64)) + }); + let position = popup_features.and_then(|features| { + (features.x_set != 0 && features.y_set != 0) + .then(|| LogicalPosition::new(features.x as f64, features.y as f64)) + }); + let features = + tauri_runtime::webview::NewWindowFeatures::new(size, position, NewWindowOpener {}); + + match handler(url, features) { + tauri_runtime::webview::NewWindowResponse::Allow => 0, + tauri_runtime::webview::NewWindowResponse::Create { window_id } => { + // CEF cannot transplant a popup's contents into an existing + // browser, so cancel the popup and navigate the designated + // window's first webview to the URL instead — the closest + // equivalent of wry hosting the popup in that window's webview. + // Note `window.opener` is not linked to the new document. + let _ = self.context.send_message(Message::NavigateFirstWebview { + window_id, + url: url_str, + }); + 1 + } + tauri_runtime::webview::NewWindowResponse::Deny => 1, } - // ponytail: published tauri's new-window handler cannot be invoked from - // CEF — its NewWindowFeatures wraps a wry platform webview handle - // (webkit2gtk::WebView on Linux) that a CEF browser cannot construct. - // An installed handler therefore degrades to a popup deny (the - // verdict every current caller returns); no handler keeps CEF's native - // popup behavior. Revisit when upstream releases feat/cef's - // runtime-generic opener. - i32::from(self.new_window_handler.is_some()) } fn on_before_close(&self, browser: Option<&mut Browser>) { if browser.is_none() { return; } - // Any permission prompt still open over this webview can no longer be - // granted to — deny it rather than leave the callback (and the app's - // consent UI) hanging over a dead browser. - crate::policy::cancel_pending(&self.webview_label); let _ = self .sender .send(Message::BrowserClosed(self.window_id, self.webview_id)); diff --git a/src/cef_impl/client/load.rs b/src/cef_impl/client/load.rs index 54d56df..33e53c2 100644 --- a/src/cef_impl/client/load.rs +++ b/src/cef_impl/client/load.rs @@ -8,7 +8,7 @@ use cef::*; wrap_load_handler! { pub struct TauriCefLoadHandler { - on_page_load_handler: Option>, + on_page_load_handler: Option>, } impl LoadHandler { diff --git a/src/cef_impl/client/mod.rs b/src/cef_impl/client/mod.rs index ecbb967..6ddfeb9 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::{CefRuntime, 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,32 +38,31 @@ 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! { @@ -108,7 +107,6 @@ wrap_client! { self.proxy.clone(), self.window_id, self.webview_id, - self.label.clone(), self.context.clone(), self.handlers.new_window_handler.clone(), self.initial_url.clone(), @@ -145,7 +143,7 @@ wrap_client! { } fn permission_handler(&self) -> Option { - Some(TauriCefPermissionHandler::new(self.label.clone())) + Some(TauriCefPermissionHandler::new()) } fn on_process_message_received( diff --git a/src/cef_impl/client/permission.rs b/src/cef_impl/client/permission.rs index 5f0c018..74b436d 100644 --- a/src/cef_impl/client/permission.rs +++ b/src/cef_impl/client/permission.rs @@ -2,90 +2,51 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -//! Adapter from CEF's permission callbacks to the runtime-neutral policy in -//! [`crate::policy`]. -//! -//! Both handlers hand the policy an owned [`PermissionResponder`] holding a -//! reference-counted clone of the CEF callback, so a policy may answer now or -//! later (a native consent prompt) without the callback dying underneath it. -//! Every path — including a policy that panics its way out or drops the -//! responder — completes the callback exactly once, and only an explicit -//! verdict completes it with a grant. - -use cef::{rc::Rc as _, *}; - -use crate::policy::{self, RequestSource}; +use cef::*; wrap_permission_handler! { - pub struct TauriCefPermissionHandler { - webview_label: String, - } + pub struct TauriCefPermissionHandler {} impl PermissionHandler { fn on_request_media_access_permission( &self, _browser: Option<&mut Browser>, - frame: Option<&mut Frame>, - requesting_origin: Option<&CefString>, + _frame: Option<&mut Frame>, + _requesting_origin: Option<&CefString>, requested_permissions: u32, callback: Option<&mut MediaAccessCallback>, ) -> ::std::os::raw::c_int { let Some(callback) = callback else { return 0; }; - // Reference-counted clone: the callback outlives this stack frame when - // the policy defers to a prompt. - let callback = callback.clone(); - let origin = requesting_origin.map(|origin| origin.to_string()).unwrap_or_default(); - let is_main_frame = frame.map(|frame| frame.is_main() != 0); - policy::dispatch( - &self.webview_label, - &origin, - RequestSource::MediaAccess, - policy::media_kinds(requested_permissions), - is_main_frame, - move |granted| { - // getUserMedia requires the granted mask to equal the requested one - // (cef_media_access_callback_t::cont), so this is all or nothing. - callback.cont(if granted { - requested_permissions - } else { - cef::sys::cef_media_access_permission_types_t::CEF_MEDIA_PERMISSION_NONE as u32 - }); - }, - ); - 1 + // Allow microphone and camera when requested. + let allowed = requested_permissions + & (cef::sys::cef_media_access_permission_types_t::CEF_MEDIA_PERMISSION_DEVICE_AUDIO_CAPTURE + as u32 + | cef::sys::cef_media_access_permission_types_t::CEF_MEDIA_PERMISSION_DEVICE_VIDEO_CAPTURE + as u32); + if allowed != 0 { + callback.cont(requested_permissions); + return 1; + } + 0 } fn on_show_permission_prompt( &self, _browser: Option<&mut Browser>, _prompt_id: u64, - requesting_origin: Option<&CefString>, - requested_permissions: u32, + _requesting_origin: Option<&CefString>, + _requested_permissions: u32, callback: Option<&mut PermissionPromptCallback>, ) -> ::std::os::raw::c_int { let Some(callback) = callback else { return 0; }; - let callback = callback.clone(); - let origin = requesting_origin.map(|origin| origin.to_string()).unwrap_or_default(); - policy::dispatch( - &self.webview_label, - &origin, - RequestSource::Prompt, - policy::prompt_kinds(requested_permissions), - // CEF reports no frame for permission prompts — they are browser-scoped. - None, - move |granted| { - let result = if granted { - cef::sys::cef_permission_request_result_t::CEF_PERMISSION_RESULT_ACCEPT - } else { - cef::sys::cef_permission_request_result_t::CEF_PERMISSION_RESULT_DENY - }; - callback.cont(PermissionRequestResult::from(result)); - }, - ); + // Allow permission prompt (e.g. microphone/camera). + callback.cont(PermissionRequestResult::from( + cef::sys::cef_permission_request_result_t::CEF_PERMISSION_RESULT_ACCEPT, + )); 1 } } diff --git a/src/cef_impl/client/process.rs b/src/cef_impl/client/process.rs index 92a2f2c..95cf9f3 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::*; diff --git a/src/cef_impl/cookie.rs b/src/cef_impl/cookie.rs index 5509d07..79bfcde 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,58 +111,59 @@ 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 e166d19..3a477a6 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,47 @@ 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(), - }, - }; - // 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 + 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) { + handler( + 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(), + }, + }, + request, + ); + } + 1 } diff --git a/src/cef_impl/request_context.rs b/src/cef_impl/request_context.rs index 62c41e4..3a15c38 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(); - } 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); + 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 a relative path with parent components, we will derive a profile hash from it" - ); + log::warn!( + "data directory is not a child of the global cache path, 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() + } 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" ); - 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,138 +258,116 @@ 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, + ..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 { + 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(), + )), + ); } + } - request_context + request_context } diff --git a/src/cef_impl/request_handler.rs b/src/cef_impl/request_handler.rs index 9d26dc6..f667f82 100644 --- a/src/cef_impl/request_handler.rs +++ b/src/cef_impl/request_handler.rs @@ -3,119 +3,107 @@ // 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_runtime::{ + UserEvent, + webview::{NavigationHandler, UriSchemeProtocolHandler}, + window::WindowId, +}; 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, + webview::{CefInitScript, INITIAL_LOAD_URL}, }; type HttpResponse = Arc>>>>>; - -/// The pull side of a streaming custom-scheme response, installed by -/// `process_request` when the request's scheme has a streaming handler. `head` -/// is the shared slot the handler's `StreamResponder` publishes status + -/// headers into; `body` is drained by `read()`. Wrapped in `Arc` (not -/// the buffered path's `RefCell`) because the producer thread's wake closure -/// re-enters `body` to deliver chunks asynchronously. -type StreamCell = Arc>>; - -struct StreamState { - 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 + }; + + 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)) + Some(serialize_node(&document)) } wrap_request_handler! { @@ -212,55 +200,6 @@ wrap_request_handler! { } } -/// Copy an `http` head onto CEF's `Response`, set `Cache-Control: no-store`, -/// derive the MIME from `Content-Type`, and mark the body length unknown -/// (`-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.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_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())); - - if let Some(length) = response_length { - *length = -1; - } - - if let Some(redirect_url) = redirect_url { - let _ = std::mem::take(redirect_url); - } -} - wrap_resource_handler! { pub struct WebResourceHandler { webview_label: String, @@ -276,9 +215,6 @@ wrap_resource_handler! { initiator_origin: Option, // we clone response to send it to the handler thread response: HttpResponse, - // Set only when this request's scheme has a registered streaming handler; - // `response` stays empty in that case and the two paths never mix. - stream: StreamCell, } impl ResourceHandler { @@ -293,50 +229,70 @@ wrap_resource_handler! { let url = CefString::from(&request.url()).to_string(); let url = Url::parse(&url).ok(); - let Some(url) = url else { return 0 }; - let scheme = url.scheme().to_string(); - - // Extraction shared by both paths — reads `request` before it is dropped. - let label = self.webview_label.clone(); - let data = read_request_body(request); - let mut headers = get_request_headers(request); - - // The renderer can issue an IPC request before its execution context is - // fully wired to the loader; in that window Chromium sends the request - // with `Origin: null` even though the document already has a real - // origin. Repair that from the initiating main frame's URL, which the - // browser process tracks reliably. - // - // ONLY a literal `null` is repaired — an ABSENT `Origin` is left absent. - // Absence is meaningful: a top-level navigation and a same-origin GET - // carry no `Origin` by design, and inventing one turns a navigation into - // what looks like a cross-origin call from the *previous* page — which a - // server's origin check then rightly refuses. A correct renderer-sent - // origin always wins. - if let Some(initiator_origin) = &self.initiator_origin - && headers - .get(ORIGIN) - .is_some_and(|value| value.as_bytes() == b"null") - && let Ok(value) = HeaderValue::from_str(initiator_origin) - { - headers.insert(ORIGIN, value); - } + if let Some(url) = url { + let callback = ThreadSafe(callback.clone()); + let response_store = ThreadSafe(self.response.clone()); + let initialization_scripts = self.initialization_scripts.clone(); + let responder = Box::new(move |response: http::Response>| { + let is_html = response + .headers() + .get(CONTENT_TYPE) + .and_then(|ct| ct.to_str().ok()) + .map(|ct| ct.to_lowercase().starts_with("text/html")) + .unwrap_or(false); + + let (parts, body) = response.into_parts(); + let body_bytes = body.into_owned(); + let body_bytes = if is_html { + inject_scripts_into_html_body(&body_bytes, &initialization_scripts) + .unwrap_or(body_bytes) + } else { + body_bytes + }; + + let mut response = http::Response::from_parts(parts, Cursor::new(body_bytes)); + + if let Some(csp) = response.headers_mut().get_mut(CONTENT_SECURITY_POLICY) { + let csp_string = csp.to_str().unwrap_or_default().to_string(); + let new_csp = + csp_inject_initialization_scripts_hashes(csp_string, &initialization_scripts); + if let Ok(new_csp) = HeaderValue::from_str(&new_csp) { + *csp = new_csp; + } + } + + response_store.into_owned().borrow_mut().replace(response); + + let callback = callback.into_owned(); + callback.cont(); + }); + + let label = self.webview_label.clone(); + let handler = self.handler.clone(); + + let data = read_request_body(request); + let mut headers = get_request_headers(request); + + // The renderer can issue an IPC request before its execution context is + // fully wired to the loader; in that window Chromium sends the request + // with `Origin: null` even though the document already has a real + // origin. Repair it from the initiating main frame's URL, which the + // browser process tracks reliably. Only done when the renderer sent no + // origin or a literal `null`, so a correct renderer-sent origin always + // wins. + if let Some(initiator_origin) = &self.initiator_origin { + let origin_missing_or_null = headers + .get(ORIGIN) + .map(|value| value.as_bytes() == b"null") + .unwrap_or(true); + if origin_missing_or_null && let Ok(value) = HeaderValue::from_str(initiator_origin) { + headers.insert(ORIGIN, value); + } + } + + let method_str = CefString::from(&request.method()).to_string(); + let method = http::Method::from_bytes(method_str.as_bytes()).unwrap_or(http::Method::GET); - let method_str = CefString::from(&request.method()).to_string(); - let method = http::Method::from_bytes(method_str.as_bytes()).unwrap_or(http::Method::GET); - - // Streaming path: the scheme registered a streaming handler. The handler - // publishes the head (which fires `callback.cont()`), then writes body - // chunks that `read()` drains. Init-script HTML injection is intentionally - // skipped — streaming bodies are never buffered or parsed. - if let Some(stream_handler) = streaming::streaming_handler_for(&scheme) { - let on_head = ThreadSafe(callback.clone()); - let (responder, head, body) = streaming::make_stream(Box::new(move || { - on_head.into_owned().cont(); - })); - *self.stream.lock().expect("stream slot poisoned") = Some(StreamState { head, body }); - - let initiator = InitiatorOrigin(self.initiator_origin.clone()); std::thread::spawn(move || { let mut http_request = http::Request::builder() .method(method) @@ -344,136 +300,25 @@ wrap_resource_handler! { .body(data) .unwrap(); *http_request.headers_mut() = headers; - http_request.extensions_mut().insert(initiator); - stream_handler(&label, http_request, responder); + // handler is Arc>, so we need to dereference to call it + (**handler)(&label, http_request, responder); }); - return 1; + 1 + } else { + 0 } - - // Buffered path: tauri's uri-scheme protocol handler produces one whole - // response, which `read()` streams out of a `Cursor`. - let callback = ThreadSafe(callback.clone()); - let response_store = ThreadSafe(self.response.clone()); - let initialization_scripts = self.initialization_scripts.clone(); - let responder = Box::new(move |response: http::Response>| { - let is_html = response - .headers() - .get(CONTENT_TYPE) - .and_then(|ct| ct.to_str().ok()) - .map(|ct| ct.to_lowercase().starts_with("text/html")) - .unwrap_or(false); - - let (parts, body) = response.into_parts(); - let body_bytes = body.into_owned(); - let body_bytes = if is_html { - inject_scripts_into_html_body(&body_bytes, &initialization_scripts).unwrap_or(body_bytes) - } else { - body_bytes - }; - - let mut response = http::Response::from_parts(parts, Cursor::new(body_bytes)); - - if let Some(csp) = response.headers_mut().get_mut(CONTENT_SECURITY_POLICY) { - let csp_string = csp.to_str().unwrap_or_default().to_string(); - let new_csp = - csp_inject_initialization_scripts_hashes(csp_string, &initialization_scripts); - if let Ok(new_csp) = HeaderValue::from_str(&new_csp) { - *csp = new_csp; - } - } - - response_store.into_owned().borrow_mut().replace(response); - - let callback = callback.into_owned(); - callback.cont(); - }); - - let handler = self.handler.clone(); - std::thread::spawn(move || { - let mut http_request = http::Request::builder() - .method(method) - .uri(url.as_str()) - .body(data) - .unwrap(); - *http_request.headers_mut() = headers; - // handler is Arc>, so we need to dereference to call it - (**handler)(&label, http_request, responder); - }); - 1 } - #[allow(clippy::not_unsafe_ptr_arg_deref)] fn read( &self, data_out: *mut u8, bytes_to_read: ::std::os::raw::c_int, bytes_read: Option<&mut ::std::os::raw::c_int>, - callback: Option<&mut ResourceReadCallback>, + _callback: Option<&mut ResourceReadCallback>, ) -> ::std::os::raw::c_int { let Ok(bytes_to_read) = usize::try_from(bytes_to_read) else { return 0; }; - - // Streaming path: drive the StreamBody. When a chunk is buffered we copy - // it synchronously; when the producer has not written yet we retain - // `data_out` + `callback`, park a wake, and return continue-with-0 — CEF's - // async read contract. The wake (fired by the next `StreamWriter::write` - // or writer drop, on the producer thread) copies the chunk into the - // retained buffer and calls `callback.cont(n)`; `cont(0)` signals EOF. - { - let mut guard = self.stream.lock().expect("stream slot poisoned"); - if let Some(state) = guard.as_mut() { - if bytes_to_read == 0 { - if let Some(bytes_read) = bytes_read { - *bytes_read = 0; - } - return 1; - } - let out = unsafe { std::slice::from_raw_parts_mut(data_out, bytes_to_read) }; - let stream = self.stream.clone(); - let callback = callback.map(|callback| ThreadSafe(callback.clone())); - let retained = ThreadSafe((data_out, bytes_to_read)); - let wake = move || { - let (ptr, len) = retained.into_owned(); - let out = unsafe { std::slice::from_raw_parts_mut(ptr, len) }; - let mut guard = stream.lock().expect("stream slot poisoned"); - let count = match guard.as_mut() { - // The wake only fires after a real write or the writer's drop, so - // `Pending` cannot occur here; treat it (and `Done`) as EOF. - Some(state) => match state.body.read(out, || {}) { - ReadOutcome::Copied(count) => count as ::std::os::raw::c_int, - ReadOutcome::Pending | ReadOutcome::Done => 0, - }, - None => 0, - }; - if let Some(callback) = callback { - callback.into_owned().cont(count); - } - }; - return match state.body.read(out, wake) { - ReadOutcome::Copied(count) => { - if let Some(bytes_read) = bytes_read { - *bytes_read = count as ::std::os::raw::c_int; - } - 1 - } - ReadOutcome::Pending => { - if let Some(bytes_read) = bytes_read { - *bytes_read = 0; - } - 1 - } - ReadOutcome::Done => { - if let Some(bytes_read) = bytes_read { - *bytes_read = 0; - } - 0 - } - }; - } - } - - // Buffered path: copy out of the response `Cursor`. let data_out = unsafe { std::slice::from_raw_parts_mut(data_out, bytes_to_read) }; let count = self .response @@ -499,28 +344,42 @@ wrap_resource_handler! { response_length: Option<&mut i64>, redirect_url: Option<&mut CefString>, ) { - let Some(response) = response else { + let (Some(response), Some(response_data)) = (response, &*self.response.borrow()) else { return; }; - // Streaming path: publish the head the `StreamResponder` stored. By the - // time CEF calls this, the handler has already fired `callback.cont()`, so - // the head slot is populated. - { - let guard = self.stream.lock().expect("stream slot poisoned"); - if let Some(state) = guard.as_ref() { - if let Some(head) = state.head.lock().expect("stream head poisoned").as_ref() { - write_response_headers(response, head, response_length, redirect_url); - } - return; + response.set_status(response_data.status().as_u16() as i32); + let mut content_type = None; + + // Set response headers and remember the MIME type for CEF. + for (name, value) in response_data.headers() { + let Ok(value) = value.to_str() else { + continue; + }; + + response.set_header_by_name(Some(&name.as_str().into()), Some(&value.into()), 0); + + if name == CONTENT_TYPE { + content_type.replace(value.to_string()); } } - // Buffered path. - let Some(response_data) = &*self.response.borrow() else { - return; - }; - write_response_headers(response, response_data, response_length, redirect_url); + 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"); + response.set_mime_type(Some(&mime_type.into())); + + if let Some(length) = response_length { + *length = -1; + } + + if let Some(redirect_url) = redirect_url { + let _ = std::mem::take(redirect_url); + } } } } @@ -558,7 +417,8 @@ wrap_scheme_handler_factory! { .filter(|frame| frame.is_main() == 1) .map(|frame| CefString::from(&frame.url()).to_string()) .and_then(|url| Url::parse(&url).ok()) - .and_then(|url| tuple_origin(&url)); + .map(|url| url.origin().ascii_serialization()) + .filter(|origin| origin != "null"); Some(WebResourceHandler::new( webview_label, @@ -566,129 +426,78 @@ wrap_scheme_handler_factory! { initialization_scripts, initiator_origin, Arc::new(RefCell::new(None)), - Arc::new(Mutex::new(None)), )) } } } -pub(crate) struct ThreadSafe(pub(crate) T); +struct ThreadSafe(T); impl ThreadSafe { - pub(crate) fn into_owned(self) -> T { - self.0 - } + 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); - } - 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); - } - } - } - _ => {} + 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 -} - -/// The tuple origin of `url`, as **Chromium** serializes it. -/// -/// `Url::origin()` implements the URL spec, where only *special* schemes -/// (http, https, ws, wss, ftp, file) get a tuple origin and everything else is -/// opaque — serialized `"null"`. But a custom scheme registered with -/// `CEF_SCHEME_OPTION_STANDARD` (which is every scheme in -/// `CefConfig::custom_schemes`, see `register_tauri_schemes`) DOES get a real -/// `scheme://host[:port]` tuple origin inside Chromium, and that is the origin -/// the renderer actually enforces (CSP, CORS) — and the one a scheme handler -/// must compare against. Composing it by hand for the opaque case is what -/// makes `InitiatorOrigin` usable on custom schemes at all; without this it is -/// 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), - }) -} - -#[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); - } + body } 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/external_message_pump/linux.rs b/src/external_message_pump/linux.rs index 09d57fa..ada5ebd 100644 --- a/src/external_message_pump/linux.rs +++ b/src/external_message_pump/linux.rs @@ -13,10 +13,10 @@ //! GLib poll through a pipe. use std::{ - mem, - os::raw::{c_int, c_uint}, - sync::Weak, - time::{Duration, Instant}, + mem, + os::raw::{c_int, c_uint}, + sync::Weak, + time::{Duration, Instant}, }; use gtk::glib::{self, ffi, translate::ToGlibPtr}; @@ -25,32 +25,32 @@ use super::PumpState; #[repr(C)] struct WorkSource { - source: ffi::GSource, - source_state: *mut SourceState, + source: ffi::GSource, + source_state: *mut SourceState, } struct SourceState { - state: Weak, + state: Weak, - // The time when we need to do delayed work. - delayed_work_time: Option, + // 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, + // 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, + // Boxed to keep the GPollFD address stable while it is registered with the + // work source. + wakeup_gpollfd: Box, } pub(super) struct PlatformPump { - // The work source. It is destroyed when the message pump is destroyed. - work_source: *mut ffi::GSource, - source_state: Box, + // 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 @@ -59,118 +59,118 @@ pub(super) struct PlatformPump { unsafe impl Send for PlatformPump {} impl PlatformPump { - 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 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 + }; - 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"); - } + 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); + 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)); - } + 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 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 is_timer_pending(&self) -> bool { + get_time_interval_milliseconds(self.source_state.delayed_work_time) > 0 + } - pub(super) fn deadline(&self) -> Option { - self.source_state.delayed_work_time - } + pub(super) fn deadline(&self) -> Option { + self.source_state.delayed_work_time + } } 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); - } + 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; - }; - - // 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); - - // If this value is negative, then we need to run delayed work soon. - if delay < 0 { 0 } else { delay } + let Some(from) = from else { + return -1; + }; + + // 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); + + // 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. @@ -178,121 +178,121 @@ fn get_time_interval_milliseconds(from: Option) -> c_int { // signal and return EINTR. See man 7 signal. fn retry_eintr(mut f: F) -> isize where - F: FnMut() -> isize, + F: FnMut() -> isize, { - loop { - let result = f(); - if result != -1 || std::io::Error::last_os_error().raw_os_error() != Some(libc::EINTR) { - return result; - } + 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 } + 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) + // 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]); - } - } + // 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"); } - - 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; + 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]); + } } + } - false + 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(); - } + 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, + 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) }; - } + 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 + // 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 - } + 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, + 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 + 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, + 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 7c168d1..546a828 100644 --- a/src/external_message_pump/macos.rs +++ b/src/external_message_pump/macos.rs @@ -18,8 +18,8 @@ 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; @@ -52,21 +52,21 @@ 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 that will run events. - owner_thread: Retained, + // Owner thread that will run events. + owner_thread: Retained, - // Used to handle event callbacks on the owner thread. - event_handler: Retained, + // Used to handle event callbacks on the owner thread. + event_handler: Retained, - // Pending work timer. - timer: Option>, + // Pending work timer. + timer: Option>, } // SAFETY: the owner thread and timer are only touched on the AppKit thread that @@ -75,59 +75,60 @@ pub(super) struct PlatformPump { 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 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 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, - ) - }; - - // 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); + 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); } - pub(super) fn kill_timer(&mut self) { - if let Some(timer) = self.timer.take() { - timer.invalidate(); - } - } + self.timer = Some(timer); + } - pub(super) fn is_timer_pending(&self) -> bool { - self.timer.is_some() + 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() + } } diff --git a/src/external_message_pump/mod.rs b/src/external_message_pump/mod.rs index 870ca40..0165ac3 100644 --- a/src/external_message_pump/mod.rs +++ b/src/external_message_pump/mod.rs @@ -29,16 +29,16 @@ //! inside nested GLib loops (e.g. GTK menus/dialogs) for the same reason. 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")] @@ -71,141 +71,142 @@ const K_MAX_TIMER_DELAY: i64 = 1000 / 30; // 30fps /// 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() -> 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" - ))] - pub(crate) fn next_deadline(&self) -> Option { - self.state.platform.lock().ok().and_then(|p| p.deadline()) - } + 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" + ))] + 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 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); - } + /// 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); } - - /// 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); - } + } + + /// 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(); } - /// 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(); + 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); } + } - /// 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); - } + /// 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(); } - - fn is_timer_pending(&self) -> bool { - self.platform - .lock() - .map(|platform| platform.is_timer_pending()) - .unwrap_or(true) + self.do_work(); + } + + /// 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); } - - /// 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) + } + + fn is_timer_pending(&self) -> bool { + self + .platform + .lock() + .map(|platform| platform.is_timer_pending()) + .unwrap_or(true) + } + + /// 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 c52a8eb..d90660e 100644 --- a/src/external_message_pump/windows.rs +++ b/src/external_message_pump/windows.rs @@ -17,16 +17,16 @@ 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; @@ -40,11 +40,11 @@ const K_MSG_HAVE_WORK: u32 = WM_USER + 1; const TIMER_ID: usize = 1; pub(super) struct PlatformPump { - // HWND owned by the thread that CefDoMessageLoopWork should be invoked on. - main_thread_target: HWND, + // 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, + // True if a timer event is currently pending. + timer_pending: bool, } // SAFETY: `main_thread_target` is created on, and its timer is only @@ -54,124 +54,124 @@ pub(super) struct PlatformPump { 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(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, - } + 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"); - 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), - ) - }; - } + let state = Box::into_raw(Box::new(state)); + unsafe { SetWindowLongPtrW(hwnd, GWLP_USERDATA, state 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.main_thread_target), - TIMER_ID, - delay_ms as u32, - None, - ) - }; + Self { + main_thread_target: hwnd, + 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 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), + ) + }; + } + + 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 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.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); - } + 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 wnd_proc( - hwnd: HWND, - msg: u32, - wparam: WPARAM, - lparam: LPARAM, + 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(); - } - } + 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 4cd51f2..29c5a55 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,28 +6,15 @@ #![allow(clippy::too_many_arguments)] mod cef_impl; -mod compat; -mod config; mod external_message_pump; mod platform; -mod policy; mod runtime; -mod streaming; mod webview; mod window; mod window_builder; mod window_handle; -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, -}; 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 eeb03e3..2664b04 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 9c92b4d..c7f1c31 100644 --- a/src/platform/linux/monitor.rs +++ b/src/platform/linux/monitor.rs @@ -12,120 +12,122 @@ 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 17c3d34..f5ff835 100644 --- a/src/platform/linux/taskbar.rs +++ b/src/platform/linux/taskbar.rs @@ -7,36 +7,35 @@ 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! { @@ -44,138 +43,136 @@ 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; - } + fn ensure_lib_load(&mut self) { + if self.attempted_load { + return; + } - self.attempted_load = true; + 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() - }; + 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; + } - 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; + } + + 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(); } - 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 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 }) + }; + } + } + } + + 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; } - 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; - } + self.ensure_lib_load(); + + if !self.is_unity_running() { + return; + } - false + 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 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 self.unity_entry.is_none() { + self.ensure_entry_load(); } - 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) }; - } - } + 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 88e6ab5..fe9b4e6 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() { + 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() { 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 a5992be..410884c 100644 --- a/src/platform/linux/webview.rs +++ b/src/platform/linux/webview.rs @@ -13,127 +13,112 @@ 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 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 2e26cf4..1f39b25 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 42f714c..e285b05 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 6eaf04a..f40c094 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,65 +178,66 @@ 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 b4876b9..9eee282 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,78 +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; - } - } - - self.set_windows_can_hide(false); - utils::transform_process_type(kProcessTransformToUIElementApplication); + 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_show(&self) { - self.set_last_dock_show(Instant::now()); - self.set_windows_can_hide(true); + 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); - 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); - #[allow(deprecated)] - 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 86adf3b..4d64a6b 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,86 +16,84 @@ 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 = 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 fdaa741..4e42472 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 d9a6a02..a67058c 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)? - } - }; - - if let Some(progress_indicator) = existing_progress_indicator(&content_view) { - return Some(progress_indicator); + 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 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); + 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); - 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 6f5e694..6c941cf 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 b1851e9..43e1e0a 100644 --- a/src/platform/macos/webview.rs +++ b/src/platform/macos/webview.rs @@ -14,96 +14,84 @@ 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 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 2e99892..29930e2 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,226 +24,227 @@ 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 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 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 set_enabled(&self, enabled: bool) { + let nsview = self.nsview(); + let Some(nswindow) = nsview.window() else { + return; + }; - 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); - } + 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; + }; - pub(crate) fn is_enabled(&self) -> bool { - self.nsview() - .window() - .map(|nswindow| nswindow.attachedSheet().is_none()) - .unwrap_or(true) + 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 set_traffic_light_position(&self, position: &Position) { - let nsview = self.nsview(); - let Some(nswindow) = nsview.window() else { - return; - }; + inset_traffic_lights(&nswindow, pos.x, pos.y); + swizzle_draw_rect(&nsview); + } - 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 associate_appkit_state(&self) { + let nsview = self.nsview(); + let Some(nswindow) = nsview.window() else { + return; + }; - inset_traffic_lights(&nswindow, pos.x, pos.y); - swizzle_draw_rect(&nsview); - } + AppkitState::associate(&self.appkit_state, &nswindow); + } - pub(crate) fn associate_appkit_state(&self) { - 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; + }; - AppkitState::associate(&self.appkit_state, &nswindow); + 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); + } + _ => {} } + } - 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); - } - _ => {} - } - } + 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_visible_on_all_workspaces(&self, visible: bool) { - 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 mut collection_behavior = nswindow.collectionBehavior(); - collection_behavior.set(NSWindowCollectionBehavior::CanJoinAllSpaces, visible); - nswindow.setCollectionBehavior(collection_behavior); - } + pub(crate) fn set_background_color(&self, color: Option) { + 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 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)); - } + 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 704956f..ea35617 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 723c377..a3d43c5 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 d77c5d2..4a9b1fc 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 09f22a0..062e7c9 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 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 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 } + } } diff --git a/src/platform/windows/webview.rs b/src/platform/windows/webview.rs index 9d698e9..ffb4f95 100644 --- a/src/platform/windows/webview.rs +++ b/src/platform/windows/webview.rs @@ -6,94 +6,148 @@ 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, LPARAM, LRESULT, POINT, RECT, WPARAM}, + Graphics::Gdi::MapWindowPoints, + UI::Shell::{DefSubclassProc, SetWindowSubclass}, + UI::WindowsAndMessaging::{ + GetParent, GetWindowRect, HWND_TOP, SW_HIDE, SW_SHOW, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, + SWP_NOZORDER, SetParent, SetWindowPos, ShowWindow, WINDOWPOS, WM_WINDOWPOSCHANGING, + }, }; 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(); - - let mut rect = RECT::default(); - unsafe { - let parent = GetParent(hwnd).ok()?; - if parent.0.is_null() { - return None; - } - - 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 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 bounds(&self) -> Option { + let hwnd = self.hwnd(); - pub(crate) fn reparent(&self, parent: &AppWindow) { - let parent = parent.hwnd(); - let _ = unsafe { SetParent(self.hwnd(), Some(parent)) }; - } + let mut rect = RECT::default(); + unsafe { + let parent = GetParent(hwnd).ok()?; + if parent.0.is_null() { + return None; + } - pub(crate) fn apply_visible(&self, visible: bool) { - let _ = unsafe { ShowWindow(self.hwnd(), if visible { SW_SHOW } else { SW_HIDE }) }; + 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 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 destroy_native(&self) { - let _ = unsafe { DestroyWindow(self.hwnd()) }; + pub(crate) fn apply_visible(&self, visible: bool) { + let _ = unsafe { ShowWindow(self.hwnd(), if visible { SW_SHOW } else { SW_HIDE }) }; + } + + const PIN_Z_ORDER_SUBCLASS_ID: usize = 124; + /// `dwRefData` of the pin subclass: whether it is currently vetoing. + const Z_ORDER_UNPINNED: usize = 0; + const Z_ORDER_PINNED: usize = 1; + + /// Refuses every z-order change to this webview while the pin is engaged. + unsafe extern "system" fn pin_z_order_subclass_proc( + hwnd: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + _subclass_id: usize, + pinned: usize, + ) -> LRESULT { + unsafe { + if pinned == Self::Z_ORDER_PINNED && msg == WM_WINDOWPOSCHANGING && lparam.0 != 0 { + let window_pos = &mut *(lparam.0 as *mut WINDOWPOS); + window_pos.flags |= SWP_NOZORDER; + } + + DefSubclassProc(hwnd, msg, wparam, lparam) } + } + + /// Engages or disengages the z-order pin. + /// + /// Re-installing the same proc under the same id does not chain a second + /// subclass, it just updates `dwRefData` — so this both installs the pin the + /// first time and toggles it afterwards. + fn set_z_order_pinned(&self, pinned: bool) { + let _ = unsafe { + SetWindowSubclass( + self.hwnd(), + Some(Self::pin_z_order_subclass_proc), + Self::PIN_Z_ORDER_SUBCLASS_ID, + if pinned { + Self::Z_ORDER_PINNED + } else { + Self::Z_ORDER_UNPINNED + }, + ) + }; + } + + /// Raises this webview above its siblings and pins it there, so nothing but + /// this runtime can move it again. See [`Self::pin_z_order_subclass_proc`]. + pub(crate) fn raise_to_top(&self) { + self.set_z_order_pinned(false); + + let _ = unsafe { + SetWindowPos( + self.hwnd(), + Some(HWND_TOP), + 0, + 0, + 0, + 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE, + ) + }; + + self.set_z_order_pinned(true); + } - 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 aceaf2c..ab5de61 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, - }, - WindowsAndMessaging::DestroyIcon, + 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, + }, }; use crate::{window::AppWindow, window_handle::SoftbufferWindowHandle}; @@ -26,139 +26,138 @@ 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 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 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 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 set_enabled(&self, enabled: bool) { - let _ = unsafe { EnableWindow(self.hwnd(), enabled) }; + } + + 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_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) }; - } + if let Some(progress) = state.progress { + let _ = unsafe { taskbar.SetProgressValue(hwnd, progress.min(100), 100) }; } - - 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(); + } + + 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 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 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); } - /// 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) + 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) + } } diff --git a/src/runtime.rs b/src/runtime.rs index 3cfdddd..421083f 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -6,62 +6,64 @@ #![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, Webview, WebviewAtribute, 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; @@ -74,33 +76,81 @@ use winit::platform::x11::EventLoopBuilderExtX11; /// in minor releases when a known breaking change is discovered. pub use cef; +/// Platform-specific runtime init attributes. +#[derive(Clone, Debug)] +pub enum RuntimeInitAttribute { + /// Command line arguments passed to CEF. + CommandLineArgs { args: Vec<(String, Option)> }, + /// Deep link schemes. + DeepLinkSchemes { schemes: Vec }, + /// Directory used for CEF disk cache (`Settings::cache_path`). + /// + /// If unspecified, defaults to `{user cache}/{app identifier}/cef`. + CachePath { path: PathBuf }, +} + +impl tauri_runtime::InitAttribute for RuntimeInitAttribute { + fn new(config: &tauri_utils::config::Config) -> Result> { + let mut attrs = Vec::new(); + if let Some(plugin_config) = config + .plugins + .0 + .get("deep-link") + .and_then(|config| config.get("desktop").cloned()) + { + #[derive(serde::Deserialize)] + #[serde(untagged)] + enum DesktopDeepLinks { + One(tauri_utils::config::DeepLinkProtocol), + List(Vec), + } + + let protocols: DesktopDeepLinks = + serde_json::from_value(plugin_config).map_err(tauri_runtime::Error::Json)?; + let schemes = match protocols { + DesktopDeepLinks::One(protocol) => protocol.schemes, + DesktopDeepLinks::List(protocols) => protocols + .into_iter() + .flat_map(|protocol| protocol.schemes) + .collect(), + }; + + attrs.push(RuntimeInitAttribute::DeepLinkSchemes { schemes }); + } + Ok(attrs) + } +} + +#[derive(Debug)] +pub struct NewWindowOpener {} + #[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. @@ -114,855 +164,827 @@ 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 + }; - pub(crate) fn is_main_thread(&self) -> bool { - std::thread::current().id() == self.main_thread_id - } + self + .sender + .send(message) + .map_err(|_| Error::FailedToSendMessage)?; + self.proxy.wake_up(); + Ok(()) + } - /// 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 is_main_thread(&self) -> bool { + std::thread::current().id() == self.main_thread_id + } - pub(crate) fn next_window_id(&self) -> WindowId { - self.next_window_id.fetch_add(1, Ordering::Relaxed).into() + /// 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_webview_id(&self) -> u32 { - self.next_webview_id.fetch_add(1, Ordering::Relaxed) - } + pub(crate) fn next_window_id(&self) -> WindowId { + self.next_window_id.fetch_add(1, Ordering::Relaxed).into() + } - pub(crate) fn next_window_event_id(&self) -> u32 { - self.next_window_event_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_webview_event_id(&self) -> u32 { - self.next_webview_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) 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, + }, + NavigateFirstWebview { + window_id: WindowId, + url: String, + }, + 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, } 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, + ) -> 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, } + } - 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, - } + 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); - } + fn drain_messages(&mut self, event_loop: &dyn ActiveEventLoop) { + while let Ok(message) = self.receiver.try_recv() { + self.handle_message(event_loop, message); } + } - 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)), + 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 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(), + 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::NavigateFirstWebview { window_id, url } => { + self.navigate_first_webview(window_id, &url) + } + 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); } - } - - /// 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())); + } + Message::Task(task) => task(), + Message::RequestExit(code) => { + if self.request_exit(Some(code)) { + self.close_all_browsers(); + self.exit_if_done(event_loop); } + } + Message::Opened(urls) => self.run_callback(RunEvent::Opened { urls }), + #[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 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 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(), } + } - 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(); + /// 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())); + } + } - self.run_callback(RunEvent::WindowEvent { - label, - event: event.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)); + } + } + } - { - 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(); + + self.run_callback(RunEvent::WindowEvent { + label, + event: event.clone(), + }); + + { + let listeners = listeners.lock().unwrap(); + for handler in listeners.values() { + handler(&event); + } } + } - 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(), - }); + 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); + } + } + } - { - let listeners = listeners.lock().unwrap(); - for handler in listeners.values() { - handler(&event); - } - } + fn request_exit(&mut self, code: Option) -> bool { + // if we already exiting, don't request exit again + if self.state.exiting { + return false; } - 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 }); + + if matches!(rx.try_recv(), Ok(ExitRequestedEventAction::Prevent)) { + false + } else { + self.state.exiting = true; + true + } + } - let (tx, rx) = mpsc::channel(); - self.run_callback(RunEvent::ExitRequested { code, tx }); + pub(crate) fn close_window(&mut self, window_id: WindowId, event_loop: &dyn ActiveEventLoop) { + 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); + } - if matches!(rx.try_recv(), Ok(ExitRequestedEventAction::Prevent)) { - false - } else { - self.state.exiting = true; - true - } + 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; } - 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); + 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 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; - } + self.run_callback(RunEvent::WindowEvent { + label, + event: WindowEvent::CloseRequested { signal_tx: tx }, + }); - 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(), - }); - } - } + if !matches!(rx.try_recv(), Ok(true)) { + self.close_window(window_id, event_loop); + } + } - self.run_callback(RunEvent::WindowEvent { - label, - event: WindowEvent::CloseRequested { signal_tx: tx }, - }); + fn navigate_first_webview(&self, window_id: WindowId, url: &str) { + let Some(frame) = self + .state + .windows + .get(&window_id) + .and_then(|window| window.children.first()) + .and_then(|webview| webview.browser.main_frame()) + else { + return; + }; - if !matches!(rx.try_recv(), Ok(true)) { - self.close_window(window_id, event_loop); - } - } + frame.load_url(Some(&CefString::from(url))); + } - 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(); + 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(); + } - #[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); - } - } + #[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); + } } + } - fn exit_if_done(&mut self, event_loop: &dyn ActiveEventLoop) { - if self.state.live_browsers != 0 { - return; - } + 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 self.state.exiting || (self.state.windows.is_empty() && self.request_exit(None)) { + self.run_callback(RunEvent::Exit); + event_loop.exit(); } + } - /// 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)); - } + /// Service the default GLib main context so the external message pump's GLib + /// source (and any GTK work CEF schedules) gets dispatched, then arm winit to + /// wake when the next GLib pump deadline is due. 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 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_work(); - } - // Match wry/tao, which emit `Resumed` on each `Poll` start cause. - StartCause::Poll => self.run_callback(RunEvent::Resumed), - _ => {} - } + 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 proxy_wake_up(&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 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 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(); - } - _ => {} - } - } -} + 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; + }; -/// Registers the config-listed tauri custom protocol schemes with Chromium. -/// -/// Published tauri serves custom protocols at their native URL forms on -/// Linux/macOS (`tauri://localhost`, `ipc://localhost`, `asset://localhost`), -/// so Chromium must know each scheme as standard (URLs get an origin and -/// relative resolution), secure (secure-context APIs like WebCodecs and -/// getUserMedia work), CORS-enabled and fetch-enabled (the IPC transport is a -/// `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); + match event { + WinitWindowEvent::CloseRequested => self.request_window_close(window_id, event_loop), + + WinitWindowEvent::Destroyed => { + if !self.state.exiting { + self.emit_window_event(window_id, WindowEvent::Destroyed); + } + 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(); + } + _ => {} } + } } wrap_app! { @@ -974,10 +996,6 @@ wrap_app! { } impl App { - fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) { - register_tauri_schemes(registrar); - } - fn render_process_handler(&self) -> Option { Some(ipc::TauriRenderProcessHandler::new()) } @@ -1013,67 +1031,36 @@ wrap_app! { } } -/// Returns the pid of a verifiably-alive process holding this cache's -/// Chromium `SingletonLock`, if any. The lock is a symlink to -/// `-`; 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) -} - 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! { struct TauriCefHelperApp; impl App { - fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) { - register_tauri_schemes(registrar); - } - fn render_process_handler(&self) -> Option { Some(ipc::TauriRenderProcessHandler::new()) } @@ -1082,507 +1069,581 @@ 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(); - - #[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()); - - (sandbox, loader) - }; - - #[cfg(target_os = "macos")] - if !is_helper { - crate::platform::macos::setup_application(); - } +/// CEF installs Chromium's shutdown signal handlers for SIGINT/SIGTERM/SIGHUP +/// inside `cef::initialize`. Those handlers hand the signal to Chrome's own exit +/// machinery, which never quits the winit loop this runtime owns, so the process +/// just ignores the signal. Because the handler disarms itself on delivery, it +/// takes a second Ctrl+C to kill the app by default disposition. +/// +/// Put the process's original signal policy back so termination signals behave +/// the way they do for any other app (and the way they already do under +/// `tauri-runtime-wry`, which installs no handlers at all). +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd" +))] +struct TerminationSignals { + sigint: Option, + sigterm: Option, + sighup: Option, +} - // 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)); - } +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd" +))] +impl TerminationSignals { + fn capture() -> Self { + Self { + sigint: Self::capture_one(libc::SIGINT), + sigterm: Self::capture_one(libc::SIGTERM), + sighup: Self::capture_one(libc::SIGHUP), + } + } - // 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(); + /// Must run *after* `cef::initialize`, which is the only place CEF installs + /// these handlers; nothing reinstalls them later, so one restore is enough. + fn restore(&self) { + Self::restore_one(libc::SIGINT, self.sigint); + Self::restore_one(libc::SIGTERM, self.sigterm); + Self::restore_one(libc::SIGHUP, self.sighup); + } - 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(), - )); - } + fn capture_one(sig: libc::c_int) -> Option { + let mut action = std::mem::MaybeUninit::::uninit(); + if unsafe { libc::sigaction(sig, std::ptr::null(), action.as_mut_ptr()) } == 0 { + Some(unsafe { action.assume_init() }) + } else { + None + } + } - // 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(); - } + fn restore_one(sig: libc::c_int, previous: Option) { + let Some(previous) = previous else { + return; + }; - #[cfg(windows)] - if let Some(hook) = runtime_args.msg_hook { - use winit::platform::windows::EventLoopBuilderExtWindows; - event_loop_builder.with_msg_hook(hook); - } + unsafe { libc::sigaction(sig, &previous, std::ptr::null_mut()) }; + } +} - #[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, - ); +impl CefRuntime { + fn init( + mut event_loop_builder: EventLoopBuilder, + runtime_args: RuntimeInitArgs, + ) -> Result { + // Snapshot before CEF can touch anything, so we can tell an embedder's own + // signal policy apart from the handlers CEF installs in `cef::initialize`. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd" + ))] + let pre_cef_signals = TerminationSignals::capture(); - // 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 args = cef::args::Args::new(); - 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")] + let is_helper = is_cef_helper_process(); - #[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)); - } + #[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) + }; - Ok(Self { - event_loop, - receiver, - context, - scheme_registry: Default::default(), - #[cfg(target_os = "macos")] - _app_delegate: app_delegate, - }) + #[cfg(target_os = "macos")] + if !is_helper { + crate::platform::macos::setup_application(); } -} -impl Runtime for CefRuntime { - type WindowDispatcher = CefWindowDispatcher; - type WebviewDispatcher = CefWebviewDispatcher; - type Handle = CefRuntimeHandle; - type EventLoopProxy = EventProxy; + // 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); - fn new(args: RuntimeInitArgs) -> Result { - Self::init(EventLoopBuilder::default(), args) + // 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)); + } + + let mut command_line_args = Vec::new(); + let mut deep_link_schemes = Vec::new(); + let mut cache_path_override = None::; + for arg in runtime_args.platform_specific_attributes { + match arg { + RuntimeInitAttribute::CommandLineArgs { args } => command_line_args.extend(args), + RuntimeInitAttribute::DeepLinkSchemes { schemes } => deep_link_schemes.extend(schemes), + RuntimeInitAttribute::CachePath { path } => cache_path_override = Some(path), + } } + let cache_path = cache_path_override.unwrap_or_else(|| { + let cache_base = dirs::cache_dir().unwrap_or_else(std::env::temp_dir); + cache_base.join(&runtime_args.identifier).join("cef") + }); + let _ = create_dir_all(&cache_path); + + // Force X11 usage on Linux #[cfg(any( - windows, - 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" ))] - 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) + { + command_line_args.push(("ozone-platform".to_string(), Some("x11".to_string()))); + event_loop_builder.with_x11(); } - fn create_proxy(&self) -> Self::EventLoopProxy { - EventProxy { - context: self.context.clone(), - } + #[cfg(windows)] + if let Some(hook) = runtime_args.msg_hook { + use winit::platform::windows::EventLoopBuilderExtWindows; + event_loop_builder.with_msg_hook(hook); } - fn handle(&self) -> Self::Handle { - CefRuntimeHandle { - context: self.context.clone(), - } - } + #[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()), + }; - fn create_window) + Send + 'static>( - &self, - pending: PendingWindow, - after_window_creation: Option, - ) -> Result> { - create_window_detached(&self.context, pending, after_window_creation) - } + command_line_args.push(("--enable-media-stream".to_string(), None)); + let mut app = TauriCefApp::new( + context.clone(), + context_initialized.clone(), + deep_link_schemes, + command_line_args, + ); - fn create_webview( - &self, - window_id: WindowId, - pending: PendingWebview, - ) -> Result> { - create_webview_detached(&self.context, window_id, pending) - } + // 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" + ); - fn primary_monitor(&self) -> Option { - event_loop_getter!(self, PrimaryMonitor).ok().flatten() + 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, + ..Default::default() + }; + if cef::initialize( + Some(args.as_main_args()), + Some(&settings), + Some(&mut app), + std::ptr::null_mut(), + ) != 1 + { + return Err(Error::WebviewRuntimeNotInstalled); } - 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() - } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd" + ))] + pre_cef_signals.restore(); - fn available_monitors(&self) -> Vec { - event_loop_getter!(self, AvailableMonitors).unwrap_or_default() - } + #[cfg(target_os = "macos")] + let app_delegate = if !is_helper { + use crate::platform::macos::AppDelegateEvent; - fn cursor_position(&self) -> Result> { - event_loop_getter!(self, CursorPosition)? - } + 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 + }; - fn set_theme(&self, theme: Option) { - let message = Message::EventLoop(EventLoopMessage::SetTheme(theme)); - let _ = self.context.send_message(message); + // 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)); } - #[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); - } + Ok(Self { + event_loop, + receiver, + context, + scheme_registry: Default::default(), + #[cfg(target_os = "macos")] + _app_delegate: app_delegate, + }) + } +} - #[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); - } +impl Runtime for CefRuntime { + type WindowDispatcher = CefWindowDispatcher; + type WebviewDispatcher = CefWebviewDispatcher; + type Handle = CefRuntimeHandle; + type EventLoopProxy = EventProxy; + type PlatformSpecificWebviewAttribute = WebviewAtribute; + type Webview = Webview; + type PlatformSpecificInitAttribute = RuntimeInitAttribute; + type WindowOpener = NewWindowOpener; + + fn new(args: RuntimeInitArgs) -> Result { + Self::init(EventLoopBuilder::default(), args) + } - #[cfg(target_os = "macos")] - fn show(&self) { - let message = Message::EventLoop(EventLoopMessage::ShowApplication); - let _ = self.context.send_message(message); - } + #[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(target_os = "macos")] - fn hide(&self) { - let message = Message::EventLoop(EventLoopMessage::HideApplication); - let _ = self.context.send_message(message); + fn create_proxy(&self) -> Self::EventLoopProxy { + EventProxy { + context: self.context.clone(), } + } - fn set_device_event_filter(&mut self, filter: DeviceEventFilter) { - self.event_loop - .listen_device_events(device_event_filter_to_winit(filter)); + fn handle(&self) -> Self::Handle { + CefRuntimeHandle { + context: self.context.clone(), } + } - 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); - } + fn create_window) + Send + 'static>( + &self, + pending: PendingWindow, + after_window_creation: Option, + ) -> Result> { + create_window_detached(&self.context, pending, after_window_creation) + } - 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 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 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 run) + 'static>(self, callback: F) { - self.run_return(callback); + fn available_monitors(&self) -> Vec { + event_loop_getter!(self, AvailableMonitors).unwrap_or_default() + } + + 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); + } + + #[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 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); + } + + fn set_device_event_filter(&mut self, filter: DeviceEventFilter) { + self + .event_loop + .listen_device_events(device_event_filter_to_winit(filter)); + } + + fn custom_scheme_url(scheme: &str, https: bool) -> String { + format!( + "{}://{scheme}.localhost", + if https { "https" } else { "http" } + ) + } + + 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); + } + + fn run_return) + 'static>(self, callback: F) -> i32 { + self.run(callback); + // TODO: return the exit code from the runtime, if possible. For now, always return 0 + 0 + } + + fn run) + 'static>(self, callback: F) { + let app = WinitCefApp::new( + self.context, + self.receiver, + Box::new(callback), + self.scheme_registry, + ); + let _ = self.event_loop.run_app(app); + cef::shutdown(); + } } diff --git a/src/webview.rs b/src/webview.rs index 1c35cd0..edbc103 100644 --- a/src/webview.rs +++ b/src/webview.rs @@ -5,24 +5,26 @@ 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, Runtime, UserEvent, WebviewDispatch, WebviewEventId, + dpi::{PhysicalPosition, PhysicalSize, Position, Rect, Size}, + webview::{ + DetachedWebview, InitializationScript, PendingWebview, UriSchemeProtocolHandler, + WebviewAttributes, + }, + window::{WebviewEvent, WindowId}, }; use tauri_utils::{Theme, config::Color, html::normalize_script_for_csp}; use url::Url; use crate::cef_impl::{client as browser_client, cookie, request_context, request_handler}; -use crate::compat::{self, UriSchemeProtocolHandler}; use crate::runtime::{CefRuntime, Message, RuntimeContext, WinitCefApp}; use crate::window::AppWindow; @@ -32,37 +34,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 +86,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,1094 +126,1105 @@ 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); - - 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, - }); - } + 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); - self.host.notify_move_or_resize_started(); - self.apply_physical_bounds(scale, x, y, w, h); - self.host.was_resized(); + 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, + }); } - pub(crate) fn set_visible(&self, visible: bool) { - self.host.was_hidden(if visible { 0 } else { 1 }); - self.apply_visible(visible); - } + self.host.notify_move_or_resize_started(); + self.apply_physical_bounds(scale, x, y, w, h); + self.host.was_resized(); + } - pub fn url(&self) -> Option { - self.browser - .main_frame() - .map(|frame| cef::CefString::from(&frame.url()).to_string()) - } + 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()) + } } 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, - ) - } + 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(), - )); - }; + /// 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(()) + // On Windows a window's webviews are sibling child HWNDs. Put each new one + // on top of the ones already there — the order they were created in — and + // pin it, so Chromium's focus raise cannot reshuffle them behind our back + // and bury an overlay webview under the one that fills the window. + #[cfg(windows)] + child.raise_to_top(); + + *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); + let address_changed_handler = pending.address_changed_handler.take().map(Arc::from); + 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 = bounds.to_physical::(scale); + #[cfg(target_os = "macos")] + let bounds = bounds.to_logical::(scale); + let bounds = cef::Rect { + x: bounds.position.x, + y: bounds.position.y, + width: bounds.size.width, + height: bounds.size.height, + }; + + // Let CEF pick the runtime style unless overridden per-webview. + let cef_runtime_style = pending + .platform_specific_attributes + .iter() + .map(|attr| match attr { + WebviewAtribute::RuntimeStyle { style } => match style { + RuntimeStyle::Alloy => cef::RuntimeStyle::ALLOY, + RuntimeStyle::Chrome => cef::RuntimeStyle::CHROME, + }, + }) + .next() + .unwrap_or(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; + }; + 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(), + ), + ); + } + } - 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 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, ); - 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, + browser_tx + .send(AppWebview { 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(), - ); + 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); - // 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, - }; + // `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() + } - // 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; + 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 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 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; + }; - let custom_protocol_scheme = if pending.webview_attributes.use_https_scheme { - "https" - } else { - "http" + 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); } - .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, + } + 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 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 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(); - 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; + 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()); } - - let Some(appwindow) = self.state.windows.get_mut(&window_id) else { - return; + } + 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)), + WebviewMessage::Close => child.host.close_browser(0), + 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, }; - let Some(child) = appwindow - .children - .iter_mut() - .find(|child| child.webview_id == webview_id) + 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 { - return; + let _ = tx.send(Err(Error::WindowNotFound)); + 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(), - ); - - 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(())); - } - } + 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, + ); + // Re-parenting does not preserve z-order: a view docked back into a + // window that already owns a full-window main webview must be put back + // on top, or it lands behind it and renders nothing. + #[cfg(windows)] + child.raise_to_top(); + + 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(Clone, Copy, Debug)] +pub enum RuntimeStyle { + Alloy, + Chrome, +} + +#[derive(Debug)] +pub enum WebviewAtribute { + RuntimeStyle { style: RuntimeStyle }, } +unsafe impl Send for WebviewAtribute {} +unsafe impl Sync for WebviewAtribute {} + #[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, - }) - } +impl WebviewDispatch for CefWebviewDispatcher { + type Runtime = CefRuntime; - pub fn can_go_back(&self) -> Result { - webview_getter!(self, CanGoBack) - } + fn run_on_main_thread(&self, f: F) -> Result<()> { + self.context.run_on_main_thread(f) + } - 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, - }) - } + 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 + } - pub fn can_go_forward(&self) -> Result { - webview_getter!(self, CanGoForward) - } -} + fn with_webview>::Webview) + Send + 'static>( + &self, + f: F, + ) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::WithWebview(Box::new(f)), + }) + } -impl WebviewDispatch for CefWebviewDispatcher { - type Runtime = CefRuntime; + #[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, + }); + } - fn run_on_main_thread(&self, f: F) -> Result<()> { - self.context.run_on_main_thread(f) - } + #[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, + }); + } - 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 - } + #[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 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 url(&self) -> Result { + webview_getter!(self, Url) + } - #[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, - }); - } + fn bounds(&self) -> Result { + webview_getter!(self, Bounds) + } - #[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, - }); - } + fn position(&self) -> Result> { + webview_getter!(self, Position) + } - #[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 size(&self) -> Result> { + webview_getter!(self, Size) + } - fn url(&self) -> Result { - webview_getter!(self, 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 bounds(&self) -> Result { - webview_getter!(self, Bounds) - } + 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 position(&self) -> Result> { - webview_getter!(self, Position) - } + 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, + }) + } - fn size(&self) -> Result> { - webview_getter!(self, Size) - } + fn can_go_back(&self) -> Result { + webview_getter!(self, CanGoBack) + } - 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 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, + }) + } - 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 can_go_forward(&self) -> Result { + webview_getter!(self, CanGoForward) + } - 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; - } - result + 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 + } - 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. @@ -1220,22 +1233,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. @@ -1243,50 +1256,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); @@ -1368,22 +1381,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(); + }; + + 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() + 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; @@ -1427,29 +1440,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__ = @@ -1463,71 +1476,70 @@ 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__"); - } - source.push_str(") {\n"); - source.push_str(init_script.script.as_str()); - source.push_str("\n }\n"); + 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"); - 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, - } - }) - .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 + 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 + } } wrap_task! { @@ -1544,8 +1556,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 @@ -1557,31 +1569,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 b14de91..71c99e2 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,1408 +45,1401 @@ 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; + 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}, }; - // `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; - } + 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; - }; + 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(); + #[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; - } + // 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) + } - let position = calculate_window_center_position(window_size, &monitor); - self.window.set_outer_position(Position::Physical(position)); + 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); + } + + 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); + } + } +} + +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())), + }; + + #[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); + } } - pub(crate) fn preferred_theme(&self) -> Option { - self.attrs - .inner - .preferred_theme - .map(winit_theme_to_tauri_theme) + #[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 resolved_theme(&self, app_wide_theme: Option) -> Option { - self.preferred_theme().or(app_wide_theme) + #[cfg(windows)] + if appwindow.attrs.inner.transparent || appwindow.attrs.background_color.is_some() { + appwindow.draw_background_surface(); } - 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(not(windows))] + if appwindow.attrs.background_color.is_some() { + appwindow.set_background_color(appwindow.attrs.background_color); } - 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); - } + #[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; + } + _ => {} } -} -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())), + 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.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); - } + 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" + 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); - } - - #[cfg(windows)] - if appwindow.attrs.inner.transparent || appwindow.attrs.background_color.is_some() { - appwindow.draw_background_surface(); + appwindow.attrs.visible_on_all_workspaces = _value; + appwindow.set_visible_on_all_workspaces(_value); } - - #[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, - }); + } + 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)) } - - // 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::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); + } } + } } #[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, - }) - } - - 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 create_window) + Send + 'static>( - &mut self, - pending: PendingWindow, - after_window_creation: Option, - ) -> Result> { - create_window_detached(&self.context, pending, after_window_creation) - } + 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, + }) + } - fn create_webview( - &mut self, - pending: PendingWebview, - ) -> Result> { - create_webview_detached(&self.context, self.window_id, pending) - } + 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_resizable(&self, resizable: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetResizable(resizable), - }) - } + 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 set_enabled(&self, enabled: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetEnabled(enabled), - }) - } + fn set_enabled(&self, enabled: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetEnabled(enabled), + }) + } - fn set_maximizable(&self, maximizable: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetMaximizable(maximizable), - }) - } + fn set_maximizable(&self, maximizable: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetMaximizable(maximizable), + }) + } - fn set_minimizable(&self, minimizable: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetMinimizable(minimizable), - }) - } + fn set_minimizable(&self, minimizable: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetMinimizable(minimizable), + }) + } - fn set_closable(&self, closable: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetClosable(closable), - }) - } + fn set_closable(&self, closable: bool) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetClosable(closable), + }) + } - fn set_title>(&self, title: S) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetTitle(title.into()), - }) - } + fn set_title>(&self, title: S) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetTitle(title.into()), + }) + } - fn maximize(&self) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Maximize, - }) - } + fn maximize(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Maximize, + }) + } - fn unmaximize(&self) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Unmaximize, - }) - } + fn unmaximize(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Unmaximize, + }) + } - fn minimize(&self) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Minimize, - }) - } + fn minimize(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Minimize, + }) + } - fn unminimize(&self) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Unminimize, - }) - } + fn unminimize(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Unminimize, + }) + } - fn show(&self) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Show, - }) - } + fn show(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Show, + }) + } - fn hide(&self) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Hide, - }) - } + fn hide(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Hide, + }) + } - fn close(&self) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Close, - }) - } + fn close(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Close, + }) + } - fn destroy(&self) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::Destroy, - }) - } + fn destroy(&self) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::Destroy, + }) + } - fn set_decorations(&self, decorations: bool) -> Result<()> { - self.context.send_message(Message::Window { - window_id: self.window_id, - message: WindowMessage::SetDecorations(decorations), - }) - } + 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_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_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_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_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_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_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_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_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_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_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), - }) - } + 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), - }) - } + #[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_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_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_icon(&self, icon: Icon) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetIcon(icon.into_owned()), + }) + } - 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_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_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_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_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_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 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_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 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_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_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_overlay_icon(&self, icon: Option) -> Result<()> { + self.context.send_message(Message::Window { + window_id: self.window_id, + message: WindowMessage::SetOverlayIcon(icon.map(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_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_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_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_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), - }) - } + 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)), - webview_id, - context: context.clone(), - }, - }, - use_https_scheme, - }); - - Ok(DetachedWindow { - id: window_id, - label, - dispatcher: CefWindowDispatcher { - window_id, - context: context.clone(), - }, - webview, + let label = pending.label.clone(); + let window_id = context.next_window_id(); + let (webview_id, use_https_scheme, devtools) = pending + .webview + .as_ref() + .map(|w| { + ( + Some(context.next_webview_id()), + w.webview_attributes.use_https_scheme, + w.webview_attributes.devtools, + ) }) + .unwrap_or((None, false, None)); + + 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)), + webview_id, + context: context.clone(), + }, + }, + use_https_scheme, + devtools, + }); + + 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 3a2ee83..7d5bf30 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,467 +46,493 @@ 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() - }, - } - .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); - } - #[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 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); - fn visible(mut self, visible: bool) -> Self { - self.attrs.inner = self.attrs.inner.with_visible(visible); - self + #[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); } - #[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 + { + builder = builder.transparent(config.transparent); } - - 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 + if let (Some(min_width), Some(min_height)) = (config.min_width, config.min_height) { + builder = builder.min_inner_size(min_width, min_height); } - - 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; - } - - 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 content_protected(mut self, protected: bool) -> Self { - self.attrs.inner = self.attrs.inner.with_content_protected(protected); - self + if let Some(color) = config.background_color { + builder = builder.background_color(color); } - - 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) + if let (Some(x), Some(y)) = (config.x, config.y) { + builder = builder.position(x, y); } - - #[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)); - } - - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - { - self.attrs.skip_taskbar = skip; + #[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()) } - - self + _ => 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; } - fn background_color(mut self, color: Color) -> Self { - self.attrs.background_color = Some(color); - self - } + 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 content_protected(mut self, protected: bool) -> Self { + self.attrs.inner = self.attrs.inner.with_content_protected(protected); + 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 - } + 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)] - 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 + { + 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)); } #[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" ))] - 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 + { + self.attrs.skip_taskbar = skip; } #[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 - } + let _skip = skip; - #[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 - } + 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)); - } + fn background_color(mut self, color: Color) -> Self { + self.attrs.background_color = Some(color); + self + } - self - } - - fn has_icon(&self) -> bool { - self.attrs.inner.window_icon.is_some() + #[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 get_theme(&self) -> Option { - self.attrs - .inner - .preferred_theme - .map(winit_theme_to_tauri_theme) - } + #[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 { + use objc2::rc::Retained; + use objc2_app_kit::{NSView, NSWindow}; + + let Some(nswindow) = NonNull::new(parent) else { + return self; + }; + let Some(nswindow) = (unsafe { Retained::::from_raw(nswindow.as_ptr() as _) }) else { + return self; + }; + + let Some(nsview) = nswindow.contentView() else { + return self; + }; + let nsview = Retained::::into_raw(nsview); + let Some(nsview) = NonNull::new(nsview as _) else { + return self; + }; + + let handle = winit::raw_window_handle::AppKitWindowHandle::new(nsview); + let handle = RawWindowHandle::AppKit(handle); + 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 + } + + // TODO + fn no_redirection_bitmap(#[allow(unused_mut)] mut self, _enable: bool) -> Self { + 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)] @@ -516,9 +542,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 6edff6a..3d0ae80 100644 --- a/src/window_handle.rs +++ b/src/window_handle.rs @@ -16,36 +16,34 @@ 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) }) + } }