Skip to main content

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}