tauri_runtime_cef/tauri_ext.rs
1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! Extension traits exposing CEF-specific APIs on [`tauri`] types.
6//!
7//! The traits are implemented for the statically typed [`CefRuntime`](crate::CefRuntime)
8//! and for the type-erased [`tauri::DynRuntime`]. With the latter, the methods fail with
9//! [`tauri_runtime::Error::RuntimeTypeMismatch`] when the application is not running on CEF.
10
11use std::sync::Arc;
12
13use tauri::{EventLoopMessage, Manager, Runtime, Webview, WebviewWindow};
14use tauri_runtime::dynamic::{DynWebviewAttributes, DynWebviewDispatcher, DynWindowOpener};
15
16use crate::{
17 CefWebviewAttributes, CefWebviewDispatcher, ChromeCommandGroup, ConsoleMessage, DevToolsProtocol,
18 FrameEvent, NewWindowOpener, RuntimeStyle,
19};
20
21type Result<T> = std::result::Result<T, tauri::Error>;
22
23fn not_cef() -> tauri::Error {
24 tauri_runtime::Error::RuntimeTypeMismatch(
25 "the application is not running on the CEF runtime".into(),
26 )
27 .into()
28}
29
30/// Webview dispatchers that may expose the underlying [`CefWebviewDispatcher`].
31pub trait AsCefWebviewDispatcher {
32 /// Returns the CEF webview dispatcher, if the runtime is CEF.
33 fn as_cef_webview_dispatcher(&self) -> Option<&CefWebviewDispatcher<EventLoopMessage>>;
34}
35
36impl AsCefWebviewDispatcher for CefWebviewDispatcher<EventLoopMessage> {
37 fn as_cef_webview_dispatcher(&self) -> Option<&CefWebviewDispatcher<EventLoopMessage>> {
38 Some(self)
39 }
40}
41
42impl AsCefWebviewDispatcher for DynWebviewDispatcher<EventLoopMessage> {
43 fn as_cef_webview_dispatcher(&self) -> Option<&CefWebviewDispatcher<EventLoopMessage>> {
44 self.downcast_ref()
45 }
46}
47
48/// Window openers that may expose the CEF [`NewWindowOpener`].
49///
50/// Lets a new window handler read the CEF popup source regardless of the runtime generic in use:
51///
52/// ```rust,no_run
53/// use tauri::{WebviewUrl, WebviewWindowBuilder, webview::NewWindowResponse};
54/// use tauri_runtime_cef::AsCefWindowOpener;
55///
56/// tauri::Builder::default()
57/// .runtime(tauri_runtime_cef::Cef::default())
58/// .setup(|app| {
59/// WebviewWindowBuilder::new(app, "main", WebviewUrl::App("index.html".into()))
60/// .on_new_window(|url, features| {
61/// if let Some(opener) = features.opener().as_cef_window_opener() {
62/// println!("{url} was opened by {:?}", opener.source_url());
63/// }
64/// NewWindowResponse::Allow
65/// })
66/// .build()?;
67/// Ok(())
68/// });
69/// ```
70pub trait AsCefWindowOpener {
71 /// Returns the CEF window opener, `None` when the opener belongs to another runtime.
72 fn as_cef_window_opener(&self) -> Option<&NewWindowOpener>;
73}
74
75impl AsCefWindowOpener for NewWindowOpener {
76 fn as_cef_window_opener(&self) -> Option<&NewWindowOpener> {
77 Some(self)
78 }
79}
80
81impl AsCefWindowOpener for DynWindowOpener {
82 fn as_cef_window_opener(&self) -> Option<&NewWindowOpener> {
83 self.downcast_ref()
84 }
85}
86
87/// Runtime webview attributes that may expose the [`CefWebviewAttributes`].
88pub trait AsCefWebviewAttributes {
89 /// Returns the CEF attributes, `None` when the attributes belong to another runtime.
90 fn as_cef_webview_attributes_mut(&mut self) -> Option<&mut CefWebviewAttributes>;
91}
92
93impl AsCefWebviewAttributes for CefWebviewAttributes {
94 fn as_cef_webview_attributes_mut(&mut self) -> Option<&mut CefWebviewAttributes> {
95 Some(self)
96 }
97}
98
99impl AsCefWebviewAttributes for DynWebviewAttributes {
100 fn as_cef_webview_attributes_mut(&mut self) -> Option<&mut CefWebviewAttributes> {
101 self.get_or_default()
102 }
103}
104
105/// Modifies the CEF attributes of a webview builder, if the builder's attributes are not of another runtime.
106fn with_cef_webview_attributes<A: AsCefWebviewAttributes>(
107 attributes: &mut A,
108 f: impl FnOnce(&mut CefWebviewAttributes),
109) {
110 match attributes.as_cef_webview_attributes_mut() {
111 Some(attributes) => f(attributes),
112 None => log::warn!(
113 "ignoring the CEF webview attributes: attributes of another runtime were already set on the webview builder"
114 ),
115 }
116}
117
118/// CEF-specific APIs of [`tauri::Webview`] and [`tauri::WebviewWindow`].
119pub trait WebviewCefExt {
120 /// Send a message to the DevTools agent. The message should be a UTF-8 encoded JSON
121 /// string following the Chrome DevTools Protocol format.
122 ///
123 /// Callers share one native request identifier space on this browser, so the
124 /// message's `id` must come from
125 /// [`allocate_devtools_message_id`](crate::allocate_devtools_message_id).
126 /// A hardcoded or self-incremented `id` can collide with a request another
127 /// caller already sent, which consumes that producer's
128 /// [`DevToolsProtocol::MethodResult`]. The runtime's own requests are issued
129 /// from a reserved range the public allocator never returns, so they cannot
130 /// be consumed this way.
131 ///
132 /// # Examples
133 ///
134 /// ```rust,no_run
135 /// use tauri::Manager;
136 /// use tauri_runtime_cef::{WebviewCefExt, allocate_devtools_message_id};
137 ///
138 /// tauri::Builder::default()
139 /// .runtime(tauri_runtime_cef::Cef::default())
140 /// .setup(|app| {
141 /// let webview = app.get_webview_window("main").unwrap();
142 /// // Enable Page domain to receive page lifecycle events
143 /// let message_id = allocate_devtools_message_id()?;
144 /// let msg = format!(r#"{{"id":{message_id},"method":"Page.enable","params":{{}}}}"#);
145 /// webview.send_dev_tools_message(msg.as_bytes())?;
146 /// Ok(())
147 /// });
148 /// ```
149 fn send_dev_tools_message(&self, message: &[u8]) -> Result<()>;
150
151 /// Register a callback to receive DevTools protocol messages. Messages include
152 /// both method results and events from the DevTools agent.
153 ///
154 /// The callback observes the whole browser, including requests the runtime and
155 /// other callers sent. Match [`DevToolsProtocol::MethodResult`] against an
156 /// identifier obtained from
157 /// [`allocate_devtools_message_id`](crate::allocate_devtools_message_id)
158 /// instead of assuming every result belongs to this observer.
159 ///
160 /// It is scoped to this webview's own native browser, so a CEF-owned popup is
161 /// a separate browser whose protocol traffic — its page content, its network
162 /// activity and its dialog messages — is never reported here; observe popups
163 /// through [`Webview::popups`](crate::Webview::popups).
164 ///
165 /// # Examples
166 ///
167 /// ```rust,no_run
168 /// use tauri::Manager;
169 /// use tauri_runtime_cef::{DevToolsProtocol, WebviewCefExt, allocate_devtools_message_id};
170 ///
171 /// tauri::Builder::default()
172 /// .runtime(tauri_runtime_cef::Cef::default())
173 /// .setup(|app| {
174 /// let webview = app.get_webview_window("main").unwrap();
175 /// let message_id = allocate_devtools_message_id()?;
176 /// webview.on_dev_tools_protocol(move |protocol| {
177 /// match protocol {
178 /// DevToolsProtocol::Message(msg) => {
179 /// if let Ok(s) = std::str::from_utf8(&msg) {
180 /// println!("DevTools message: {}", s);
181 /// }
182 /// }
183 /// DevToolsProtocol::Event { method, params } => {
184 /// println!("DevTools event: {} {:?}", method, params);
185 /// }
186 /// // Only this result answers the request sent below.
187 /// DevToolsProtocol::MethodResult { message_id: id, success, .. } if id == message_id => {
188 /// println!("Page.enable success={}", success);
189 /// }
190 /// DevToolsProtocol::MethodResult { .. } => {}
191 /// }
192 /// })?;
193 /// let msg = format!(r#"{{"id":{message_id},"method":"Page.enable","params":{{}}}}"#);
194 /// webview.send_dev_tools_message(msg.as_bytes())?;
195 /// Ok(())
196 /// });
197 /// ```
198 fn on_dev_tools_protocol<F: Fn(DevToolsProtocol) + Send + Sync + 'static>(
199 &self,
200 f: F,
201 ) -> Result<()>;
202
203 /// Executes a closure with the CEF platform webview handle, on the CEF UI thread.
204 ///
205 /// See [`crate::Webview`] for the native state it exposes, which is sampled
206 /// immediately before the closure runs and is not refreshed afterwards.
207 fn with_cef_webview<F: FnOnce(&crate::Webview) + Send + 'static>(&self, f: F) -> Result<()>;
208}
209
210impl<R: Runtime> WebviewCefExt for Webview<R>
211where
212 R::WebviewDispatcher: AsCefWebviewDispatcher,
213{
214 fn send_dev_tools_message(&self, message: &[u8]) -> Result<()> {
215 self
216 .dispatcher()
217 .as_cef_webview_dispatcher()
218 .ok_or_else(not_cef)?
219 .send_dev_tools_message(message)
220 .map_err(Into::into)
221 }
222
223 fn on_dev_tools_protocol<F: Fn(DevToolsProtocol) + Send + Sync + 'static>(
224 &self,
225 f: F,
226 ) -> Result<()> {
227 self
228 .dispatcher()
229 .as_cef_webview_dispatcher()
230 .ok_or_else(not_cef)?
231 .on_dev_tools_protocol(f)
232 .map_err(Into::into)
233 }
234
235 fn with_cef_webview<F: FnOnce(&crate::Webview) + Send + 'static>(&self, f: F) -> Result<()> {
236 if self.dispatcher().as_cef_webview_dispatcher().is_none() {
237 return Err(not_cef());
238 }
239 self.with_webview(move |webview| {
240 if let Some(webview) = webview.downcast_ref::<crate::Webview>() {
241 f(webview)
242 }
243 })
244 }
245}
246
247impl<R: Runtime> WebviewCefExt for WebviewWindow<R>
248where
249 R::WebviewDispatcher: AsCefWebviewDispatcher,
250{
251 fn send_dev_tools_message(&self, message: &[u8]) -> Result<()> {
252 self.as_ref().send_dev_tools_message(message)
253 }
254
255 fn on_dev_tools_protocol<F: Fn(DevToolsProtocol) + Send + Sync + 'static>(
256 &self,
257 f: F,
258 ) -> Result<()> {
259 self.as_ref().on_dev_tools_protocol(f)
260 }
261
262 fn with_cef_webview<F: FnOnce(&crate::Webview) + Send + 'static>(&self, f: F) -> Result<()> {
263 self.as_ref().with_cef_webview(f)
264 }
265}
266
267/// CEF-specific APIs of [`tauri::WebviewWindowBuilder`].
268pub trait WebviewWindowBuilderCefExt {
269 /// Sets the browser runtime style.
270 ///
271 /// See [`RuntimeStyle`] for more information.
272 #[must_use]
273 fn browser_runtime_style(self, style: RuntimeStyle) -> Self;
274
275 /// Observes native CEF lifecycle events for main and child frames.
276 ///
277 /// The callback runs synchronously on CEF's UI thread. It must return
278 /// promptly and must not wait for an event-loop operation. This observer
279 /// does not replace the navigation policy configured by `on_navigation`.
280 /// It is scoped to this webview's own native browser, so a CEF-owned popup
281 /// is a separate browser that is never reported here — observe popups
282 /// through [`Webview::popups`](crate::Webview::popups).
283 #[must_use]
284 fn on_frame_event<F: Fn(FrameEvent) + Send + Sync + 'static>(self, handler: F) -> Self;
285
286 /// Observes the messages the renderer writes to the JavaScript console,
287 /// without DevTools having to be open.
288 ///
289 /// The callback runs synchronously on CEF's UI thread, so it must return
290 /// promptly and must not wait for an event-loop operation. Observing a message
291 /// does not suppress it: CEF logs it as it normally would. It is scoped to this
292 /// webview's own native browser, so neither a CEF-owned popup's output nor that
293 /// of a DevTools window opened on this webview is reported here.
294 #[must_use]
295 fn on_console_message<F: Fn(ConsoleMessage) + Send + Sync + 'static>(self, handler: F) -> Self;
296
297 /// Keeps the named families of Chrome commands rather than swallowing them.
298 ///
299 /// A Chrome style browser keeps its whole accelerator table live even hosted as a
300 /// child view with no browser UI, so by default this runtime swallows the commands
301 /// that have no meaning in an app window — new window and tab, the tab strip,
302 /// history, downloads and settings, print, save page, view source, the omnibox
303 /// focus commands. Naming a [`ChromeCommandGroup`] here lets that family run the
304 /// way it would in a browser. Calling this more than once replaces the previous
305 /// list.
306 ///
307 /// DevTools and zoom accelerators are not covered here: they follow
308 /// `WebviewAttributes::devtools` and `WebviewAttributes::zoom_hotkeys_enabled`.
309 ///
310 /// ```no_run
311 /// # use tauri_runtime_cef::{AsCefWebviewAttributes, ChromeCommandGroup};
312 /// # fn f<R, M>(builder: tauri::WebviewWindowBuilder<'_, R, M>)
313 /// # where
314 /// # R: tauri::Runtime,
315 /// # M: tauri::Manager<R>,
316 /// # R::RuntimeWebviewAttributes: AsCefWebviewAttributes,
317 /// # {
318 /// use tauri_runtime_cef::WebviewWindowBuilderCefExt;
319 /// // Ctrl+P prints and Alt+Left goes back, as a user expects.
320 /// builder.allow_chrome_commands([ChromeCommandGroup::Document, ChromeCommandGroup::History]);
321 /// # }
322 /// ```
323 #[must_use]
324 fn allow_chrome_commands<I: IntoIterator<Item = ChromeCommandGroup>>(self, groups: I) -> Self;
325
326 /// Takes a last look at the CEF [`BrowserSettings`](cef::BrowserSettings) before the
327 /// browser is created.
328 ///
329 /// The runtime maps the portable `WebviewAttributes` onto these settings first, so this
330 /// can change what it decided as well as reach the fields Tauri has no attribute for:
331 /// the font families and sizes, `remote_fonts`, `local_storage`, `databases`, `webgl`,
332 /// `tab_to_links`, `javascript_dom_paste` and `default_encoding`.
333 ///
334 /// ```no_run
335 /// # use tauri_runtime_cef::AsCefWebviewAttributes;
336 /// # fn f<R, M>(builder: tauri::WebviewWindowBuilder<'_, R, M>)
337 /// # where
338 /// # R: tauri::Runtime,
339 /// # M: tauri::Manager<R>,
340 /// # R::RuntimeWebviewAttributes: AsCefWebviewAttributes,
341 /// # {
342 /// use tauri_runtime_cef::WebviewWindowBuilderCefExt;
343 /// use tauri_runtime_cef::cef::{State, sys::cef_state_t};
344 ///
345 /// // An app that ships its own fonts has no use for the ones a page asks for.
346 /// builder.with_browser_settings(|settings| {
347 /// settings.remote_fonts = State::from(cef_state_t::STATE_DISABLED);
348 /// });
349 /// # }
350 /// ```
351 #[must_use]
352 fn with_browser_settings<F: Fn(&mut cef::BrowserSettings) + Send + Sync + 'static>(
353 self,
354 callback: F,
355 ) -> Self;
356}
357
358impl<'a, R: Runtime, M: Manager<R>> WebviewWindowBuilderCefExt
359 for tauri::WebviewWindowBuilder<'a, R, M>
360where
361 R::RuntimeWebviewAttributes: AsCefWebviewAttributes,
362{
363 fn browser_runtime_style(mut self, style: RuntimeStyle) -> Self {
364 with_cef_webview_attributes(self.runtime_specific_attributes_mut(), |attributes| {
365 attributes.runtime_style = Some(style);
366 });
367 self
368 }
369
370 fn on_frame_event<F: Fn(FrameEvent) + Send + Sync + 'static>(mut self, handler: F) -> Self {
371 let handler = Arc::new(handler);
372 with_cef_webview_attributes(self.runtime_specific_attributes_mut(), |attributes| {
373 attributes.frame_event_handler = Some(handler);
374 });
375 self
376 }
377
378 fn on_console_message<F: Fn(ConsoleMessage) + Send + Sync + 'static>(
379 mut self,
380 handler: F,
381 ) -> Self {
382 let handler = Arc::new(handler);
383 with_cef_webview_attributes(self.runtime_specific_attributes_mut(), |attributes| {
384 attributes.console_message_handler = Some(handler);
385 });
386 self
387 }
388
389 fn allow_chrome_commands<I: IntoIterator<Item = ChromeCommandGroup>>(
390 mut self,
391 groups: I,
392 ) -> Self {
393 let groups = groups.into_iter().collect::<Vec<_>>();
394 with_cef_webview_attributes(self.runtime_specific_attributes_mut(), |attributes| {
395 attributes.allowed_chrome_commands = groups.clone();
396 });
397 self
398 }
399
400 fn with_browser_settings<F: Fn(&mut cef::BrowserSettings) + Send + Sync + 'static>(
401 mut self,
402 callback: F,
403 ) -> Self {
404 let callback = Arc::new(callback);
405 with_cef_webview_attributes(self.runtime_specific_attributes_mut(), |attributes| {
406 attributes.browser_settings_callback = Some(callback.clone());
407 });
408 self
409 }
410}
411
412/// CEF-specific APIs of [`tauri::webview::WebviewBuilder`].
413#[cfg(feature = "unstable")]
414pub trait WebviewBuilderCefExt {
415 /// Sets the browser runtime style.
416 ///
417 /// See [`RuntimeStyle`] for more information.
418 #[must_use]
419 fn browser_runtime_style(self, style: RuntimeStyle) -> Self;
420
421 /// Observes native CEF lifecycle events for main and child frames.
422 ///
423 /// The callback runs synchronously on CEF's UI thread. It must return
424 /// promptly and must not wait for an event-loop operation. This observer
425 /// does not replace the navigation policy configured by `on_navigation`.
426 /// It is scoped to this webview's own native browser, so a CEF-owned popup
427 /// is a separate browser that is never reported here — observe popups
428 /// through [`Webview::popups`](crate::Webview::popups).
429 #[must_use]
430 fn on_frame_event<F: Fn(FrameEvent) + Send + Sync + 'static>(self, handler: F) -> Self;
431
432 /// Observes the messages the renderer writes to the JavaScript console,
433 /// without DevTools having to be open.
434 ///
435 /// The callback runs synchronously on CEF's UI thread, so it must return
436 /// promptly and must not wait for an event-loop operation. Observing a message
437 /// does not suppress it: CEF logs it as it normally would. It is scoped to this
438 /// webview's own native browser, so neither a CEF-owned popup's output nor that
439 /// of a DevTools window opened on this webview is reported here.
440 #[must_use]
441 fn on_console_message<F: Fn(ConsoleMessage) + Send + Sync + 'static>(self, handler: F) -> Self;
442
443 /// Keeps the named families of Chrome commands rather than swallowing them.
444 ///
445 /// A Chrome style browser keeps its whole accelerator table live even hosted as a
446 /// child view with no browser UI, so by default this runtime swallows the commands
447 /// that have no meaning in an app window — new window and tab, the tab strip,
448 /// history, downloads and settings, print, save page, view source, the omnibox
449 /// focus commands. Naming a [`ChromeCommandGroup`] here lets that family run the
450 /// way it would in a browser. Calling this more than once replaces the previous
451 /// list.
452 ///
453 /// DevTools and zoom accelerators are not covered here: they follow
454 /// `WebviewAttributes::devtools` and `WebviewAttributes::zoom_hotkeys_enabled`.
455 #[must_use]
456 fn allow_chrome_commands<I: IntoIterator<Item = ChromeCommandGroup>>(self, groups: I) -> Self;
457
458 /// Takes a last look at the CEF [`BrowserSettings`](cef::BrowserSettings) before the
459 /// browser is created.
460 ///
461 /// The runtime maps the portable `WebviewAttributes` onto these settings first, so this
462 /// can change what it decided as well as reach the fields Tauri has no attribute for:
463 /// the font families and sizes, `remote_fonts`, `local_storage`, `databases`, `webgl`,
464 /// `tab_to_links`, `javascript_dom_paste` and `default_encoding`.
465 #[must_use]
466 fn with_browser_settings<F: Fn(&mut cef::BrowserSettings) + Send + Sync + 'static>(
467 self,
468 callback: F,
469 ) -> Self;
470}
471
472#[cfg(feature = "unstable")]
473impl<R: Runtime> WebviewBuilderCefExt for tauri::webview::WebviewBuilder<R>
474where
475 R::RuntimeWebviewAttributes: AsCefWebviewAttributes,
476{
477 fn browser_runtime_style(mut self, style: RuntimeStyle) -> Self {
478 with_cef_webview_attributes(self.runtime_specific_attributes_mut(), |attributes| {
479 attributes.runtime_style = Some(style);
480 });
481 self
482 }
483
484 fn on_frame_event<F: Fn(FrameEvent) + Send + Sync + 'static>(mut self, handler: F) -> Self {
485 let handler = Arc::new(handler);
486 with_cef_webview_attributes(self.runtime_specific_attributes_mut(), |attributes| {
487 attributes.frame_event_handler = Some(handler);
488 });
489 self
490 }
491
492 fn on_console_message<F: Fn(ConsoleMessage) + Send + Sync + 'static>(
493 mut self,
494 handler: F,
495 ) -> Self {
496 let handler = Arc::new(handler);
497 with_cef_webview_attributes(self.runtime_specific_attributes_mut(), |attributes| {
498 attributes.console_message_handler = Some(handler);
499 });
500 self
501 }
502
503 fn allow_chrome_commands<I: IntoIterator<Item = ChromeCommandGroup>>(
504 mut self,
505 groups: I,
506 ) -> Self {
507 let groups = groups.into_iter().collect::<Vec<_>>();
508 with_cef_webview_attributes(self.runtime_specific_attributes_mut(), |attributes| {
509 attributes.allowed_chrome_commands = groups.clone();
510 });
511 self
512 }
513
514 fn with_browser_settings<F: Fn(&mut cef::BrowserSettings) + Send + Sync + 'static>(
515 mut self,
516 callback: F,
517 ) -> Self {
518 let callback = Arc::new(callback);
519 with_cef_webview_attributes(self.runtime_specific_attributes_mut(), |attributes| {
520 attributes.browser_settings_callback = Some(callback.clone());
521 });
522 self
523 }
524}