Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 11 additions & 16 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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"] }
4 changes: 2 additions & 2 deletions src/cef_impl/client/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ use crate::webview::INITIAL_LOAD_URL;

wrap_display_handler! {
pub struct TauriCefDisplayHandler {
document_title_changed_handler: Option<Arc<crate::compat::DocumentTitleChangedHandler>>,
address_changed_handler: Option<Arc<crate::compat::AddressChangedHandler>>,
document_title_changed_handler: Option<Arc<tauri_runtime::webview::DocumentTitleChangedHandler>>,
address_changed_handler: Option<Arc<tauri_runtime::webview::AddressChangedHandler>>,
}

impl DisplayHandler {
Expand Down
2 changes: 1 addition & 1 deletion src/cef_impl/client/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use cef::*;

wrap_download_handler! {
pub struct TauriCefDownloadHandler {
download_handler: Arc<crate::compat::DownloadHandler>,
download_handler: Arc<tauri_runtime::webview::DownloadHandler>,
}

impl DownloadHandler {
Expand Down
74 changes: 48 additions & 26 deletions src/cef_impl/client/life_span.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
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:
Expand Down Expand Up @@ -48,9 +52,8 @@ wrap_life_span_handler! {
proxy: WinitEventLoopProxy,
window_id: WindowId,
webview_id: u32,
webview_label: String,
context: RuntimeContext<T>,
new_window_handler: Option<Arc<crate::compat::NewWindowHandler>>,
new_window_handler: Option<Arc<tauri_runtime::webview::NewWindowHandler<T, CefRuntime<T>>>>,
initial_url: Option<String>,
}

Expand All @@ -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<Client>>,
_settings: Option<&mut BrowserSettings>,
_extra_info: Option<&mut Option<DictionaryValue>>,
_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));
Expand Down
2 changes: 1 addition & 1 deletion src/cef_impl/client/load.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use cef::*;

wrap_load_handler! {
pub struct TauriCefLoadHandler {
on_page_load_handler: Option<Arc<crate::compat::OnPageLoadHandler>>,
on_page_load_handler: Option<Arc<tauri_runtime::webview::OnPageLoadHandler>>,
}

impl LoadHandler {
Expand Down
18 changes: 9 additions & 9 deletions src/cef_impl/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use winit::event_loop::EventLoopProxy as WinitEventLoopProxy;

use crate::{
cef_impl::{ipc, request_handler},
runtime::{Message, RuntimeContext},
runtime::{CefRuntime, Message, RuntimeContext},
};

mod context_menu;
Expand Down Expand Up @@ -39,13 +39,14 @@ pub(crate) use process::TauriCefBrowserProcessHandler;

pub(crate) struct TauriCefBrowserClientHandlers<T: UserEvent> {
pub(crate) ipc_handler: Option<Arc<ipc::IpcHandler<T>>>,
pub(crate) on_page_load_handler: Option<Arc<crate::compat::OnPageLoadHandler>>,
pub(crate) on_page_load_handler: Option<Arc<tauri_runtime::webview::OnPageLoadHandler>>,
pub(crate) document_title_changed_handler:
Option<Arc<crate::compat::DocumentTitleChangedHandler>>,
pub(crate) navigation_handler: Option<Arc<crate::compat::NavigationHandler>>,
pub(crate) address_changed_handler: Option<Arc<crate::compat::AddressChangedHandler>>,
pub(crate) new_window_handler: Option<Arc<crate::compat::NewWindowHandler>>,
pub(crate) download_handler: Option<Arc<crate::compat::DownloadHandler>>,
Option<Arc<tauri_runtime::webview::DocumentTitleChangedHandler>>,
pub(crate) navigation_handler: Option<Arc<tauri_runtime::webview::NavigationHandler>>,
pub(crate) address_changed_handler: Option<Arc<tauri_runtime::webview::AddressChangedHandler>>,
pub(crate) new_window_handler:
Option<Arc<tauri_runtime::webview::NewWindowHandler<T, CefRuntime<T>>>>,
pub(crate) download_handler: Option<Arc<tauri_runtime::webview::DownloadHandler>>,
pub(crate) web_content_process_terminate_handler: Option<Arc<dyn Fn() + Send>>,
}

Expand Down Expand Up @@ -106,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(),
Expand Down Expand Up @@ -143,7 +143,7 @@ wrap_client! {
}

fn permission_handler(&self) -> Option<PermissionHandler> {
Some(TauriCefPermissionHandler::new(self.label.clone()))
Some(TauriCefPermissionHandler::new())
}

fn on_process_message_received(
Expand Down
81 changes: 21 additions & 60 deletions src/cef_impl/client/permission.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
41 changes: 10 additions & 31 deletions src/cef_impl/ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,38 +152,17 @@ pub(crate) fn on_process_message_received<T: UserEvent>(
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(),
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(),
},
},
};
// 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}");
}
request,
);
}
1
}
Loading