diff --git a/crates/tauri-runtime-cef/src/cef_impl.rs b/crates/tauri-runtime-cef/src/cef_impl.rs index a538fdcbc..50d094cd6 100644 --- a/crates/tauri-runtime-cef/src/cef_impl.rs +++ b/crates/tauri-runtime-cef/src/cef_impl.rs @@ -10,13 +10,13 @@ use std::{ }, }; use tauri_runtime::{ - webview::{InitializationScript, UriSchemeProtocol}, + webview::{InitializationScript, PendingWebview, UriSchemeProtocol}, window::{PendingWindow, WindowId}, RunEvent, UserEvent, }; use tauri_utils::html::normalize_script_for_csp; -use crate::{AppWindow, CefRuntime, Message}; +use crate::{AppWindow, BrowserViewWrapper, CefRuntime, Message}; mod request_handler; @@ -135,9 +135,31 @@ wrap_client! { } } +wrap_browser_view_delegate! { + struct BrowserViewDelegateImpl { + use_alloy_style: bool, + } + + impl ViewDelegate {} + + impl BrowserViewDelegate { + fn browser_runtime_style(&self) -> RuntimeStyle { + use cef::sys::cef_runtime_style_t; + + if self.use_alloy_style { + // Use Alloy style for additional webviews (multiwebview support) + RuntimeStyle::from(cef_runtime_style_t::CEF_RUNTIME_STYLE_ALLOY) + } else { + // Use Chrome style (default) for the first webview + RuntimeStyle::from(cef_runtime_style_t::CEF_RUNTIME_STYLE_CHROME) + } + } + } +} + wrap_window_delegate! { struct AppWindowDelegate { - browser_view: BrowserView, + initial_browser_view: Option, } impl ViewDelegate { @@ -156,8 +178,11 @@ wrap_window_delegate! { impl WindowDelegate { fn on_window_created(&self, window: Option<&mut Window>) { if let Some(window) = window { - let mut view = View::from(&self.browser_view); - window.add_child_view(Some(&mut view)); + // If we have an initial browser view, add it + if let Some(ref browser_view) = self.initial_browser_view { + let mut view = View::from(browser_view); + window.add_child_view(Some(&mut view)); + } window.show(); } } @@ -188,6 +213,81 @@ wrap_window_delegate! { } } +pub fn handle_message(context: &Context, message: Message) { + match message { + Message::CreateWindow { + window_id, + webview_id, + pending, + after_window_creation: _todo, + } => create_window(context, window_id, webview_id, pending), + Message::CreateWebview { + window_id, + webview_id, + pending, + } => create_webview( + WebviewKind::WindowChild, + context, + window_id, + webview_id, + pending, + ), + #[cfg(any(debug_assertions, feature = "devtools"))] + Message::OpenDevTools { + window_id, + webview_id, + } => { + if let Some(app_window) = context.windows.borrow().get(&window_id) { + if let Some(browser_view_wrapper) = app_window + .webviews + .iter() + .find(|w| w.webview_id == webview_id) + { + if let Some(browser) = browser_view_wrapper.browser_view.browser() { + if let Some(host) = browser.host() { + // ShowDevTools(window_info, client, settings, inspect_element_at) + // Using None for client and default settings, inspect at (0,0) + let window_info = cef::WindowInfo::default(); + let settings = cef::BrowserSettings::default(); + let inspect_at = cef::Point { x: 0, y: 0 }; + host.show_dev_tools( + Some(&window_info), + Option::<&mut cef::Client>::None, + Some(&settings), + Some(&inspect_at), + ); + } + } + } + } + } + #[cfg(any(debug_assertions, feature = "devtools"))] + Message::CloseDevTools { + window_id, + webview_id, + } => { + if let Some(app_window) = context.windows.borrow().get(&window_id) { + if let Some(browser_view_wrapper) = app_window + .webviews + .iter() + .find(|w| w.webview_id == webview_id) + { + if let Some(browser) = browser_view_wrapper.browser_view.browser() { + if let Some(host) = browser.host() { + host.close_dev_tools(); + } + } + } + } + } + Message::Task(t) => t(), + Message::UserEvent(evt) => { + (context.callback.borrow_mut())(RunEvent::UserEvent(evt)); + } + Message::Noop => {} + } +} + wrap_task! { pub struct SendMessageTask { context: Context, @@ -196,19 +296,7 @@ wrap_task! { impl Task { fn execute(&self) { - match self.message.replace(Message::Noop) { - Message::CreateWindow { - window_id, - webview_id, - pending, - after_window_creation: _todo, - } => create_window(&self.context, window_id, webview_id, pending), - Message::Task(t) => t(), - Message::UserEvent(evt) => { - (self.context.callback.borrow_mut())(RunEvent::UserEvent(evt)); - } - Message::Noop => {} - } + handle_message(&self.context, self.message.replace(Message::Noop)); } } } @@ -216,16 +304,70 @@ wrap_task! { fn create_window( context: &Context, window_id: WindowId, - webview_id: u32, + _webview_id: u32, pending: PendingWindow>, ) { let label = pending.label.clone(); - let webview = pending.webview.unwrap(); + // Create window delegate - we'll handle webviews separately + // For windows without webviews, we use a delegate without initial browser view + let mut delegate = AppWindowDelegate::new(None); + + let window = window_create_top_level(Some(&mut delegate)).expect("Failed to create window"); + window.show(); + + // Insert window with empty webviews list + context.windows.borrow_mut().insert( + window_id, + AppWindow { + label, + window, + webviews: Vec::new(), + content_panel: None, + }, + ); + + // If a webview was provided, create it now + if let Some(webview) = pending.webview { + let webview_id = context.next_webview_id(); + create_webview( + WebviewKind::WindowContent, + context, + window_id, + webview_id, + webview, + ); + } +} + +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +enum WebviewKind { + // webview is the entire window content + WindowContent, + // webview is a child of the window, which can contain other webviews too + WindowChild, +} + +fn create_webview( + kind: WebviewKind, + context: &Context, + window_id: WindowId, + webview_id: u32, + pending: PendingWebview>, +) { + // Get the window - return early if not found + let mut windows = context.windows.borrow_mut(); + let app_window = match windows.get_mut(&window_id) { + Some(w) => w, + None => { + eprintln!("Window {:?} not found when creating webview", window_id); + return; + } + }; // Get initialization scripts from webview attributes // Pre-compute script hashes once at webview creation time - let initialization_scripts: Vec<_> = webview + let initialization_scripts: Vec<_> = pending .webview_attributes .initialization_scripts .into_iter() @@ -233,7 +375,7 @@ fn create_window( .collect(); let mut client = BrowserClient::new(initialization_scripts.clone()); - let url = CefString::from(webview.url.as_str()); + let url = CefString::from(pending.url.as_str()); let global_context = request_context_get_global_context().expect("Failed to get global request context"); @@ -248,11 +390,8 @@ fn create_window( ); if let Some(request_context) = &request_context { // Ensure schemes are registered with proper flags (fetch-enabled, secure, etc.) - // This is done in App::on_register_custom_schemes, but we also track - // which schemes we've seen - - for (scheme, handler) in webview.uri_scheme_protocols { - let label = label.clone(); + for (scheme, handler) in pending.uri_scheme_protocols { + let label = app_window.label.clone(); request_context.register_scheme_handler_factory( Some(&scheme.as_str().into()), None, @@ -268,22 +407,72 @@ fn create_window( } } + let mut browser_view_delegate = + BrowserViewDelegateImpl::new(matches!(kind, WebviewKind::WindowChild)); + let browser_view = browser_view_create( Some(&mut client), Some(&url), Some(&Default::default()), Option::<&mut DictionaryValue>::None, request_context.as_mut(), - Option::<&mut BrowserViewDelegate>::None, + Some(&mut browser_view_delegate), ) .expect("Failed to create browser view"); - let mut delegate = AppWindowDelegate::new(browser_view); + let mut view = View::from(&browser_view); - let window = window_create_top_level(Some(&mut delegate)).expect("Failed to create window"); + let bounds = pending.webview_attributes.bounds.map(|bounds| { + let device_scale_factor = app_window + .window + .display() + .map(|d| d.device_scale_factor() as f64) + .unwrap_or(1.0); + let physical_position = bounds.position.to_physical::(device_scale_factor); + let physical_size = bounds.size.to_physical::(device_scale_factor); + Rect { + x: physical_position.x, + y: physical_position.y, + width: physical_size.width as i32, + height: physical_size.height as i32, + } + }); - context - .windows - .borrow_mut() - .insert(window_id, AppWindow { label, window }); + if let Some(bounds) = &bounds { + view.set_bounds(Some(bounds)); + } + + if kind == WebviewKind::WindowChild { + let panel = if let Some(panel) = &app_window.content_panel { + panel.clone() + } else { + let panel = cef::panel_create(None).expect("Failed to create content panel"); + + panel.set_bounds(Some(&app_window.window.bounds())); + + use cef::BoxLayoutSettings; + let mut layout_settings = BoxLayoutSettings::default(); + layout_settings.horizontal = 1; + layout_settings.default_flex = 0; + layout_settings.between_child_spacing = 0; + panel.set_to_box_layout(Some(&layout_settings)); + + app_window + .window + .add_child_view(Some(&mut View::from(&panel))); + + app_window.content_panel.replace(panel.clone()); + + panel + }; + + panel.add_child_view(Some(&mut view)); + } else { + app_window.window.add_child_view(Some(&mut view)); + } + + app_window.webviews.push(BrowserViewWrapper { + webview_id, + browser_view, + }); } diff --git a/crates/tauri-runtime-cef/src/cef_impl/request_handler.rs b/crates/tauri-runtime-cef/src/cef_impl/request_handler.rs index e8420b247..bb67ab801 100644 --- a/crates/tauri-runtime-cef/src/cef_impl/request_handler.rs +++ b/crates/tauri-runtime-cef/src/cef_impl/request_handler.rs @@ -7,9 +7,12 @@ use std::{ use cef::{rc::*, *}; use html5ever::{interface::QualName, namespace_url, ns, LocalName}; -use http::{header::CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue}; +use http::{ + header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE}, + HeaderMap, HeaderName, HeaderValue, +}; use kuchiki::NodeRef; -use tauri_runtime::webview::{InitializationScript, UriSchemeProtocol}; +use tauri_runtime::webview::UriSchemeProtocol; use tauri_utils::{ config::{Csp, CspDirectiveSources}, html::{parse as parse_html, serialize_node}, @@ -18,15 +21,10 @@ use url::Url; use super::CefInitScript; -// Note: We handle head element manipulation inline in the filter - // ResponseFilter that injects initialization scripts into HTML responses -// For HTTP/HTTPS with CSP headers, we inject a modified CSP meta tag wrap_response_filter! { pub struct HtmlScriptInjectionFilter { - initialization_scripts: Vec, - script_hashes: Vec, // Pre-computed script hashes - csp_header: Option, // Original CSP header from HTTP response (if any) + initialization_scripts: Vec, processed_html: RefCell>>, output_offset: RefCell, } @@ -65,65 +63,13 @@ wrap_response_filter! { head_node }; - // If CSP header exists, inject/modify CSP meta tag with script hashes - // This ensures injected scripts work even when HTTP response has CSP header - if let Some(ref original_csp) = self.csp_header { - // Parse CSP using tauri-utils - let mut csp_map: std::collections::HashMap = - Csp::Policy(original_csp.clone()).into(); - - // Update or create script-src directive with script hashes - let script_src = csp_map - .entry("script-src".to_string()) - .or_insert_with(|| CspDirectiveSources::List(vec!["'self'".to_string()])); - - // Extend with script hashes - script_src.extend(self.script_hashes.clone()); - - // Convert back to CSP string - let updated_csp = Csp::DirectiveMap(csp_map).to_string(); - // Check if CSP meta tag already exists - let should_update_meta = if let Ok(ref meta_node) = document.select_first("meta[http-equiv='Content-Security-Policy']") { - let element = meta_node.as_node().as_element().unwrap(); - let mut attrs = element.attributes.borrow_mut(); - attrs.insert("content", updated_csp.clone()); - false // Updated existing meta tag, don't create new one - } else { - true // Need to create new meta tag - }; - - if should_update_meta { - use kuchiki::{Attribute, ExpandedName}; - let csp_meta = NodeRef::new_element( - QualName::new(None, ns!(html), LocalName::from("meta")), - vec![ - ( - ExpandedName::new(ns!(), LocalName::from("http-equiv")), - Attribute { - prefix: None, - value: "Content-Security-Policy".into(), - }, - ), - ( - ExpandedName::new(ns!(), LocalName::from("content")), - Attribute { - prefix: None, - value: updated_csp.into(), - }, - ), - ], - ); - head.prepend(csp_meta); - } - } - // iterate in reverse order since we are prepending each script to the head tag for init_script in self.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())); + script_el.append(NodeRef::new_text(init_script.script.script.as_str())); head.prepend(script_el); } @@ -233,29 +179,16 @@ wrap_resource_request_handler! { return None; }; - // Extract CSP header if present - let csp_header = { - let csp_header_name = CefString::from("Content-Security-Policy"); - let existing_csp = response.header_by_name(Some(&csp_header_name)); - // check if csp_header is not null manually - I believe header_by_name should return Option instead - let csp_header: Option<&cef_dll_sys::_cef_string_utf16_t> = (&existing_csp).into(); - if csp_header.is_some() { - Some(CefString::from(&existing_csp).to_string()) - } else { - None - } - }; - let is_main_frame = frame.is_main() == 1; // Filter scripts based on frame type let scripts_to_inject: Vec<_> = if is_main_frame { - self.initialization_scripts.iter().map(|s| s.script.clone()).collect() + self.initialization_scripts.clone() } else { self.initialization_scripts .iter() .filter(|s| !s.script.for_main_frame_only) - .map(|s| s.script.clone()) + .cloned() .collect() }; @@ -277,8 +210,6 @@ wrap_resource_request_handler! { // Return a filter that will inject scripts and update CSP in HTML if header exists Some(HtmlScriptInjectionFilter::new( scripts_to_inject, - script_hashes, - csp_header, RefCell::new(None), RefCell::new(0), )) @@ -408,7 +339,7 @@ wrap_resource_handler! { for (name, value) in response_data.headers() { let Ok(value) = value.to_str() else { continue; }; - if name.as_str().eq_ignore_ascii_case("content-security-policy") { + if name == CONTENT_SECURITY_POLICY { csp_header = Some(value.to_string()); } else { response.set_header_by_name(Some(&name.as_str().into()), Some(&value.into()), 0); @@ -434,7 +365,7 @@ wrap_resource_handler! { .map(|s| s.hash.clone()) .collect(); - let csp_header_name = CefString::from("Content-Security-Policy"); + let csp_header_name = CefString::from(CONTENT_SECURITY_POLICY.as_str()); let new_csp = if let Some(existing_csp) = csp_header { // Parse CSP using tauri-utils let mut csp_map: std::collections::HashMap = @@ -469,7 +400,7 @@ wrap_resource_handler! { } else if let Some(csp) = csp_header { // No scripts to inject, just copy the original CSP header response.set_header_by_name( - Some(&CefString::from("Content-Security-Policy")), + Some(&CefString::from(CONTENT_SECURITY_POLICY.as_str())), Some(&CefString::from(csp.as_str())), 0, ); diff --git a/crates/tauri-runtime-cef/src/lib.rs b/crates/tauri-runtime-cef/src/lib.rs index 2c78a9818..cb5bd040c 100644 --- a/crates/tauri-runtime-cef/src/lib.rs +++ b/crates/tauri-runtime-cef/src/lib.rs @@ -34,6 +34,7 @@ use std::{ atomic::{AtomicBool, Ordering}, Arc, Mutex, }, + thread::{self, ThreadId}, }; mod cef_impl; @@ -48,10 +49,41 @@ enum Message { pending: PendingWindow>, after_window_creation: Option>, }, + CreateWebview { + window_id: WindowId, + webview_id: u32, + pending: PendingWebview>, + }, + #[cfg(any(debug_assertions, feature = "devtools"))] + OpenDevTools { + window_id: WindowId, + webview_id: u32, + }, + #[cfg(any(debug_assertions, feature = "devtools"))] + CloseDevTools { + window_id: WindowId, + webview_id: u32, + }, UserEvent(T), Noop, } +impl fmt::Debug for Message { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Task(_) => write!(f, "Task"), + Self::CreateWindow { .. } => write!(f, "CreateWindow"), + Self::CreateWebview { .. } => write!(f, "CreateWebview"), + #[cfg(any(debug_assertions, feature = "devtools"))] + Self::OpenDevTools { .. } => write!(f, "OpenDevTools"), + #[cfg(any(debug_assertions, feature = "devtools"))] + Self::CloseDevTools { .. } => write!(f, "CloseDevTools"), + Self::UserEvent(_) => write!(f, "UserEvent"), + Self::Noop => write!(f, "Noop"), + } + } +} + impl Clone for Message { fn clone(&self) -> Self { match self { @@ -61,9 +93,16 @@ impl Clone for Message { } } -struct AppWindow { - label: String, - window: cef::Window, +pub(crate) struct BrowserViewWrapper { + pub webview_id: u32, + pub browser_view: cef::BrowserView, +} + +pub(crate) struct AppWindow { + pub label: String, + pub window: cef::Window, + pub webviews: Vec, + pub content_panel: Option, // Panel container for multiwebview (similar to Electron's contentView) } #[derive(Clone)] @@ -71,6 +110,7 @@ pub struct RuntimeContext { is_running: Arc, windows: Arc>>, main_thread_task_runner: cef::TaskRunner, + main_thread_id: ThreadId, cef_context: cef_impl::Context, event_queue: Arc>>>, } @@ -85,13 +125,20 @@ unsafe impl Sync for RuntimeContext {} impl RuntimeContext { fn post_message(&self, message: Message) -> Result<()> { - self - .main_thread_task_runner - .post_task(Some(&mut cef_impl::SendMessageTask::new( - self.cef_context.clone(), - Arc::new(RefCell::new(message)), - ))); - Ok(()) + if thread::current().id() == self.main_thread_id { + // Already on main thread, execute directly + cef_impl::handle_message(&self.cef_context, message); + Ok(()) + } else { + // Post to main thread via TaskRunner + self + .main_thread_task_runner + .post_task(Some(&mut cef_impl::SendMessageTask::new( + self.cef_context.clone(), + Arc::new(RefCell::new(message)), + ))); + Ok(()) + } } fn create_window( @@ -148,6 +195,29 @@ impl RuntimeContext { webview: detached_webview, }) } + + fn create_webview( + &self, + window_id: WindowId, + pending: PendingWebview>, + ) -> Result>> { + let label = pending.label.clone(); + let webview_id = self.cef_context.next_webview_id(); + + self.post_message(Message::CreateWebview { + window_id, + webview_id, + pending, + })?; + + let dispatcher = CefWebviewDispatcher { + window_id: Arc::new(Mutex::new(window_id)), + webview_id, + context: self.clone(), + }; + + Ok(DetachedWebview { label, dispatcher }) + } } impl fmt::Debug for RuntimeContext { @@ -201,7 +271,7 @@ impl RuntimeHandle for CefRuntimeHandle { window_id: WindowId, pending: PendingWebview, ) -> Result> { - todo!() + self.context.create_webview(window_id, pending) } /// Run a task on the main thread. @@ -538,10 +608,24 @@ impl WebviewDispatch for CefWebviewDispatcher { } #[cfg(any(debug_assertions, feature = "devtools"))] - fn open_devtools(&self) {} + fn open_devtools(&self) { + let window_id = *self.window_id.lock().unwrap(); + let webview_id = self.webview_id; + let _ = self.context.post_message(Message::OpenDevTools { + window_id, + webview_id, + }); + } #[cfg(any(debug_assertions, feature = "devtools"))] - fn close_devtools(&self) {} + fn close_devtools(&self) { + let window_id = *self.window_id.lock().unwrap(); + let webview_id = self.webview_id; + let _ = self.context.post_message(Message::CloseDevTools { + window_id, + webview_id, + }); + } #[cfg(any(debug_assertions, feature = "devtools"))] fn is_devtools_open(&self) -> Result { @@ -822,7 +906,7 @@ impl WindowDispatch for CefWindowDispatcher { &mut self, pending: PendingWebview, ) -> Result> { - todo!() + self.context.create_webview(self.window_id, pending) } fn set_resizable(&self, resizable: bool) -> Result<()> { @@ -1118,10 +1202,12 @@ impl CefRuntime { 1 ); + let main_thread_id = thread::current().id(); let context = RuntimeContext { is_running: is_running.clone(), windows: Default::default(), main_thread_task_runner: cef::task_runner_get_for_current_thread().expect("null task runner"), + main_thread_id, cef_context, event_queue, }; diff --git a/crates/tauri/src/app.rs b/crates/tauri/src/app.rs index 0ac6a5869..0d00a7067 100644 --- a/crates/tauri/src/app.rs +++ b/crates/tauri/src/app.rs @@ -1449,6 +1449,15 @@ impl Default for Builder { } } +/// Make `Cef` the default `Runtime` for `Builder` +#[cfg(feature = "cef")] +#[cfg_attr(docsrs, doc(cfg(feature = "cef")))] +impl Default for Builder { + fn default() -> Self { + Self::new() + } +} + #[cfg(not(any(feature = "wry", feature = "cef")))] #[cfg_attr(docsrs, doc(cfg(not(any(feature = "wry", feature = "cef")))))] impl Default for Builder {