prepare multiwebview

This commit is contained in:
Lucas Nogueira
2025-11-01 15:30:11 -03:00
parent 0f00219976
commit 9132d90e95
4 changed files with 344 additions and 129 deletions

View File

@@ -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<BrowserView>,
}
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<T: UserEvent>(context: &Context<T>, message: Message<T>) {
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<T: UserEvent> {
context: Context<T>,
@@ -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<T: UserEvent>(
context: &Context<T>,
window_id: WindowId,
webview_id: u32,
_webview_id: u32,
pending: PendingWindow<T, CefRuntime<T>>,
) {
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<T: UserEvent>(
kind: WebviewKind,
context: &Context<T>,
window_id: WindowId,
webview_id: u32,
pending: PendingWebview<T, CefRuntime<T>>,
) {
// 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<T: UserEvent>(
.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<T: UserEvent>(
);
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<T: UserEvent>(
}
}
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::<i32>(device_scale_factor);
let physical_size = bounds.size.to_physical::<u32>(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,
});
}

View File

@@ -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<InitializationScript>,
script_hashes: Vec<String>, // Pre-computed script hashes
csp_header: Option<String>, // Original CSP header from HTTP response (if any)
initialization_scripts: Vec<CefInitScript>,
processed_html: RefCell<Option<Vec<u8>>>,
output_offset: RefCell<usize>,
}
@@ -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<String, CspDirectiveSources> =
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<String, CspDirectiveSources> =
@@ -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,
);

View File

@@ -34,6 +34,7 @@ use std::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
thread::{self, ThreadId},
};
mod cef_impl;
@@ -48,10 +49,41 @@ enum Message<T: UserEvent + 'static> {
pending: PendingWindow<T, CefRuntime<T>>,
after_window_creation: Option<Box<dyn Fn(RawWindow) + Send + 'static>>,
},
CreateWebview {
window_id: WindowId,
webview_id: u32,
pending: PendingWebview<T, CefRuntime<T>>,
},
#[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<T: UserEvent> fmt::Debug for Message<T> {
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<T: UserEvent> Clone for Message<T> {
fn clone(&self) -> Self {
match self {
@@ -61,9 +93,16 @@ impl<T: UserEvent> Clone for Message<T> {
}
}
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<BrowserViewWrapper>,
pub content_panel: Option<cef::Panel>, // Panel container for multiwebview (similar to Electron's contentView)
}
#[derive(Clone)]
@@ -71,6 +110,7 @@ pub struct RuntimeContext<T: UserEvent> {
is_running: Arc<AtomicBool>,
windows: Arc<RefCell<HashMap<WindowId, AppWindow>>>,
main_thread_task_runner: cef::TaskRunner,
main_thread_id: ThreadId,
cef_context: cef_impl::Context<T>,
event_queue: Arc<RefCell<Vec<RunEvent<T>>>>,
}
@@ -85,13 +125,20 @@ unsafe impl<T: UserEvent> Sync for RuntimeContext<T> {}
impl<T: UserEvent> RuntimeContext<T> {
fn post_message(&self, message: Message<T>) -> 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<F: Fn(RawWindow) + Send + 'static>(
@@ -148,6 +195,29 @@ impl<T: UserEvent> RuntimeContext<T> {
webview: detached_webview,
})
}
fn create_webview(
&self,
window_id: WindowId,
pending: PendingWebview<T, CefRuntime<T>>,
) -> Result<DetachedWebview<T, CefRuntime<T>>> {
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<T: UserEvent> fmt::Debug for RuntimeContext<T> {
@@ -201,7 +271,7 @@ impl<T: UserEvent> RuntimeHandle<T> for CefRuntimeHandle<T> {
window_id: WindowId,
pending: PendingWebview<T, Self::Runtime>,
) -> Result<DetachedWebview<T, Self::Runtime>> {
todo!()
self.context.create_webview(window_id, pending)
}
/// Run a task on the main thread.
@@ -538,10 +608,24 @@ impl<T: UserEvent> WebviewDispatch<T> for CefWebviewDispatcher<T> {
}
#[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<bool> {
@@ -822,7 +906,7 @@ impl<T: UserEvent> WindowDispatch<T> for CefWindowDispatcher<T> {
&mut self,
pending: PendingWebview<T, Self::Runtime>,
) -> Result<DetachedWebview<T, Self::Runtime>> {
todo!()
self.context.create_webview(self.window_id, pending)
}
fn set_resizable(&self, resizable: bool) -> Result<()> {
@@ -1118,10 +1202,12 @@ impl<T: UserEvent> CefRuntime<T> {
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,
};

View File

@@ -1449,6 +1449,15 @@ impl Default for Builder<crate::Wry> {
}
}
/// Make `Cef` the default `Runtime` for `Builder`
#[cfg(feature = "cef")]
#[cfg_attr(docsrs, doc(cfg(feature = "cef")))]
impl Default for Builder<crate::Cef> {
fn default() -> Self {
Self::new()
}
}
#[cfg(not(any(feature = "wry", feature = "cef")))]
#[cfg_attr(docsrs, doc(cfg(not(any(feature = "wry", feature = "cef")))))]
impl<R: Runtime> Default for Builder<R> {