Skip to main content

tauri_runtime_cef/
webview.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5use std::collections::HashMap;
6use std::sync::Arc;
7use std::sync::{
8  Mutex,
9  atomic::{AtomicBool, Ordering},
10  mpsc::{self, Receiver, Sender},
11};
12
13use cef::*;
14use sha2::{Digest, Sha256};
15use tauri_runtime::{
16  Cookie, Error, Result, Runtime, UserEvent, WebviewDispatch, WebviewEventId,
17  dpi::{PhysicalPosition, PhysicalSize, Position, Rect, Size},
18  webview::{
19    DetachedWebview, InitializationScript, PendingWebview, UriSchemeProtocolHandler,
20    WebviewAttributes,
21  },
22  window::{WebviewEvent, WindowId},
23};
24use tauri_utils::{
25  Theme,
26  config::{BackgroundThrottlingPolicy, Color},
27  html::normalize_script_for_csp,
28};
29use url::Url;
30
31use crate::cef_impl::{client as browser_client, cookie, request_context, request_handler};
32use crate::runtime::{CefRuntime, Message, RuntimeContext, WinitCefApp};
33use crate::window::AppWindow;
34
35/// A handle to the native CEF browser backing a Tauri webview.
36///
37/// This is the runtime-specific webview object exposed through
38/// [`tauri_runtime::WebviewDispatch::with_webview`].
39#[derive(Clone)]
40pub struct Webview {
41  browser: cef::Browser,
42  snapshot: WebviewSnapshot,
43  frame_navigation_state: crate::FrameNavigationState,
44  popups: Vec<Webview>,
45  opener: Option<crate::FrameNavigationState>,
46}
47
48/// Native state sampled on the CEF UI thread immediately before a
49/// `with_webview` callback. This does not assert renderer responsiveness or
50/// that the view is unobscured on screen.
51#[derive(Clone, Debug)]
52#[non_exhaustive]
53pub struct WebviewSnapshot {
54  /// Native browser identity, distinct for each popup.
55  pub browser_id: i32,
56  /// Native JavaScript dialog observation. Unknown is distinct from absent.
57  pub dialogs: crate::NativeDialogObservation,
58  /// All-frame document generation validated against the current native frame
59  /// identities and load state. `None` means document admission is unavailable.
60  pub document: Option<crate::NativeDocumentToken>,
61  /// Runtime window label; CEF-owned popups have no Tauri window label.
62  pub window_label: Option<String>,
63  /// Opaque lifetime of the runtime window. Labels and native handle values
64  /// may be reused after teardown; this token distinguishes their replacements.
65  /// `None` means the runtime could not observe a native window lifetime.
66  pub window: Option<crate::NativeWindowToken>,
67  /// Whether the actual native parent matches the observed native window.
68  /// `None` means the platform could not establish the relationship. A
69  /// CEF-owned popup always reports `None`, permanently rather than
70  /// transiently: CEF owns its native window, so there is no independently
71  /// observed parent for the runtime to check it against. Offscreen webviews
72  /// also report `None` because there is no native browser child.
73  pub parent_matches: Option<bool>,
74  /// Current bounds relative to the native parent, in the indicated DPI units.
75  /// Offscreen bounds are the requested layout, in logical pixels.
76  pub bounds: Option<Rect>,
77  /// Native view visibility. `None` means native inspection was unavailable.
78  /// Visibility is separate from occlusion, minimization, and page lifecycle.
79  /// Offscreen webviews report their requested visibility.
80  pub visible: Option<bool>,
81}
82
83impl Webview {
84  pub(crate) fn new(
85    browser: cef::Browser,
86    snapshot: WebviewSnapshot,
87    frame_navigation_state: crate::FrameNavigationState,
88  ) -> Self {
89    Self {
90      browser,
91      snapshot,
92      frame_navigation_state,
93      popups: Vec::new(),
94      opener: None,
95    }
96  }
97
98  /// Returns the native state sampled for this `with_webview` callback.
99  /// Retaining the handle does not refresh this observation.
100  pub fn snapshot(&self) -> &WebviewSnapshot {
101    &self.snapshot
102  }
103
104  /// Returns read-only live navigation state for this native browser lifetime.
105  /// Unlike `snapshot`, this handle follows subsequent native frame events.
106  pub fn frame_navigation_state(&self) -> &crate::FrameNavigationState {
107    &self.frame_navigation_state
108  }
109
110  pub(crate) fn set_opener(&mut self, opener: crate::FrameNavigationState) {
111    self.opener = Some(opener);
112  }
113
114  /// Native CEF-owned popup descendants sampled in this same UI-thread callback.
115  /// Popup windows have no Tauri label and retain their actual CEF opener.
116  pub fn popups(&self) -> &[Webview] {
117    &self.popups
118  }
119
120  /// Exact native opener lifetime, if this is a CEF-owned popup.
121  pub fn opener(&self) -> Option<&crate::FrameNavigationState> {
122    self.opener.as_ref()
123  }
124
125  /// Select an observed document within this runtime-owned browser family.
126  /// The returned snapshot is valid only for the current native callback.
127  pub fn for_document(&self, document: &crate::NativeDocumentToken) -> Option<&Webview> {
128    std::iter::once(self)
129      .chain(self.popups.iter())
130      .find(|view| view.snapshot.document.as_ref() == Some(document))
131  }
132
133  /// Returns the [`cef::Browser`] backing this webview.
134  ///
135  /// From the browser you can reach the rest of the CEF API, such as the
136  /// browser host, the main frame or the native window handle.
137  pub fn browser(&self) -> cef::Browser {
138    self.browser.clone()
139  }
140}
141
142pub fn webview_version() -> tauri_runtime::Result<String> {
143  Ok(crate::downgrade::ChromiumVersion::embedded().to_string())
144}
145
146#[inline]
147fn color_to_argb(color: Color) -> u32 {
148  let (r, g, b, a) = color.into();
149  ((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)
150}
151
152/// Maps the subset of [`WebviewAttributes`] that CEF's `BrowserSettings`
153/// supports.
154///
155/// The following Tauri webview attributes have no per-webview equivalent in CEF
156/// and are intentionally ignored here; [`warn_about_unsupported_attributes`] says
157/// so when a webview sets one to something the runtime cannot honour:
158/// - `additional_browser_args`: a WebView2 environment option. Chromium's command
159///   line is per process, so `Cef::command_line_arg` is the API for it.
160/// - `scroll_bar_style`: Chromium's overlay scrollbars are a process-wide
161///   feature (`OverlayScrollbar`), reachable through `Cef::enable_features`.
162/// - `background_throttling`: Chromium throttles hidden pages process-wide;
163///   `--disable-background-timer-throttling` turns that off for every webview.
164/// - `transparent` on a windowed browser: a background alpha of 0 falls back to
165///   `CefSettings.background_color`; transparent painting exists only in
166///   off-screen rendering, where `offscreen` maps it below.
167/// - `accept_first_mouse`: Chromium's own view decides whether the click that
168///   activates a window reaches the page.
169/// - `browser_extensions_enabled`, `extensions_path`: CEF removed its extension
170///   loading API along with the Alloy runtime.
171/// - `general_autofill_enabled`: already `false` in effect. The runtime turns
172///   `autofill.profile_enabled` off on every profile (see `preferences`), and
173///   `Cef::profile_preference` is how an application turns it back on.
174/// - `allow_link_preview`: Chromium has no link previews, so there is nothing
175///   to allow or forbid.
176///
177/// `data_store_identifier` *is* served, as its own request context cache path,
178/// the same way `data_directory` is (see `request_context`).
179///
180/// `proxy_url` is handled separately via the request context preference,
181/// `zoom_hotkeys_enabled` through the client's command handler, because zoom
182/// reaches a browser through Chromium's accelerator table rather than through a
183/// browser setting, and `user_agent` through the DevTools protocol (see
184/// [`apply_user_agent_override`]), because `CefSettings.user_agent` is fixed for
185/// the whole process before any webview is created.
186///
187/// An off-screen browser reads a background alpha of 0 as transparent painting
188/// rather than as "unset", so it gets an opaque white background unless the
189/// webview asks to be transparent or names a color of its own.
190fn browser_settings_from_webview_attributes(
191  webview_attributes: &WebviewAttributes,
192  offscreen: bool,
193) -> cef::BrowserSettings {
194  cef::BrowserSettings {
195    javascript: cef::State::from(if webview_attributes.javascript_disabled {
196      cef::sys::cef_state_t::STATE_DISABLED
197    } else {
198      cef::sys::cef_state_t::STATE_ENABLED
199    }),
200    javascript_access_clipboard: cef::State::from(if webview_attributes.clipboard {
201      cef::sys::cef_state_t::STATE_ENABLED
202    } else {
203      cef::sys::cef_state_t::STATE_DISABLED
204    }),
205    background_color: match webview_attributes.background_color {
206      Some(color) => color_to_argb(color),
207      None if offscreen && !webview_attributes.transparent => 0xFFFF_FFFF,
208      None => 0,
209    },
210    // Browser chrome a Tauri window has no business showing: the status bubble is
211    // the link target that slides in over the bottom-left of the page on hover,
212    // and the zoom bubble the popup Chrome anchors to its (absent) toolbar on
213    // Ctrl+Plus. Both draw over the app's own UI; both are ignored under Alloy
214    // style.
215    chrome_status_bubble: cef::State::from(cef::sys::cef_state_t::STATE_DISABLED),
216    chrome_zoom_bubble: cef::State::from(cef::sys::cef_state_t::STATE_DISABLED),
217    ..Default::default()
218  }
219}
220
221/// Warns about the attributes of a webview that the CEF runtime cannot honour.
222///
223/// Only a value the runtime would have to act on is reported, so a configuration that
224/// leaves an attribute at its default stays quiet. Each message names the runtime-wide
225/// API that does the same thing where one exists: an application ported from WebView2 or
226/// WKWebView may rely on the attribute, and silently dropping it is the worst outcome.
227fn warn_about_unsupported_attributes(label: &str, attributes: &WebviewAttributes, offscreen: bool) {
228  if attributes.additional_browser_args.is_some() {
229    log::warn!(
230      "webview {label:?} sets additional_browser_args, which the CEF runtime does not support: \
231       Chromium's command line is per process, not per webview. Use `Cef::command_line_arg` \
232       to pass switches to the browser process."
233    );
234  }
235  if attributes.transparent && !offscreen {
236    log::warn!(
237      "webview {label:?} asks to be transparent, which the CEF runtime does not support: a \
238       windowed Chromium browser paints an opaque background, and transparent painting exists \
239       only in off-screen rendering."
240    );
241  }
242  if attributes.accept_first_mouse {
243    log::warn!(
244      "webview {label:?} sets accept_first_mouse, which the CEF runtime does not support: \
245       Chromium's own view decides whether the click that activates a window reaches the page."
246    );
247  }
248  if attributes.browser_extensions_enabled || attributes.extensions_path.is_some() {
249    log::warn!(
250      "webview {label:?} asks for browser extensions, which the CEF runtime does not support: \
251       CEF removed its extension loading API."
252    );
253  }
254  if let Some(policy) = &attributes.background_throttling
255    && !matches!(policy, BackgroundThrottlingPolicy::Throttle)
256  {
257    log::warn!(
258      "webview {label:?} sets a background throttling policy, which the CEF runtime cannot \
259       apply per webview: Chromium throttles hidden pages process-wide (its default is the \
260       `throttle` policy). Pass `--disable-background-timer-throttling` through \
261       `Cef::command_line_arg` to turn it off for every webview."
262    );
263  }
264  // The overlay variant only exists on Windows; elsewhere the configuration maps to
265  // `Default` before it reaches the runtime.
266  #[cfg(windows)]
267  if !matches!(
268    attributes.scroll_bar_style,
269    tauri_runtime::webview::ScrollBarStyle::Default
270  ) {
271    log::warn!(
272      "webview {label:?} asks for overlay scrollbars, which the CEF runtime cannot apply per \
273       webview: Chromium's overlay scrollbars are a process-wide feature. Enable it for every \
274       webview with `Cef::enable_features([\"OverlayScrollbar\"])`."
275    );
276  }
277}
278
279/// A Chrome DevTools Protocol notification observed on a native browser.
280///
281/// Observers see the whole browser, including requests issued by the runtime
282/// itself and by other callers, so nothing here is scoped to one producer.
283/// No notification names a browser, and none has to: an observer is registered
284/// on one native browser and never receives another's traffic — a CEF-owned
285/// popup is a separate browser, observed only by the runtime's own internal
286/// observer.
287#[derive(Debug, Clone)]
288pub enum DevToolsProtocol {
289  /// The raw agent message, before it is classified as an event or a result.
290  Message(Vec<u8>),
291  /// An agent event. Events are unsolicited and carry no request identifier.
292  Event { method: String, params: Vec<u8> },
293  /// The result of one request.
294  ///
295  /// `message_id` correlates with the `id` of the request that produced it.
296  /// Compare it against an identifier obtained from
297  /// [`allocate_devtools_message_id`](crate::allocate_devtools_message_id);
298  /// a result whose identifier you did not allocate answers someone else's
299  /// request. Numeric correlation does not authorize a browser or document.
300  MethodResult {
301    message_id: i32,
302    success: bool,
303    result: Vec<u8>,
304  },
305}
306
307pub(crate) type DevToolsProtocolHandler = dyn Fn(DevToolsProtocol) + Send + Sync;
308
309/// A family of Chrome commands the runtime swallows in an application window.
310///
311/// A Chrome style browser keeps its whole accelerator table live even when it is hosted
312/// as a child view with no browser UI, so Ctrl+N opens a real Chrome window next to the
313/// app's and Ctrl+P prints the app's own markup. The runtime blocks the families below by
314/// default; naming one in
315/// [`allow_chrome_commands`](crate::WebviewWindowBuilderCefExt::allow_chrome_commands)
316/// lets that family run the way it would in a browser.
317///
318/// DevTools and zoom are not here: they follow `WebviewAttributes::devtools` and
319/// `WebviewAttributes::zoom_hotkeys_enabled`.
320#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
321#[non_exhaustive]
322pub enum ChromeCommandGroup {
323  /// Ctrl+N, Ctrl+Shift+N, Ctrl+T and the whole tab strip: new window, new incognito
324  /// window, new tab, duplicate, restore, reorder, and select tab 1-8.
325  ///
326  /// An app window has no tab strip for these to act on, and the windows they open are
327  /// real Chrome windows the application does not own.
328  WindowAndTab,
329  /// Ctrl+P, Ctrl+S, Ctrl+U, Ctrl+O: print, print without preview, save page, view
330  /// source, open file, and the PWA install and shortcut commands.
331  ///
332  /// The commonest group to want back — Ctrl+P is a keystroke users expect. Note that
333  /// `WebviewDispatch::print` prints on request without this, and that `IDC_OPEN_FILE`
334  /// and `IDC_SAVE_PAGE` raise OS file dialogs.
335  Document,
336  /// Ctrl+L and its neighbours: focus the omnibox, the search box, the toolbar, the menu
337  /// bar or the bookmarks bar, plus Home and open-current-URL.
338  ///
339  /// An app window has none of that chrome, so these can only move keyboard focus
340  /// somewhere the user cannot see; Home and open-current-URL additionally navigate the
341  /// webview away from the app's own UI.
342  BrowserChrome,
343  /// Ctrl+H, Ctrl+J, Ctrl+D, Ctrl+Shift+Delete and the rest: history, downloads,
344  /// bookmarks, settings, clear browsing data, the task manager, sign-in, about and
345  /// feedback.
346  ///
347  /// These load Chrome WebUI pages *in place of the app's UI*, in the very webview the
348  /// accelerator was pressed in, and expose the browsing data of every webview sharing
349  /// the request context.
350  BrowserSurface,
351  /// Alt+Left and Alt+Right: back and forward through the session history.
352  ///
353  /// The browser is created at an internal placeholder URL and only then navigated to the
354  /// app's own, so the app's first screen already sits on a second history entry and
355  /// going back from it lands on a blank page. `WebviewDispatch::go_back` and
356  /// `go_forward` work without this.
357  History,
358}
359
360impl ChromeCommandGroup {
361  /// Every group, which is what the runtime blocks when a webview allows none. The
362  /// blocklist is resolved from this, so a variant added here is blocked by default.
363  pub(crate) const ALL: &'static [Self] = &[
364    Self::WindowAndTab,
365    Self::Document,
366    Self::BrowserChrome,
367    Self::BrowserSurface,
368    Self::History,
369  ];
370}
371
372/// One message a renderer wrote to the JavaScript console.
373///
374/// Reported synchronously on CEF's UI thread. Observing a message neither
375/// suppresses CEF's own logging of it nor changes what DevTools shows.
376#[derive(Clone, Debug)]
377#[non_exhaustive]
378pub struct ConsoleMessage {
379  /// How severe the renderer considers the message.
380  pub level: ConsoleMessageLevel,
381  /// The message text, already formatted by the renderer the way DevTools shows
382  /// it.
383  pub message: String,
384  /// What wrote the message — a script URL, usually. Empty when CEF names none.
385  pub source: String,
386  /// The 1-based line in `source`. Zero when CEF names none.
387  pub line: i32,
388}
389
390/// The severity of a [`ConsoleMessage`].
391#[derive(Clone, Copy, Debug, PartialEq, Eq)]
392#[non_exhaustive]
393pub enum ConsoleMessageLevel {
394  /// `console.debug`.
395  Verbose,
396  /// `console.log` and `console.info`. Also CEF's default severity, which it
397  /// documents as INFO.
398  Info,
399  /// `console.warn`.
400  Warning,
401  /// `console.error`, and messages the renderer itself reports as errors, such as
402  /// an uncaught exception or a blocked subresource.
403  Error,
404  /// A fatal log severity. No console API produces one.
405  Fatal,
406  /// A severity this build of the runtime does not name.
407  Other,
408}
409
410/// Synchronous observer of renderer console output.
411pub type ConsoleMessageHandler = dyn Fn(ConsoleMessage) + Send + Sync + 'static;
412
413/// Last look at a webview's [`cef::BrowserSettings`] before its browser is created.
414pub type BrowserSettingsCallback = dyn Fn(&mut cef::BrowserSettings) + Send + Sync + 'static;
415
416impl ConsoleMessage {
417  pub(crate) fn from_cef(
418    level: cef::LogSeverity,
419    message: Option<&CefString>,
420    source: Option<&CefString>,
421    line: i32,
422  ) -> Self {
423    use cef::sys::cef_log_severity_t;
424
425    Self {
426      level: match cef_log_severity_t::from(level) {
427        cef_log_severity_t::LOGSEVERITY_VERBOSE => ConsoleMessageLevel::Verbose,
428        cef_log_severity_t::LOGSEVERITY_DEFAULT | cef_log_severity_t::LOGSEVERITY_INFO => {
429          ConsoleMessageLevel::Info
430        }
431        cef_log_severity_t::LOGSEVERITY_WARNING => ConsoleMessageLevel::Warning,
432        cef_log_severity_t::LOGSEVERITY_ERROR => ConsoleMessageLevel::Error,
433        cef_log_severity_t::LOGSEVERITY_FATAL => ConsoleMessageLevel::Fatal,
434        _ => ConsoleMessageLevel::Other,
435      },
436      message: message.map(ToString::to_string).unwrap_or_default(),
437      source: source.map(ToString::to_string).unwrap_or_default(),
438      line,
439    }
440  }
441}
442pub(crate) type WebviewEventHandler = Box<dyn Fn(&WebviewEvent) + Send>;
443pub(crate) type WebviewEventListeners = Arc<Mutex<HashMap<WebviewEventId, WebviewEventHandler>>>;
444
445pub(crate) enum WebviewMessage {
446  AddEventListener(WebviewEventId, Box<dyn Fn(&WebviewEvent) + Send>),
447  EvaluateScript(String),
448  EvaluateScriptWithCallback(String, Box<dyn Fn(String) + Send + 'static>),
449  Navigate(Url),
450  Reload,
451  GoBack,
452  CanGoBack(Sender<Result<bool>>),
453  GoForward,
454  CanGoForward(Sender<Result<bool>>),
455  Print,
456  Close,
457  Show,
458  Hide,
459  SetPosition(Position),
460  SetSize(Size),
461  SetBounds(Rect),
462  SetFocus,
463  Reparent(WindowId, Sender<Result<()>>),
464  SetAutoResize(bool),
465  SetZoom(f64),
466  SetBackgroundColor(Option<Color>),
467  ClearAllBrowsingData,
468  Url(Sender<Result<String>>),
469  Bounds(Sender<Result<Rect>>),
470  Position(Sender<Result<PhysicalPosition<i32>>>),
471  Size(Sender<Result<PhysicalSize<u32>>>),
472  WithWebview(Box<dyn FnOnce(Webview) + Send>),
473  CookiesForUrl(Url, Sender<Result<Vec<Cookie<'static>>>>),
474  Cookies(Sender<Result<Vec<Cookie<'static>>>>),
475  SetCookie(Cookie<'static>),
476  DeleteCookie(Cookie<'static>),
477  #[cfg(any(debug_assertions, feature = "devtools"))]
478  OpenDevTools,
479  #[cfg(any(debug_assertions, feature = "devtools"))]
480  CloseDevTools,
481  #[cfg(any(debug_assertions, feature = "devtools"))]
482  IsDevToolsOpen(Sender<bool>),
483  SendDevToolsMessage(Vec<u8>, Sender<Result<()>>),
484  OnDevToolsProtocol(Arc<DevToolsProtocolHandler>, Sender<Result<()>>),
485}
486
487/// A webview's bounds expressed as a fraction of its parent window, used to
488/// reposition/resize auto-resize webviews when the parent window changes size.
489#[derive(Clone, Copy)]
490pub(crate) struct BoundsRate {
491  pub(crate) x: f32,
492  pub(crate) y: f32,
493  pub(crate) width: f32,
494  pub(crate) height: f32,
495}
496
497impl Default for BoundsRate {
498  fn default() -> Self {
499    Self {
500      x: 0.,
501      y: 0.,
502      width: 1.,
503      height: 1.,
504    }
505  }
506}
507
508pub(crate) struct AppWebview {
509  pub(crate) webview_id: u32,
510  pub(crate) label: String,
511  pub(crate) browser: cef::Browser,
512  pub(crate) browser_id: i32,
513  pub(crate) frame_navigation_state: crate::FrameNavigationState,
514  pub(crate) popup_family: Arc<crate::popup::PopupFamily>,
515  pub(crate) dialogs: crate::dialog::DialogState,
516  pub(crate) host: cef::BrowserHost,
517  pub(crate) uri_scheme_protocols: Arc<HashMap<String, Arc<Box<UriSchemeProtocolHandler>>>>,
518  pub(crate) devtools_protocol_handlers: Arc<Mutex<Vec<Arc<DevToolsProtocolHandler>>>>,
519  /// Keeps the DevTools message observer registered. Dropping this unregisters the observer.
520  pub(crate) devtools_observer_registration: Arc<Mutex<Option<cef::Registration>>>,
521  /// Whether a DevTools window may be opened for this webview. The DevTools *protocol*
522  /// stays available either way — the runtime's own startup rides on it.
523  pub(crate) devtools_enabled: bool,
524  pub(crate) listeners: WebviewEventListeners,
525  pub(crate) bounds_rate: Option<BoundsRate>,
526  pub(crate) offscreen: Option<crate::OffscreenView>,
527}
528
529impl AppWebview {
530  fn resize(&self, scale: f64, x: i32, y: i32, width: i32, height: i32) {
531    if let Some(view) = &self.offscreen {
532      *view.bounds.lock().unwrap_or_else(|e| e.into_inner()) = Rect {
533        position: tauri_runtime::dpi::LogicalPosition::new(x as f64 / scale, y as f64 / scale)
534          .into(),
535        size: tauri_runtime::dpi::LogicalSize::new(
536          width.max(0) as f64 / scale,
537          height.max(0) as f64 / scale,
538        )
539        .into(),
540      };
541    } else {
542      self.apply_physical_bounds(scale, x, y, width, height);
543    }
544  }
545
546  pub(crate) fn set_bounds(&mut self, parent_size: PhysicalSize<u32>, scale: f64, bounds: Rect) {
547    let position = bounds.position.to_physical::<i32>(scale);
548    let size = bounds.size.to_physical::<u32>(scale);
549
550    let x = position.x;
551    let y = position.y;
552    let w = size.width as i32;
553    let h = size.height as i32;
554
555    if self.bounds_rate.is_some() {
556      let win_w = parent_size.width.max(1) as f32;
557      let win_h = parent_size.height.max(1) as f32;
558      self.bounds_rate = Some(BoundsRate {
559        x: x as f32 / win_w,
560        y: y as f32 / win_h,
561        width: w as f32 / win_w,
562        height: h as f32 / win_h,
563      });
564    }
565
566    self.host.notify_move_or_resize_started();
567    self.resize(scale, x, y, w, h);
568    self.host.was_resized();
569  }
570
571  pub(crate) fn set_visible(&self, visible: bool) {
572    self.host.was_hidden(if visible { 0 } else { 1 });
573    if let Some(view) = &self.offscreen {
574      view.visible.store(visible, Ordering::Relaxed);
575      view.request_redraw();
576    } else {
577      self.apply_visible(visible);
578    }
579  }
580
581  pub fn url(&self) -> Option<String> {
582    self
583      .browser
584      .main_frame()
585      .map(|frame| cef::CefString::from(&frame.url()).to_string())
586  }
587}
588
589impl<T: UserEvent> WinitCefApp<T> {
590  pub(crate) fn create_webview(
591    &mut self,
592    window_id: WindowId,
593    webview_id: u32,
594    pending: PendingWebview<T, CefRuntime<T>>,
595  ) -> Result<()> {
596    let Self {
597      context,
598      scheme_registry,
599      state,
600      ..
601    } = self;
602    let Some(appwindow) = state.windows.get_mut(&window_id) else {
603      return Err(Error::CreateWebview(
604        format!("window {window_id:?} does not exist").into(),
605      ));
606    };
607    Self::build_and_attach_webview(
608      context,
609      scheme_registry,
610      &mut state.live_browsers,
611      appwindow,
612      webview_id,
613      browser_client::DragDropEventTarget::Webview,
614      pending,
615    )
616  }
617
618  /// Builds a webview and attaches it to `appwindow`, bumping `live_browsers`
619  /// and relaying it out. Works whether `appwindow` already lives in `state` or
620  /// is still being assembled, so window and child creation share one path.
621  pub(crate) fn build_and_attach_webview(
622    context: &RuntimeContext<T>,
623    scheme_registry: &request_handler::SchemeRegistry,
624    live_browsers: &mut usize,
625    appwindow: &mut AppWindow,
626    webview_id: u32,
627    drag_drop_event_target: browser_client::DragDropEventTarget,
628    pending: PendingWebview<T, CefRuntime<T>>,
629  ) -> Result<()> {
630    // Windows/macOS use the native window/view; Linux uses the GTK content-area
631    // X11 host so CEF children cannot cover GTK UI like menus.
632    let offscreen = pending
633      .runtime_specific_attributes
634      .offscreen
635      .as_ref()
636      .map(|_| {
637        let scale = appwindow.window.scale_factor();
638        let bounds = pending
639          .webview_attributes
640          .bounds
641          .unwrap_or_else(|| Rect {
642            position: PhysicalPosition::new(0, 0).into(),
643            size: appwindow.safe_surface_size().into(),
644          })
645          .to_logical::<f64, f64>(scale);
646        crate::OffscreenView {
647          window: Arc::downgrade(&appwindow.window),
648          bounds: Arc::new(Mutex::new(Rect {
649            position: bounds.position.into(),
650            size: bounds.size.into(),
651          })),
652          visible: Arc::new(AtomicBool::new(true)),
653        }
654      });
655    let parent = appwindow.cef_host_handle();
656    let parent_size = appwindow.safe_surface_size();
657    let scale = appwindow.window.scale_factor();
658    let app_wide_theme = *context.app_wide_theme.lock().unwrap();
659    let theme = appwindow.resolved_theme(app_wide_theme);
660    let Some(child) = Self::build_browser_child(
661      context,
662      scheme_registry,
663      appwindow.id,
664      webview_id,
665      parent,
666      parent_size,
667      scale,
668      theme,
669      drag_drop_event_target,
670      offscreen,
671      pending,
672    ) else {
673      return Err(Error::CreateWebview(
674        "failed to create CEF browser".to_string().into(),
675      ));
676    };
677
678    // On Windows a window's webviews are sibling child HWNDs. Put each new one
679    // on top of the ones already there — the order they were created in — and
680    // pin it, so Chromium's focus raise cannot reshuffle them behind our back
681    // and bury an overlay webview under the one that fills the window.
682    #[cfg(windows)]
683    if child.offscreen.is_none() {
684      child.raise_to_top();
685    } else {
686      appwindow.background_surface = None;
687    }
688
689    *live_browsers += 1;
690    appwindow.children.push(child);
691    layout_app_window(appwindow);
692    Ok(())
693  }
694
695  pub(crate) fn build_browser_child(
696    context: &RuntimeContext<T>,
697    scheme_registry: &request_handler::SchemeRegistry,
698    window_id: WindowId,
699    webview_id: u32,
700    parent: cef::sys::cef_window_handle_t,
701    parent_size: PhysicalSize<u32>,
702    scale: f64,
703    theme: Option<Theme>,
704    drag_drop_event_target: browser_client::DragDropEventTarget,
705    offscreen: Option<crate::OffscreenView>,
706    mut pending: PendingWebview<T, CefRuntime<T>>,
707  ) -> Option<AppWebview> {
708    let bounds_rate = compute_child_bounds_rate(
709      pending.webview_attributes.bounds.as_ref(),
710      pending.webview_attributes.auto_resize,
711      parent_size,
712      scale,
713    );
714    let devtools_enabled = context.devtools_allowed
715      && (cfg!(debug_assertions) || cfg!(feature = "devtools"))
716      && pending.webview_attributes.devtools.unwrap_or(true);
717
718    // Alloy style keeps none of Chrome's accelerator table, so the DevTools chord has
719    // to be scripted the way it is for every webview `tauri-runtime-wry` drives. A
720    // Chrome style browser must not get the script: it dispatches `IDC_DEV_TOOLS` for
721    // the same chord, and with both in place the toggle closes the window the
722    // accelerator just opened.
723    #[cfg(any(debug_assertions, feature = "devtools"))]
724    if devtools_enabled
725      && (offscreen.is_some() || is_alloy_style(pending.runtime_specific_attributes.runtime_style))
726    {
727      pending.webview_attributes.initialization_scripts.push(
728        tauri_runtime::webview::InitializationScript {
729          script: tauri_runtime::webview::devtools_shortcut_script(),
730          for_main_frame_only: true,
731        },
732      );
733    }
734
735    let initialization_scripts = initialization_scripts(&mut pending.webview_attributes);
736    let uri_scheme_protocols: Arc<HashMap<_, _>> = Arc::new(
737      pending
738        .uri_scheme_protocols
739        .into_iter()
740        .map(|(scheme, handler)| (scheme, Arc::new(handler)))
741        .collect(),
742    );
743    let on_page_load_handler = pending.on_page_load_handler.take().map(Arc::from);
744    let document_title_changed_handler =
745      pending.document_title_changed_handler.take().map(Arc::from);
746    let zoom_hotkeys_enabled = pending.webview_attributes.zoom_hotkeys_enabled;
747    let allowed_chrome_commands = pending
748      .runtime_specific_attributes
749      .allowed_chrome_commands
750      .clone();
751    let drag_drop_handler_enabled = pending.webview_attributes.drag_drop_handler_enabled;
752    let drag_drop_state = Arc::new(Mutex::new(browser_client::DragDropState::default()));
753    let web_content_process_terminate_handler = pending
754      .on_web_content_process_terminate_handler
755      .take()
756      .map(Arc::from);
757    let frame_navigation_state = crate::FrameNavigationState::new();
758    let popup_family = Arc::new(crate::popup::PopupFamily::new(
759      frame_navigation_state.clone(),
760    ));
761    let dialogs = crate::dialog::DialogState::new(frame_navigation_state.clone());
762    let frame_state_for_events = frame_navigation_state.clone();
763    let frame_event_handler = pending
764      .runtime_specific_attributes
765      .frame_event_handler
766      .clone();
767    let handlers = browser_client::TauriCefBrowserClientHandlers {
768      // The internal navigation observer must see every notification this client
769      // receives; the app observer is bound to this exact native browser. CEF can
770      // route a browser this webview does not own through the same client — a
771      // DevTools window is the standing case — and a `FrameEvent` carries the full
772      // URL, so those must never reach an observer registered for this webview.
773      frame_event_handler: Some(Arc::new(move |event| {
774        frame_state_for_events.on_frame_event(&event);
775        if frame_state_for_events.has_browser_id(event.browser_id)
776          && let Some(handler) = &frame_event_handler
777        {
778          handler(event);
779        }
780      })),
781      ipc_handler: pending.ipc_handler.map(Arc::from),
782      on_page_load_handler,
783      document_title_changed_handler,
784      navigation_handler: pending.navigation_handler.map(Arc::from),
785      new_window_handler: pending.new_window_handler.map(Arc::from),
786      download_handler: pending.download_handler.take(),
787      console_message_handler: pending
788        .runtime_specific_attributes
789        .console_message_handler
790        .clone(),
791      permission_request_handler: pending.permission_request_handler.take().map(Arc::from),
792      web_content_process_terminate_handler,
793    };
794
795    let mut client =
796      browser_client::TauriCefBrowserClient::build(browser_client::TauriCefBrowserClientArgs {
797        context: context.clone(),
798        window_id,
799        webview_id,
800        label: pending.label.clone(),
801        initial_url: Some(pending.url.as_str().to_string()),
802        devtools_enabled,
803        zoom_hotkeys_enabled,
804        allowed_chrome_commands,
805        drag_drop_event_target,
806        drag_drop_handler_enabled,
807        drag_drop_state,
808        frame_navigation_state: frame_navigation_state.clone(),
809        popup_family: Arc::downgrade(&popup_family),
810        opener: None,
811        render_handler: offscreen
812          .as_ref()
813          .zip(pending.runtime_specific_attributes.offscreen.as_ref())
814          .map(|(view, options)| (options.render_handler)(view.clone())),
815        cursor_handler: pending
816          .runtime_specific_attributes
817          .offscreen
818          .as_ref()
819          .and_then(|options| options.cursor_handler.clone()),
820        handlers,
821        sender: context.sender.clone(),
822      });
823
824    // If the bounds are not specified, default to the parent window's size and position.
825    // aka full-window webview.
826    let bounds = pending.webview_attributes.bounds.unwrap_or_else(|| Rect {
827      position: PhysicalPosition::new(0, 0).into(),
828      size: parent_size.into(),
829    });
830    #[cfg(not(target_os = "macos"))]
831    let bounds = bounds.to_physical::<i32, i32>(scale);
832    #[cfg(target_os = "macos")]
833    let bounds = bounds.to_logical::<i32, i32>(scale);
834    let bounds = cef::Rect {
835      x: bounds.position.x,
836      y: bounds.position.y,
837      width: bounds.size.width,
838      height: bounds.size.height,
839    };
840
841    // Let CEF pick the runtime style unless overridden per-webview.
842    let cef_runtime_style = match pending.runtime_specific_attributes.runtime_style {
843      Some(RuntimeStyle::Alloy) => cef::RuntimeStyle::ALLOY,
844      Some(RuntimeStyle::Chrome) => cef::RuntimeStyle::CHROME,
845      None => cef::RuntimeStyle::DEFAULT,
846    };
847
848    let window_info = if let Some(options) = &pending.runtime_specific_attributes.offscreen {
849      options.window_info(parent)
850    } else {
851      let mut info = cef::WindowInfo::default().set_as_child(parent, &bounds);
852      info.runtime_style = cef_runtime_style;
853      info
854    };
855    let mut settings =
856      browser_settings_from_webview_attributes(&pending.webview_attributes, offscreen.is_some());
857    // Applied last so an application can override what the runtime mapped.
858    if let Some(callback) = &pending
859      .runtime_specific_attributes
860      .browser_settings_callback
861    {
862      callback(&mut settings);
863    }
864    let settings = settings;
865    // CEF has no per-browser user agent — `CefSettings.user_agent` is fixed for the whole
866    // process before any browser exists — so the per-webview attribute is served through
867    // the DevTools protocol instead, which overrides both the header and
868    // `navigator.userAgent` for this one target.
869    let user_agent = pending.webview_attributes.user_agent.clone();
870
871    warn_about_unsupported_attributes(
872      &pending.label,
873      &pending.webview_attributes,
874      offscreen.is_some(),
875    );
876
877    let custom_protocol_scheme = if pending.webview_attributes.use_https_scheme {
878      "https"
879    } else {
880      "http"
881    }
882    .to_string();
883    let custom_scheme_domain_names: Vec<String> = uri_scheme_protocols
884      .keys()
885      .map(|scheme| format!("{scheme}.localhost"))
886      .collect();
887    let real_initial_url = pending.url.as_str().to_string();
888    let (browser_tx, browser_rx) = mpsc::channel();
889    let (init_done, on_initialized) = request_context::deferred_init_continuation({
890      let scheme_registry = scheme_registry.clone();
891      let uri_scheme_protocols = uri_scheme_protocols.clone();
892      let initialization_scripts = initialization_scripts.clone();
893      let custom_protocol_scheme = custom_protocol_scheme.clone();
894      let custom_scheme_domain_names = custom_scheme_domain_names.clone();
895      let label = pending.label.clone();
896      move |mut request_context| {
897        request_context::apply_theme_scheme(request_context.as_ref(), theme);
898
899        // Create with an inert document so the BrowserHost exists before the real
900        // navigation; the real URL is loaded once the document-start script is set.
901        let initial_url = CefString::from(INITIAL_LOAD_URL);
902        let Some(browser) = cef::browser_host_create_browser_sync(
903          Some(&window_info),
904          Some(&mut client),
905          Some(&initial_url),
906          Some(&settings),
907          None,
908          request_context.as_mut(),
909        ) else {
910          log::error!("failed to create CEF browser for webview {label:?}");
911          return;
912        };
913        let Some(host) = browser.host() else {
914          log::error!("CEF browser for webview {label:?} has no host");
915          return;
916        };
917        let browser_id = browser.identifier();
918
919        {
920          let mut registry = scheme_registry.lock().unwrap();
921          for (scheme, handler) in uri_scheme_protocols.iter() {
922            registry.insert(
923              (browser_id, scheme.clone()),
924              (
925                label.clone(),
926                handler.clone(),
927                initialization_scripts.clone(),
928              ),
929            );
930          }
931        }
932
933        // The app observers registered through `on_dev_tools_protocol` live in
934        // this list, and it belongs to this one native browser. Every CEF-owned
935        // popup registers its own protocol observer against a list of its own,
936        // so nothing registered here ever observes a popup: a
937        // `DevToolsProtocol` notification carries the page's content, its
938        // network activity and its dialog messages with no browser identity to
939        // separate them, and a popup navigates wherever its own content goes —
940        // an SSO or OAuth window is the standing case.
941        let devtools_protocol_handlers: Arc<Mutex<Vec<Arc<DevToolsProtocolHandler>>>> =
942          Arc::default();
943        let pending_initial_loads: PendingInitialLoads = Arc::new(Mutex::new(HashMap::new()));
944        let devtools_observer_registration = Arc::new(Mutex::new(add_dev_tools_observer(
945          &browser,
946          devtools_protocol_handlers.clone(),
947          pending_initial_loads.clone(),
948          dialogs.clone(),
949        )));
950        // Before the initial navigation below, so the first request already carries it.
951        if let Some(user_agent) = &user_agent {
952          apply_user_agent_override(&host, user_agent);
953        }
954        load_initial_url_after_registering_initialization_scripts(
955          &browser,
956          &initialization_scripts,
957          &custom_protocol_scheme,
958          &custom_scheme_domain_names,
959          &real_initial_url,
960          &pending_initial_loads,
961        );
962
963        browser_tx
964          .send(AppWebview {
965            webview_id,
966            label,
967            browser,
968            browser_id,
969            frame_navigation_state,
970            popup_family,
971            dialogs,
972            host,
973            uri_scheme_protocols,
974            devtools_protocol_handlers,
975            devtools_observer_registration,
976            devtools_enabled,
977            listeners: Default::default(),
978            bounds_rate,
979            offscreen,
980          })
981          .expect("failed to send initialized CEF browser");
982      }
983    });
984    let main_thread: request_handler::MainThreadDispatcher = Arc::new({
985      let context = context.clone();
986      move |task| {
987        let _ = context.send_message(Message::Task(task));
988      }
989    });
990    let request_context = request_context::request_context_from_webview_attributes(
991      &context.cache_path,
992      &pending.webview_attributes,
993      context.profile_preferences.clone(),
994      context.content_settings.clone(),
995      uri_scheme_protocols.keys(),
996      &custom_protocol_scheme,
997      scheme_registry.clone(),
998      main_thread,
999      on_initialized,
1000    );
1001    if request_context.is_none() {
1002      init_done.store(true, Ordering::SeqCst);
1003    }
1004    request_context::wait_for_deferred_init(&init_done);
1005
1006    // `None` here means browser creation failed (or the request context never
1007    // initialized); the continuation logs the reason. Soft-fail instead of
1008    // taking down the whole process.
1009    browser_rx.recv().ok()
1010  }
1011
1012  pub(crate) fn handle_webview_message(
1013    &mut self,
1014    window_id: WindowId,
1015    webview_id: u32,
1016    message: WebviewMessage,
1017  ) {
1018    // If the runtime is exiting, don't process any more messages to avoid macOS crash on exit.
1019    if self.state.exiting {
1020      return;
1021    }
1022
1023    // Window-dependent messages must read window metrics like safe_surface_size.
1024    // Route them before borrowing a child mutably so those parent reads do not
1025    // overlap with the child borrow.
1026    match message {
1027      WebviewMessage::SetBounds(_)
1028      | WebviewMessage::SetSize(_)
1029      | WebviewMessage::SetPosition(_)
1030      | WebviewMessage::Position(_)
1031      | WebviewMessage::Size(_)
1032      | WebviewMessage::SetAutoResize(_)
1033      | WebviewMessage::WithWebview(_)
1034      | WebviewMessage::Reparent(_, _) => {
1035        self.handle_window_dependent_webview_message(window_id, webview_id, message);
1036      }
1037      message => {
1038        let Some(appwindow) = self.state.windows.get_mut(&window_id) else {
1039          return;
1040        };
1041        let Some(child) = appwindow
1042          .children
1043          .iter_mut()
1044          .find(|child| child.webview_id == webview_id)
1045        else {
1046          return;
1047        };
1048
1049        Self::handle_window_independent_webview_message(child, message);
1050      }
1051    }
1052  }
1053
1054  fn handle_window_dependent_webview_message(
1055    &mut self,
1056    window_id: WindowId,
1057    webview_id: u32,
1058    message: WebviewMessage,
1059  ) {
1060    let Some(appwindow) = self.state.windows.get_mut(&window_id) else {
1061      return;
1062    };
1063    let Some(child_index) = appwindow
1064      .children
1065      .iter_mut()
1066      .position(|child| child.webview_id == webview_id)
1067    else {
1068      return;
1069    };
1070
1071    match message {
1072      WebviewMessage::SetBounds(bounds) => {
1073        let parent_size = appwindow.safe_surface_size();
1074        let scale = appwindow.window.scale_factor();
1075        let child = &mut appwindow.children[child_index];
1076        child.set_bounds(parent_size, scale, bounds);
1077      }
1078      WebviewMessage::SetSize(size) => {
1079        let parent_size = appwindow.safe_surface_size();
1080        let scale = appwindow.window.scale_factor();
1081        let child = &mut appwindow.children[child_index];
1082        let bounds = child.bounds().unwrap_or_default();
1083        let new_bounds = Rect {
1084          position: bounds.position,
1085          size,
1086        };
1087        child.set_bounds(parent_size, scale, new_bounds);
1088      }
1089      WebviewMessage::SetPosition(position) => {
1090        let parent_size = appwindow.safe_surface_size();
1091        let scale = appwindow.window.scale_factor();
1092        let child = &mut appwindow.children[child_index];
1093        let bounds = child.bounds().unwrap_or_default();
1094        let new_bounds = Rect {
1095          position,
1096          size: bounds.size,
1097        };
1098        child.set_bounds(parent_size, scale, new_bounds);
1099      }
1100      WebviewMessage::Position(tx) => {
1101        let scale = appwindow.window.scale_factor();
1102        let child = &mut appwindow.children[child_index];
1103        let bounds = child.bounds().ok_or(Error::FailedToSendMessage);
1104        let position = bounds.map(|b| b.position);
1105        let position = position.map(|p| p.to_physical::<i32>(scale));
1106        let _ = tx.send(position);
1107      }
1108      WebviewMessage::Size(tx) => {
1109        let scale = appwindow.window.scale_factor();
1110        let child = &mut appwindow.children[child_index];
1111        let bounds = child.bounds().ok_or(Error::FailedToSendMessage);
1112        let size = bounds.map(|b| b.size.to_physical::<u32>(scale));
1113        let _ = tx.send(size);
1114      }
1115      WebviewMessage::SetAutoResize(auto_resize) => {
1116        if auto_resize {
1117          let parent_size = appwindow.safe_surface_size();
1118          let scale = appwindow.window.scale_factor();
1119          let child = &mut appwindow.children[child_index];
1120          let bounds = child.bounds();
1121          child.bounds_rate = compute_child_bounds_rate(bounds.as_ref(), true, parent_size, scale);
1122        } else {
1123          let child = &mut appwindow.children[child_index];
1124          child.bounds_rate = None;
1125        }
1126      }
1127      WebviewMessage::Reparent(target_window_id, tx) => {
1128        // CEF fixes an OSR browser's native dialog parent at creation.
1129        if appwindow.children[child_index].offscreen.is_some() && window_id != target_window_id {
1130          let _ = tx.send(Err(Error::CreateWebview(
1131            "offscreen webviews cannot be reparented".into(),
1132          )));
1133          return;
1134        }
1135        if window_id == target_window_id {
1136          let _ = tx.send(Ok(()));
1137          return;
1138        }
1139
1140        if !self.state.windows.contains_key(&target_window_id) {
1141          let _ = tx.send(Err(Error::WindowNotFound));
1142          return;
1143        }
1144
1145        let Some(mut child) = self
1146          .state
1147          .windows
1148          .get_mut(&window_id)
1149          .and_then(|appwindow| {
1150            appwindow
1151              .children
1152              .iter()
1153              .position(|child| child.webview_id == webview_id)
1154              .map(|index| appwindow.children.remove(index))
1155          })
1156        else {
1157          let _ = tx.send(Err(Error::WindowNotFound));
1158          return;
1159        };
1160
1161        let Some(target_appwindow) = self.state.windows.get_mut(&target_window_id) else {
1162          let _ = tx.send(Err(Error::WindowNotFound));
1163          return;
1164        };
1165
1166        // The parent size must be the area the child is laid out into, which on Linux is the
1167        // CEF host rather than the toplevel - `layout_app_window` applies `bounds_rate` against
1168        // the same value.
1169        let parent_size = target_appwindow.safe_surface_size();
1170        let bounds = child.bounds().unwrap_or_else(|| Rect {
1171          position: PhysicalPosition::new(0, 0).into(),
1172          size: parent_size.into(),
1173        });
1174        child.reparent(target_appwindow);
1175        child.set_bounds(parent_size, target_appwindow.window.scale_factor(), bounds);
1176        // Re-parenting does not preserve z-order: a view docked back into a
1177        // window that already owns a full-window main webview must be put back
1178        // on top, or it lands behind it and renders nothing.
1179        #[cfg(windows)]
1180        child.raise_to_top();
1181
1182        target_appwindow.children.push(child);
1183        let _ = tx.send(Ok(()));
1184      }
1185      WebviewMessage::WithWebview(callback) => {
1186        let child = &appwindow.children[child_index];
1187        let document = child
1188          .frame_navigation_state
1189          .observe_document(&child.browser);
1190        let dialogs = child.dialogs.snapshot(document.as_ref());
1191        let snapshot = WebviewSnapshot {
1192          browser_id: child.browser_id,
1193          dialogs,
1194          document,
1195          window_label: Some(appwindow.label.clone()),
1196          window: Some(appwindow.lifetime.clone()),
1197          parent_matches: if child.offscreen.is_some() {
1198            None
1199          } else {
1200            child.native_parent_matches(appwindow)
1201          },
1202          bounds: child.bounds(),
1203          visible: child
1204            .offscreen
1205            .as_ref()
1206            .map(|view| view.is_visible())
1207            .or_else(|| child.native_visible()),
1208        };
1209        let mut native = Webview::new(
1210          child.browser.clone(),
1211          snapshot,
1212          child.frame_navigation_state.clone(),
1213        );
1214        native.popups = child.popup_family.observe();
1215        callback(native);
1216      }
1217      _ => unreachable!("window-independent message routed to window-dependent handler"),
1218    }
1219  }
1220
1221  fn handle_window_independent_webview_message(child: &mut AppWebview, message: WebviewMessage) {
1222    match message {
1223      WebviewMessage::EvaluateScript(script) => {
1224        if let Some(frame) = child.browser.main_frame() {
1225          let script = cef::CefString::from(script.as_str());
1226          let url = cef::CefString::from("");
1227          frame.execute_java_script(Some(&script), Some(&url), 0);
1228        }
1229      }
1230      WebviewMessage::EvaluateScriptWithCallback(script, callback) => {
1231        let host = &child.host;
1232        let Ok(message_id) = crate::devtools::allocate_runtime_devtools_message_id() else {
1233          callback(String::new());
1234          return;
1235        };
1236        let callback = Arc::new(Mutex::new(Some(callback)));
1237        let registration = Arc::new(Mutex::new(None));
1238        let mut observer = EvalScriptWithCallbackDevToolsObserver::new(
1239          message_id,
1240          callback.clone(),
1241          registration.clone(),
1242        );
1243
1244        if let Some(observer_registration) =
1245          host.add_dev_tools_message_observer(Some(&mut observer))
1246        {
1247          *registration.lock().unwrap() = Some(observer_registration);
1248
1249          let message = serde_json::json!({
1250            "id": message_id,
1251            "method": "Runtime.evaluate",
1252            "params": {
1253              "expression": script,
1254              "returnByValue": true,
1255            }
1256          })
1257          .to_string();
1258
1259          if host.send_dev_tools_message(Some(message.as_bytes())) != 1 {
1260            let _ = registration.lock().unwrap().take();
1261            if let Some(callback) = callback.lock().unwrap().take() {
1262              callback(String::new());
1263            }
1264          }
1265        } else if let Some(callback) = callback.lock().unwrap().take() {
1266          callback(String::new());
1267        }
1268      }
1269      WebviewMessage::Navigate(url) => {
1270        if let Some(frame) = child.browser.main_frame() {
1271          frame.load_url(Some(&cef::CefString::from(url.as_str())));
1272        }
1273      }
1274      WebviewMessage::Reload => child.browser.reload(),
1275      WebviewMessage::GoBack => child.browser.go_back(),
1276      WebviewMessage::CanGoBack(tx) => _ = tx.send(Ok(child.browser.can_go_back() == 1)),
1277      WebviewMessage::GoForward => child.browser.go_forward(),
1278      WebviewMessage::CanGoForward(tx) => _ = tx.send(Ok(child.browser.can_go_forward() == 1)),
1279      WebviewMessage::Close => child.host.close_browser(0),
1280      WebviewMessage::SetFocus => child.host.set_focus(1),
1281      WebviewMessage::Url(tx) => {
1282        let url = child.url().unwrap_or_default();
1283        let _ = tx.send(Ok(url));
1284      }
1285      WebviewMessage::Bounds(tx) => {
1286        let bounds = child.bounds().ok_or(Error::FailedToSendMessage);
1287        let _ = tx.send(bounds);
1288      }
1289      WebviewMessage::WithWebview(_) => {
1290        unreachable!("window-dependent message routed to window-independent handler")
1291      }
1292      WebviewMessage::Print => child.host.print(),
1293      WebviewMessage::AddEventListener(event_id, handler) => {
1294        child.listeners.lock().unwrap().insert(event_id, handler);
1295      }
1296      WebviewMessage::Show => child.set_visible(true),
1297      WebviewMessage::Hide => child.set_visible(false),
1298      WebviewMessage::SetZoom(scale_factor) => {
1299        // CEF uses a logarithmic zoom level where percentage = 1.2^level
1300        // (Chromium's kTextSizeMultiplierRatio). Convert from Tauri linear
1301        // scale factor (1.0 = 100%) to CEF's level (0.0 = 100%)
1302        const CEF_ZOOM_BASE: f64 = 1.2;
1303        let zoom_level = if scale_factor > 0.0 {
1304          scale_factor.ln() / CEF_ZOOM_BASE.ln()
1305        } else {
1306          0.0
1307        };
1308        child.host.set_zoom_level(zoom_level);
1309      }
1310      WebviewMessage::SetBackgroundColor(color) => child.set_background_color(color),
1311      WebviewMessage::ClearAllBrowsingData => {
1312        if let Some(manager) = child.cookie_manager() {
1313          manager.delete_cookies(None, None, None);
1314          manager.flush_store(None);
1315        }
1316        if let Some(request_context) = child.host.request_context() {
1317          request_context.clear_http_cache(None);
1318        }
1319      }
1320      WebviewMessage::CookiesForUrl(url, tx) => {
1321        if let Some(manager) = child.cookie_manager() {
1322          cookie::visit_url_cookies(manager, url, tx);
1323        } else {
1324          let _ = tx.send(Ok(Vec::new()));
1325        }
1326      }
1327      WebviewMessage::Cookies(tx) => {
1328        if let Some(manager) = child.cookie_manager() {
1329          cookie::visit_all_cookies(manager, tx);
1330        } else {
1331          let _ = tx.send(Ok(Vec::new()));
1332        }
1333      }
1334      WebviewMessage::SetCookie(cookie) => {
1335        if let Some(manager) = child.cookie_manager() {
1336          let url = child.url();
1337          cookie::set_cookie(manager, url, cookie);
1338        }
1339      }
1340      WebviewMessage::DeleteCookie(cookie) => {
1341        if let Some(manager) = child.cookie_manager() {
1342          let url = child.url();
1343          cookie::delete_cookie(manager, url, cookie);
1344        }
1345      }
1346      // Refused here rather than by Chromium: the preference that would have told
1347      // Chromium to refuse it also switches off the DevTools protocol this runtime
1348      // starts every webview with. See `DevToolsPolicy`.
1349      #[cfg(any(debug_assertions, feature = "devtools"))]
1350      WebviewMessage::OpenDevTools => {
1351        if child.devtools_enabled {
1352          child.host.show_dev_tools(None, None, None, None);
1353        } else {
1354          log::warn!(
1355            "not opening devtools for webview {:?}: they are disabled for this webview \
1356             or by Cef::devtools",
1357            child.label
1358          );
1359        }
1360      }
1361      #[cfg(any(debug_assertions, feature = "devtools"))]
1362      WebviewMessage::CloseDevTools => child.host.close_dev_tools(),
1363      #[cfg(any(debug_assertions, feature = "devtools"))]
1364      WebviewMessage::IsDevToolsOpen(tx) => _ = tx.send(child.host.has_dev_tools() == 1),
1365      WebviewMessage::SendDevToolsMessage(message, tx) => {
1366        let result = child.host.send_dev_tools_message(Some(&message));
1367        let _ = tx.send(if result == 1 {
1368          Ok(())
1369        } else {
1370          Err(Error::FailedToSendMessage)
1371        });
1372      }
1373      WebviewMessage::OnDevToolsProtocol(handler, tx) => {
1374        child
1375          .devtools_protocol_handlers
1376          .lock()
1377          .unwrap()
1378          .push(handler);
1379
1380        let needs_devtools_observer = child
1381          .devtools_observer_registration
1382          .lock()
1383          .unwrap()
1384          .is_none();
1385        if needs_devtools_observer {
1386          if let Some(registration) = add_dev_tools_observer(
1387            &child.browser,
1388            child.devtools_protocol_handlers.clone(),
1389            Arc::new(Mutex::new(HashMap::new())),
1390            child.dialogs.clone(),
1391          ) {
1392            *child.devtools_observer_registration.lock().unwrap() = Some(registration);
1393            let _ = tx.send(Ok(()));
1394          } else {
1395            let _ = tx.send(Err(Error::FailedToSendMessage));
1396          }
1397        } else {
1398          let _ = tx.send(Ok(()));
1399        }
1400      }
1401      WebviewMessage::SetBounds(_)
1402      | WebviewMessage::SetSize(_)
1403      | WebviewMessage::SetPosition(_)
1404      | WebviewMessage::Position(_)
1405      | WebviewMessage::Size(_)
1406      | WebviewMessage::SetAutoResize(_)
1407      | WebviewMessage::Reparent(_, _) => {
1408        unreachable!("window-dependent message routed to window-independent handler")
1409      }
1410    }
1411  }
1412}
1413
1414#[derive(Clone, Copy, Debug)]
1415pub enum RuntimeStyle {
1416  Alloy,
1417  Chrome,
1418}
1419
1420/// The CEF-specific webview attributes, set through
1421/// [`WebviewWindowBuilderCefExt`](crate::WebviewWindowBuilderCefExt).
1422///
1423/// # Permission requests on CEF
1424///
1425/// The runtime honors `WebviewAttributes::on_permission_request` — an `Allow`
1426/// grants without showing Chrome's prompt, a `Deny` refuses without one — with
1427/// three things worth knowing before relying on it.
1428///
1429/// ## Most decisions are made once per origin and then persist
1430///
1431/// Chromium consults a permission prompt only while the stored content setting
1432/// for that (origin, permission) still says "ask", and answering the prompt
1433/// persists the decision to the on-disk profile. Everything routed through the
1434/// prompt therefore reaches the handler **once per origin and permission, ever**,
1435/// including across restarts, and nothing calls back to say the app changed its
1436/// mind: revoking a grant means rewriting the content setting through the request
1437/// context.
1438///
1439/// Camera and microphone are the exception. Chromium routes *every*
1440/// `getUserMedia()` call through the media path, so those two do reach the handler
1441/// on each call and a changing answer is honored.
1442///
1443/// ## Permissions Tauri has no kind for arrive as `PermissionKind::Other`
1444///
1445/// Chromium has more request types than Tauri has kinds. Storage access, FedCM,
1446/// protocol handler registration, idle detection, local and loopback network
1447/// access, web app installation, the WebXR sessions, hand tracking, keyboard lock
1448/// and disk quota all arrive as `PermissionKind::Other`, as does any request type
1449/// a future CEF build adds.
1450///
1451/// Failing closed is deliberate, but it means a handler written elsewhere as
1452/// `match kind { Camera => Allow, _ => Deny }` hard-denies all of them here, and
1453/// denying storage access or FedCM breaks third-party SSO flows outright. Return
1454/// `PermissionResponse::Default` for the kinds you did not mean to answer about,
1455/// and CEF's own handling runs for them unchanged.
1456///
1457/// ## `PermissionKind::DisplayCapture` is never granted by an `Allow`
1458///
1459/// A `getDisplayMedia()` request the handler answers `Allow` is handed back to
1460/// CEF, which shows Chromium's desktop media picker under Chrome style and refuses
1461/// under Alloy style. A `Deny` still refuses it outright.
1462///
1463/// Granting it from the handler would grant *everything*: CEF builds the stream
1464/// from the permission mask, and a desktop video bit with no requested source
1465/// synthesises the full desktop and returns it with no picker at all. Since
1466/// `PermissionKind::DisplayCapture` names no screen, window or tab, a blanket
1467/// `.on_permission_request(|_| PermissionResponse::Allow)` would silently hand any
1468/// page in the webview a full-desktop stream.
1469///
1470/// # Chrome accelerators an app window does not get
1471///
1472/// A Chrome style browser keeps its whole accelerator table live even hosted as a
1473/// child view with no browser UI, so this runtime swallows the commands that have
1474/// no meaning in an app window (new window and tab, the tab strip, history and
1475/// downloads and settings, print, save page, view source, the omnibox focus
1476/// commands). Any family of them can be kept with
1477/// [`allow_chrome_commands`](crate::WebviewWindowBuilderCefExt::allow_chrome_commands);
1478/// see [`ChromeCommandGroup`] for what each family covers. Two of the exclusions
1479/// take away keystrokes users expect:
1480///
1481/// - **Zoom.** `WebviewAttributes::zoom_hotkeys_enabled` is honored, and it
1482///   **defaults to `false`**, so Ctrl+Plus, Ctrl+Minus and Ctrl+0 do not zoom
1483///   unless the webview opted in. Ctrl+mouse wheel zoom is unaffected either way,
1484///   since Chromium applies it in the render widget rather than through the
1485///   command controller. On Linux and macOS Tauri also injects a JavaScript zoom
1486///   polyfill when the flag is true, which coexists with Chrome's own accelerator,
1487///   so a keyboard zoom steps twice there. `WebviewDispatch::set_zoom` is
1488///   untouched.
1489///
1490/// - **History.** Alt+Left and Alt+Right do not navigate the session history.
1491///   The browser is created at an internal placeholder URL and then navigated to
1492///   the app's own, so the app's first screen already sits on a second history
1493///   entry and going back from it lands on a blank page. The page context menu
1494///   drops Back and Forward for the same reason. `WebviewDispatch::go_back` and
1495///   `go_forward` are untouched, and an app that navigates its webview normally
1496///   can take the accelerators back with [`ChromeCommandGroup::History`].
1497#[derive(Default, Clone)]
1498pub struct CefWebviewAttributes {
1499  /// Windowless rendering and application-provided CEF callbacks.
1500  pub offscreen: Option<crate::Offscreen>,
1501  /// The browser runtime style, see [`RuntimeStyle`]. CEF picks one when not set.
1502  pub runtime_style: Option<RuntimeStyle>,
1503  /// Observer of the native lifecycle events of every frame of the webview.
1504  ///
1505  /// Scoped to this webview's own native browser — its main frame and its child
1506  /// frames. Every notification carries that one
1507  /// [`browser_id`](crate::FrameEvent::browser_id). A CEF-owned popup is a
1508  /// separate browser that navigates wherever its own content goes, and a
1509  /// [`FrameEvent`](crate::FrameEvent) carries the full URL, so popups are never
1510  /// reported here. Observe them through [`Webview::popups`], whose
1511  /// [`FrameNavigationState`](crate::FrameNavigationState) follows a popup's
1512  /// native lifecycle without exposing its URLs.
1513  pub frame_event_handler: Option<Arc<crate::FrameEventHandler>>,
1514  /// Observer of the messages the renderer writes to the JavaScript console.
1515  ///
1516  /// Scoped to this webview's own native browser, so neither a CEF-owned popup's
1517  /// output nor that of a DevTools window opened on this webview is reported here.
1518  pub console_message_handler: Option<Arc<ConsoleMessageHandler>>,
1519  /// Families of Chrome commands this webview keeps rather than swallows.
1520  ///
1521  /// Empty by default, which blocks every group in [`ChromeCommandGroup`].
1522  pub allowed_chrome_commands: Vec<ChromeCommandGroup>,
1523  /// Last look at the [`cef::BrowserSettings`] before the browser is created.
1524  ///
1525  /// Runs after the runtime has mapped the portable [`WebviewAttributes`], so it can
1526  /// change what the runtime decided as well as set the fields that have no portable
1527  /// equivalent — the font families and sizes, `remote_fonts`, `local_storage`,
1528  /// `databases`, `webgl`, `tab_to_links`, `javascript_dom_paste`, `default_encoding`.
1529  pub browser_settings_callback: Option<Arc<BrowserSettingsCallback>>,
1530}
1531
1532impl std::fmt::Debug for CefWebviewAttributes {
1533  fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1534    formatter
1535      .debug_struct("CefWebviewAttributes")
1536      .field("runtime_style", &self.runtime_style)
1537      .field("offscreen", &self.offscreen.is_some())
1538      .field("frame_event_handler", &self.frame_event_handler.is_some())
1539      .field(
1540        "console_message_handler",
1541        &self.console_message_handler.is_some(),
1542      )
1543      .field("allowed_chrome_commands", &self.allowed_chrome_commands)
1544      .field(
1545        "browser_settings_callback",
1546        &self.browser_settings_callback.is_some(),
1547      )
1548      .finish()
1549  }
1550}
1551
1552#[derive(Debug, Clone)]
1553pub struct CefInitScript {
1554  pub(crate) script: String,
1555  pub(crate) hash: String,
1556  for_main_frame_only: bool,
1557}
1558
1559impl CefInitScript {
1560  /// Whether this script must run in a document loaded in the given frame.
1561  pub(crate) fn runs_in_frame(&self, is_main_frame: bool) -> bool {
1562    is_main_frame || !self.for_main_frame_only
1563  }
1564
1565  fn new(script: InitializationScript) -> Self {
1566    let mut hasher = Sha256::new();
1567    hasher.update(normalize_script_for_csp(script.script.as_bytes()));
1568    let hash = format!(
1569      "'sha256-{}'",
1570      base64::Engine::encode(
1571        &base64::engine::general_purpose::STANDARD,
1572        hasher.finalize()
1573      )
1574    );
1575    Self {
1576      script: script.script,
1577      hash,
1578      for_main_frame_only: script.for_main_frame_only,
1579    }
1580  }
1581}
1582
1583/// Whether the browser created for a webview will be Alloy style.
1584///
1585/// CEF's default is Chrome style, with one exception this runtime always meets: on
1586/// macOS a browser given a native parent view - which is how every webview here is
1587/// hosted - is forced to Alloy style whatever the application asked for, because Chrome
1588/// style does not support a native parent there (`MaybeSetWindowInfo`, upstream issue
1589/// #3294).
1590#[cfg(any(debug_assertions, feature = "devtools"))]
1591fn is_alloy_style(runtime_style: Option<RuntimeStyle>) -> bool {
1592  cfg!(target_os = "macos") || matches!(runtime_style, Some(RuntimeStyle::Alloy))
1593}
1594
1595pub(crate) fn initialization_scripts(attrs: &mut WebviewAttributes) -> Arc<Vec<CefInitScript>> {
1596  let mut initialization_scripts = Vec::new();
1597
1598  if attrs.drag_drop_handler_enabled {
1599    let drag_script = browser_client::drag_drop_initialization_script();
1600    initialization_scripts.push(CefInitScript::new(drag_script));
1601  }
1602
1603  initialization_scripts.extend(
1604    std::mem::take(&mut attrs.initialization_scripts)
1605      .into_iter()
1606      .map(CefInitScript::new),
1607  );
1608
1609  Arc::new(initialization_scripts)
1610}
1611
1612#[derive(Debug, Clone)]
1613pub struct CefWebviewDispatcher<T: UserEvent> {
1614  pub(crate) window_id: Arc<Mutex<WindowId>>,
1615  pub(crate) webview_id: u32,
1616  pub(crate) context: RuntimeContext<T>,
1617}
1618
1619impl<T: UserEvent> CefWebviewDispatcher<T> {
1620  /// Sends a UTF-8 encoded Chrome DevTools Protocol message to the DevTools agent.
1621  ///
1622  /// The message's `id` must come from
1623  /// [`allocate_devtools_message_id`](crate::allocate_devtools_message_id), the
1624  /// allocator every caller on this browser shares. A hardcoded or
1625  /// self-incremented `id` can consume another caller's
1626  /// [`DevToolsProtocol::MethodResult`]. The runtime's own requests use
1627  /// identifiers reserved above that allocator's range, so they cannot be
1628  /// answered by a caller's message.
1629  pub fn send_dev_tools_message(&self, message: &[u8]) -> Result<()> {
1630    let (tx, rx) = mpsc::channel();
1631    self.context.send_message(Message::Webview {
1632      window_id: *self.window_id.lock().unwrap(),
1633      webview_id: self.webview_id,
1634      message: WebviewMessage::SendDevToolsMessage(message.to_vec(), tx),
1635    })?;
1636    rx.recv().map_err(|_| Error::FailedToReceiveMessage)?
1637  }
1638
1639  /// Observes the [`DevToolsProtocol`] traffic of this browser.
1640  ///
1641  /// The observer receives every message on the browser, including the runtime's
1642  /// own requests, so results must be matched against an identifier obtained from
1643  /// [`allocate_devtools_message_id`](crate::allocate_devtools_message_id).
1644  ///
1645  /// Scoped to this webview's own native browser. A CEF-owned popup is a
1646  /// separate browser whose protocol traffic — its page content, its network
1647  /// activity and its dialog messages — is never reported here, the way a
1648  /// [`FrameEvent`](crate::FrameEvent) of a popup is not. Observe popups
1649  /// through [`Webview::popups`], whose
1650  /// [`FrameNavigationState`](crate::FrameNavigationState) follows a popup's
1651  /// native lifecycle without exposing what it loaded.
1652  pub fn on_dev_tools_protocol<F: Fn(DevToolsProtocol) + Send + Sync + 'static>(
1653    &self,
1654    f: F,
1655  ) -> Result<()> {
1656    let (tx, rx) = mpsc::channel();
1657    let handler =
1658      Arc::new(move |protocol: DevToolsProtocol| f(protocol)) as Arc<DevToolsProtocolHandler>;
1659    self.context.send_message(Message::Webview {
1660      window_id: *self.window_id.lock().unwrap(),
1661      webview_id: self.webview_id,
1662      message: WebviewMessage::OnDevToolsProtocol(handler, tx),
1663    })?;
1664    rx.recv().map_err(|_| Error::FailedToReceiveMessage)?
1665  }
1666}
1667
1668pub(crate) fn create_webview_detached<T: UserEvent>(
1669  context: &RuntimeContext<T>,
1670  window_id: WindowId,
1671  pending: PendingWebview<T, CefRuntime<T>>,
1672) -> Result<DetachedWebview<T, CefRuntime<T>>> {
1673  let label = pending.label.clone();
1674  let webview_id = context.next_webview_id();
1675  let (result_tx, result_rx) = mpsc::channel();
1676  context.send_message(Message::CreateWebview {
1677    window_id,
1678    webview_id,
1679    pending: Box::new(pending),
1680    result_tx,
1681  })?;
1682  // Wait until the event loop has created the browser so a creation failure is
1683  // surfaced to the caller instead of leaving a detached, dead browser.
1684  context.wait_for_creation(result_rx)?;
1685  Ok(DetachedWebview {
1686    label,
1687    dispatcher: CefWebviewDispatcher {
1688      window_id: Arc::new(Mutex::new(window_id)),
1689      webview_id,
1690      context: context.clone(),
1691    },
1692  })
1693}
1694
1695fn getter<T: UserEvent, R>(
1696  context: &RuntimeContext<T>,
1697  message: Message<T>,
1698  receiver: Receiver<Result<R>>,
1699) -> Result<R> {
1700  context.send_message(message)?;
1701  receiver.recv().map_err(|_| Error::FailedToReceiveMessage)?
1702}
1703
1704macro_rules! webview_getter {
1705  ($self:ident, $variant:ident) => {{
1706    let (tx, rx) = mpsc::channel();
1707    // Drop the guard before waiting: CEF page-load callbacks on the UI thread
1708    // may need this same lock to dispatch work before servicing the getter.
1709    let window_id = *$self.window_id.lock().unwrap();
1710    getter(
1711      &$self.context,
1712      Message::Webview {
1713        window_id,
1714        webview_id: $self.webview_id,
1715        message: WebviewMessage::$variant(tx),
1716      },
1717      rx,
1718    )
1719  }};
1720}
1721
1722#[cfg(test)]
1723mod getter_tests {
1724  use super::{Message, WebviewMessage};
1725  use std::sync::{Arc, Mutex, mpsc};
1726  use tauri_runtime::{Result, window::WindowId};
1727
1728  // Exercise the production macro with a bounded UI-reply probe. A real CEF
1729  // event loop cannot run in a unit-test worker; the probe checks the lock
1730  // before replying so the regression fails instead of hanging the suite.
1731  struct Dispatcher {
1732    context: Arc<Mutex<WindowId>>,
1733    window_id: Arc<Mutex<WindowId>>,
1734    webview_id: u32,
1735  }
1736
1737  fn getter(
1738    window_id: &Arc<Mutex<WindowId>>,
1739    message: Message<()>,
1740    receiver: mpsc::Receiver<Result<String>>,
1741  ) -> Result<String> {
1742    let Message::Webview {
1743      window_id: requested_window,
1744      message: WebviewMessage::Url(reply),
1745      ..
1746    } = message
1747    else {
1748      panic!("expected a URL request");
1749    };
1750    let callback_window = window_id
1751      .try_lock()
1752      .expect("the UI callback must acquire the window lock before replying");
1753    assert_eq!(*callback_window, requested_window);
1754    reply.send(Ok("https://example.test/".into())).unwrap();
1755    receiver.recv().unwrap()
1756  }
1757
1758  #[test]
1759  fn url_getter_does_not_hold_the_window_lock_while_waiting_for_ui() {
1760    let window_id = Arc::new(Mutex::new(WindowId::from(1)));
1761    let dispatcher = Dispatcher {
1762      context: Arc::clone(&window_id),
1763      window_id,
1764      webview_id: 1,
1765    };
1766    assert_eq!(
1767      webview_getter!(dispatcher, Url).unwrap(),
1768      "https://example.test/"
1769    );
1770  }
1771}
1772
1773impl<T: UserEvent> WebviewDispatch<T> for CefWebviewDispatcher<T> {
1774  type Runtime = CefRuntime<T>;
1775
1776  fn run_on_main_thread<F: FnOnce() + Send + 'static>(&self, f: F) -> Result<()> {
1777    self.context.run_on_main_thread(f)
1778  }
1779
1780  fn on_webview_event<F: Fn(&WebviewEvent) + Send + 'static>(&self, f: F) -> WebviewEventId {
1781    let id = self.context.next_webview_event_id();
1782    let _ = self.context.send_message(Message::Webview {
1783      window_id: *self.window_id.lock().unwrap(),
1784      webview_id: self.webview_id,
1785      message: WebviewMessage::AddEventListener(id, Box::new(f)),
1786    });
1787    id
1788  }
1789
1790  fn with_webview<F: FnOnce(<Self::Runtime as Runtime<T>>::Webview) + Send + 'static>(
1791    &self,
1792    f: F,
1793  ) -> Result<()> {
1794    self.context.send_message(Message::Webview {
1795      window_id: *self.window_id.lock().unwrap(),
1796      webview_id: self.webview_id,
1797      message: WebviewMessage::WithWebview(Box::new(f)),
1798    })
1799  }
1800
1801  fn open_devtools(&self) {
1802    #[cfg(any(debug_assertions, feature = "devtools"))]
1803    {
1804      let _ = self.context.send_message(Message::Webview {
1805        window_id: *self.window_id.lock().unwrap(),
1806        webview_id: self.webview_id,
1807        message: WebviewMessage::OpenDevTools,
1808      });
1809    }
1810    #[cfg(not(any(debug_assertions, feature = "devtools")))]
1811    log::warn!("devtools are not available: enable the `devtools` feature of `tauri-runtime-cef`");
1812  }
1813
1814  fn close_devtools(&self) {
1815    #[cfg(any(debug_assertions, feature = "devtools"))]
1816    {
1817      let _ = self.context.send_message(Message::Webview {
1818        window_id: *self.window_id.lock().unwrap(),
1819        webview_id: self.webview_id,
1820        message: WebviewMessage::CloseDevTools,
1821      });
1822    }
1823  }
1824
1825  fn is_devtools_open(&self) -> Result<bool> {
1826    #[cfg(any(debug_assertions, feature = "devtools"))]
1827    {
1828      let (tx, rx) = mpsc::channel();
1829      self.context.send_message(Message::Webview {
1830        window_id: *self.window_id.lock().unwrap(),
1831        webview_id: self.webview_id,
1832        message: WebviewMessage::IsDevToolsOpen(tx),
1833      })?;
1834      rx.recv().map_err(|_| Error::FailedToReceiveMessage)
1835    }
1836    #[cfg(not(any(debug_assertions, feature = "devtools")))]
1837    {
1838      Ok(false)
1839    }
1840  }
1841
1842  fn url(&self) -> Result<String> {
1843    webview_getter!(self, Url)
1844  }
1845
1846  fn bounds(&self) -> Result<Rect> {
1847    webview_getter!(self, Bounds)
1848  }
1849
1850  fn position(&self) -> Result<PhysicalPosition<i32>> {
1851    webview_getter!(self, Position)
1852  }
1853
1854  fn size(&self) -> Result<PhysicalSize<u32>> {
1855    webview_getter!(self, Size)
1856  }
1857
1858  fn navigate(&self, url: Url) -> Result<()> {
1859    self.context.send_message(Message::Webview {
1860      window_id: *self.window_id.lock().unwrap(),
1861      webview_id: self.webview_id,
1862      message: WebviewMessage::Navigate(url),
1863    })
1864  }
1865
1866  fn reload(&self) -> Result<()> {
1867    self.context.send_message(Message::Webview {
1868      window_id: *self.window_id.lock().unwrap(),
1869      webview_id: self.webview_id,
1870      message: WebviewMessage::Reload,
1871    })
1872  }
1873
1874  fn go_back(&self) -> Result<()> {
1875    self.context.send_message(Message::Webview {
1876      window_id: *self.window_id.lock().unwrap(),
1877      webview_id: self.webview_id,
1878      message: WebviewMessage::GoBack,
1879    })
1880  }
1881
1882  fn can_go_back(&self) -> Result<bool> {
1883    webview_getter!(self, CanGoBack)
1884  }
1885
1886  fn go_forward(&self) -> Result<()> {
1887    self.context.send_message(Message::Webview {
1888      window_id: *self.window_id.lock().unwrap(),
1889      webview_id: self.webview_id,
1890      message: WebviewMessage::GoForward,
1891    })
1892  }
1893
1894  fn can_go_forward(&self) -> Result<bool> {
1895    webview_getter!(self, CanGoForward)
1896  }
1897
1898  fn print(&self) -> Result<()> {
1899    self.context.send_message(Message::Webview {
1900      window_id: *self.window_id.lock().unwrap(),
1901      webview_id: self.webview_id,
1902      message: WebviewMessage::Print,
1903    })
1904  }
1905
1906  fn close(&self) -> Result<()> {
1907    self.context.send_message(Message::Webview {
1908      window_id: *self.window_id.lock().unwrap(),
1909      webview_id: self.webview_id,
1910      message: WebviewMessage::Close,
1911    })
1912  }
1913
1914  fn set_bounds(&self, bounds: Rect) -> Result<()> {
1915    self.context.send_message(Message::Webview {
1916      window_id: *self.window_id.lock().unwrap(),
1917      webview_id: self.webview_id,
1918      message: WebviewMessage::SetBounds(bounds),
1919    })
1920  }
1921
1922  fn set_size(&self, size: Size) -> Result<()> {
1923    self.context.send_message(Message::Webview {
1924      window_id: *self.window_id.lock().unwrap(),
1925      webview_id: self.webview_id,
1926      message: WebviewMessage::SetSize(size),
1927    })
1928  }
1929
1930  fn set_position(&self, position: Position) -> Result<()> {
1931    self.context.send_message(Message::Webview {
1932      window_id: *self.window_id.lock().unwrap(),
1933      webview_id: self.webview_id,
1934      message: WebviewMessage::SetPosition(position),
1935    })
1936  }
1937
1938  fn set_focus(&self) -> Result<()> {
1939    self.context.send_message(Message::Webview {
1940      window_id: *self.window_id.lock().unwrap(),
1941      webview_id: self.webview_id,
1942      message: WebviewMessage::SetFocus,
1943    })
1944  }
1945
1946  fn hide(&self) -> Result<()> {
1947    self.context.send_message(Message::Webview {
1948      window_id: *self.window_id.lock().unwrap(),
1949      webview_id: self.webview_id,
1950      message: WebviewMessage::Hide,
1951    })
1952  }
1953
1954  fn show(&self) -> Result<()> {
1955    self.context.send_message(Message::Webview {
1956      window_id: *self.window_id.lock().unwrap(),
1957      webview_id: self.webview_id,
1958      message: WebviewMessage::Show,
1959    })
1960  }
1961
1962  fn eval_script<S: Into<String>>(&self, script: S) -> Result<()> {
1963    self.context.send_message(Message::Webview {
1964      window_id: *self.window_id.lock().unwrap(),
1965      webview_id: self.webview_id,
1966      message: WebviewMessage::EvaluateScript(script.into()),
1967    })
1968  }
1969
1970  fn eval_script_with_callback<S: Into<String>>(
1971    &self,
1972    script: S,
1973    callback: impl Fn(String) + Send + 'static,
1974  ) -> Result<()> {
1975    self.context.send_message(Message::Webview {
1976      window_id: *self.window_id.lock().unwrap(),
1977      webview_id: self.webview_id,
1978      message: WebviewMessage::EvaluateScriptWithCallback(script.into(), Box::new(callback)),
1979    })
1980  }
1981
1982  fn reparent(&self, window_id: WindowId) -> Result<()> {
1983    let (tx, rx) = mpsc::channel();
1984    self.context.send_message(Message::Webview {
1985      window_id: *self.window_id.lock().unwrap(),
1986      webview_id: self.webview_id,
1987      message: WebviewMessage::Reparent(window_id, tx),
1988    })?;
1989    let result = rx.recv().map_err(|_| Error::FailedToReceiveMessage)?;
1990    if result.is_ok() {
1991      *self.window_id.lock().unwrap() = window_id;
1992    }
1993    result
1994  }
1995
1996  fn cookies_for_url(&self, url: Url) -> Result<Vec<Cookie<'static>>> {
1997    let (tx, rx) = mpsc::channel();
1998    self.context.send_message(Message::Webview {
1999      window_id: *self.window_id.lock().unwrap(),
2000      webview_id: self.webview_id,
2001      message: WebviewMessage::CookiesForUrl(url, tx),
2002    })?;
2003    rx.recv().map_err(|_| Error::FailedToReceiveMessage)?
2004  }
2005
2006  fn cookies(&self) -> Result<Vec<Cookie<'static>>> {
2007    webview_getter!(self, Cookies)
2008  }
2009
2010  fn set_cookie(&self, cookie: Cookie<'_>) -> Result<()> {
2011    self.context.send_message(Message::Webview {
2012      window_id: *self.window_id.lock().unwrap(),
2013      webview_id: self.webview_id,
2014      message: WebviewMessage::SetCookie(cookie.into_owned()),
2015    })
2016  }
2017
2018  fn delete_cookie(&self, cookie: Cookie<'_>) -> Result<()> {
2019    self.context.send_message(Message::Webview {
2020      window_id: *self.window_id.lock().unwrap(),
2021      webview_id: self.webview_id,
2022      message: WebviewMessage::DeleteCookie(cookie.into_owned()),
2023    })
2024  }
2025
2026  fn set_auto_resize(&self, auto_resize: bool) -> Result<()> {
2027    self.context.send_message(Message::Webview {
2028      window_id: *self.window_id.lock().unwrap(),
2029      webview_id: self.webview_id,
2030      message: WebviewMessage::SetAutoResize(auto_resize),
2031    })
2032  }
2033
2034  fn set_zoom(&self, scale_factor: f64) -> Result<()> {
2035    self.context.send_message(Message::Webview {
2036      window_id: *self.window_id.lock().unwrap(),
2037      webview_id: self.webview_id,
2038      message: WebviewMessage::SetZoom(scale_factor),
2039    })
2040  }
2041
2042  fn set_background_color(&self, color: Option<Color>) -> Result<()> {
2043    self.context.send_message(Message::Webview {
2044      window_id: *self.window_id.lock().unwrap(),
2045      webview_id: self.webview_id,
2046      message: WebviewMessage::SetBackgroundColor(color),
2047    })
2048  }
2049
2050  fn clear_all_browsing_data(&self) -> Result<()> {
2051    self.context.send_message(Message::Webview {
2052      window_id: *self.window_id.lock().unwrap(),
2053      webview_id: self.webview_id,
2054      message: WebviewMessage::ClearAllBrowsingData,
2055    })
2056  }
2057}
2058
2059/// Reposition every child webview to follow the parent window size.
2060///
2061/// Children with a bounds rate (auto-resize / window-filling) are recomputed
2062/// from the current window size; children with fixed bounds keep whatever bounds
2063/// they were last given.
2064pub(crate) fn layout_app_window(appwindow: &AppWindow) {
2065  // An offscreen webview paints into the toplevel itself, so the X11 host the
2066  // windowed browsers live in would only cover it. Unmap the host while every
2067  // webview of the window is offscreen.
2068  #[cfg(target_os = "linux")]
2069  appwindow.cef_host.set_visible(
2070    appwindow.children.is_empty()
2071      || appwindow
2072        .children
2073        .iter()
2074        .any(|child| child.offscreen.is_none()),
2075  );
2076  let parent_size = appwindow.safe_surface_size();
2077  let win_w = parent_size.width as f32;
2078  let win_h = parent_size.height as f32;
2079  let scale = appwindow.window.scale_factor();
2080  for child in &appwindow.children {
2081    let Some(rate) = child.bounds_rate else {
2082      continue;
2083    };
2084    let x = (rate.x * win_w).round() as i32;
2085    let y = (rate.y * win_h).round() as i32;
2086    let w = (rate.width * win_w).round() as i32;
2087    let h = (rate.height * win_h).round() as i32;
2088    child.host.notify_move_or_resize_started();
2089    child.resize(scale, x, y, w, h);
2090    child.host.was_resized();
2091  }
2092}
2093
2094/// Compute the bounds rate of a child webview relative to its parent window.
2095///
2096/// For webiews filling the window, default rate is used, otherwise the rate is computed from the current bounds and parent size
2097/// if auto_resize is enabled, otherwise None is returned.
2098pub(crate) fn compute_child_bounds_rate(
2099  bounds: Option<&Rect>,
2100  auto_resize: bool,
2101  parent_size: PhysicalSize<u32>,
2102  scale: f64,
2103) -> Option<BoundsRate> {
2104  let Some(bounds) = bounds else {
2105    return Some(BoundsRate::default());
2106  };
2107
2108  if !auto_resize {
2109    return None;
2110  }
2111
2112  let min_w = parent_size.width.max(1) as i32;
2113  let min_h = parent_size.height.max(1) as i32;
2114
2115  let pos = bounds.position.to_physical::<i32>(scale);
2116  let size = bounds.size.to_physical::<u32>(scale);
2117
2118  let x = pos.x;
2119  let y = pos.y;
2120  let w = size.width;
2121  let h = size.height;
2122
2123  Some(BoundsRate {
2124    x: x as f32 / min_w as f32,
2125    y: y as f32 / min_h as f32,
2126    width: w as f32 / min_w as f32,
2127    height: h as f32 / min_h as f32,
2128  })
2129}
2130
2131pub(crate) const INITIAL_LOAD_URL: &str = concat!(
2132  "data:text/html;charset=utf-8,",
2133  "%3C!doctype%20html%3E",
2134  "%3Chtml%20data-tauri-cef-internal%3D%22initial-load%22%3E",
2135  "%3Chead%3E",
2136  "%3Cmeta%20charset%3D%22utf-8%22%3E",
2137  "%3Ctitle%3ETauri%20CEF%20Initial%20Load%3C%2Ftitle%3E",
2138  "%3C%2Fhead%3E",
2139  "%3Cbody%20data-tauri-cef-internal%3D%22initial-load%22%3E",
2140  "%3C!--%20Tauri%20CEF%20internal%20initial%20load%20placeholder%20--%3E",
2141  "%3C%2Fbody%3E",
2142  "%3C%2Fhtml%3E",
2143);
2144
2145/// Maps a pending `Page.addScriptToEvaluateOnNewDocument` CDP message id to the
2146/// `(browser, real_url)` whose real navigation is deferred until that message is
2147/// acknowledged.
2148///
2149/// The keys only ever come from `allocate_runtime_devtools_message_id`, whose
2150/// reserved range no caller identifier can reach, so a caller cannot release the
2151/// deferred navigation early by sending a request with a hardcoded `id`.
2152pub(crate) type PendingInitialLoads = Arc<Mutex<HashMap<i32, (Browser, String)>>>;
2153
2154cef::wrap_dev_tools_message_observer! {
2155  struct TauriDevToolsProtocolObserver {
2156    handlers: Arc<Mutex<Vec<Arc<DevToolsProtocolHandler>>>>,
2157    pending_initial_loads: PendingInitialLoads,
2158    dialogs: crate::dialog::DialogState,
2159  }
2160
2161  impl DevToolsMessageObserver {
2162    fn on_dev_tools_message(
2163      &self,
2164      _browser: Option<&mut cef::Browser>,
2165      message: Option<&[u8]>,
2166    ) -> std::os::raw::c_int {
2167      if let Some(message) = message {
2168        let protocol = DevToolsProtocol::Message(message.to_vec());
2169        if let Ok(handlers) = self.handlers.lock() {
2170          for handler in handlers.iter() {
2171            handler(protocol.clone());
2172          }
2173        }
2174      }
2175      0
2176    }
2177
2178    fn on_dev_tools_method_result(
2179      &self,
2180      _browser: Option<&mut Browser>,
2181      message_id: std::os::raw::c_int,
2182      success: std::os::raw::c_int,
2183      result: Option<&[u8]>,
2184    ) {
2185      // The real navigation was deferred until the document-start script was
2186      // registered; this result acknowledges that, so kick off the real load.
2187      if let Some((browser, initial_url)) = self
2188        .pending_initial_loads
2189        .lock()
2190        .unwrap()
2191        .remove(&message_id)
2192      {
2193        post_load_initial_url(browser, initial_url);
2194      }
2195
2196      let protocol = DevToolsProtocol::MethodResult {
2197        message_id,
2198        success: success != 0,
2199        result: result.map(|r| r.to_vec()).unwrap_or_default(),
2200      };
2201      if let Ok(handlers) = self.handlers.lock() {
2202        for handler in handlers.iter() {
2203          handler(protocol.clone());
2204        }
2205      }
2206    }
2207
2208    fn on_dev_tools_event(
2209      &self,
2210      browser: Option<&mut Browser>,
2211      method: Option<&CefString>,
2212      params: Option<&[u8]>,
2213    ) {
2214      if let (Some(browser), Some(method)) = (browser, method)
2215        && self.dialogs.accepts_browser(browser.identifier()) {
2216        self.dialogs.on_event(&method.to_string(), params.unwrap_or_default());
2217      }
2218      let protocol = DevToolsProtocol::Event {
2219        method: method.map(|m| format!("{m}")).unwrap_or_default(),
2220        params: params.map(|p| p.to_vec()).unwrap_or_default(),
2221      };
2222      if let Ok(handlers) = self.handlers.lock() {
2223        for handler in handlers.iter() {
2224          handler(protocol.clone());
2225        }
2226      }
2227    }
2228  }
2229}
2230
2231fn runtime_evaluate_result_to_json(result: Option<&[u8]>) -> String {
2232  let Some(result) = result else {
2233    return String::new();
2234  };
2235  let Ok(result) = serde_json::from_slice::<serde_json::Value>(result) else {
2236    return String::new();
2237  };
2238
2239  if result.get("exceptionDetails").is_some() {
2240    return String::new();
2241  }
2242
2243  let remote_object = result.get("result").unwrap_or(&result);
2244  remote_object
2245    .get("value")
2246    .and_then(|value| serde_json::to_string(value).ok())
2247    .unwrap_or_default()
2248}
2249
2250type EvalScriptCallback = Box<dyn Fn(String) + Send + 'static>;
2251
2252cef::wrap_dev_tools_message_observer! {
2253  struct EvalScriptWithCallbackDevToolsObserver {
2254    message_id: i32,
2255    callback: Arc<Mutex<Option<EvalScriptCallback>>>,
2256    registration: Arc<Mutex<Option<cef::Registration>>>,
2257  }
2258
2259  impl DevToolsMessageObserver {
2260    fn on_dev_tools_method_result(
2261      &self,
2262      _browser: Option<&mut Browser>,
2263      message_id: std::os::raw::c_int,
2264      success: std::os::raw::c_int,
2265      result: Option<&[u8]>,
2266    ) {
2267      if message_id != self.message_id {
2268        return;
2269      }
2270
2271      let Some(callback) = self.callback.lock().unwrap().take() else {
2272        return;
2273      };
2274
2275      let result = if success != 0 {
2276        runtime_evaluate_result_to_json(result)
2277      } else {
2278        String::new()
2279      };
2280      callback(result);
2281
2282      let _ = self.registration.lock().unwrap().take();
2283    }
2284  }
2285}
2286
2287/// Registers a DevTools protocol observer. Returns the [`cef::Registration`] which must be
2288/// kept alive for the observer to stay registered. The observer is unregistered when
2289/// the Registration is dropped.
2290/// Overrides the user agent of one native browser through the DevTools protocol.
2291///
2292/// `CefSettings.user_agent` is process-wide and fixed before any browser exists, so it
2293/// cannot answer `WebviewAttributes::user_agent`. `Emulation.setUserAgentOverride` can:
2294/// it is scoped to this target and applies to both the `User-Agent` request header and
2295/// `navigator.userAgent`, for the life of the browser.
2296///
2297/// Sent before the initial navigation so the first request already carries it. Client
2298/// hints are deliberately left alone: overriding the user agent without them is what
2299/// Chromium itself does for the `--user-agent` switch.
2300///
2301/// Scoped to this one native browser, so a CEF-owned popup keeps the process-wide user
2302/// agent from `Cef::user_agent`; use that one to cover popups too.
2303fn apply_user_agent_override(host: &BrowserHost, user_agent: &str) {
2304  let Ok(message_id) = crate::devtools::allocate_runtime_devtools_message_id() else {
2305    log::warn!("could not set the webview user agent: no DevTools message id was available");
2306    return;
2307  };
2308
2309  let message = serde_json::json!({
2310    "id": message_id,
2311    "method": "Emulation.setUserAgentOverride",
2312    "params": { "userAgent": user_agent },
2313  })
2314  .to_string();
2315
2316  if host.send_dev_tools_message(Some(message.as_bytes())) != 1 {
2317    log::warn!("failed to set the webview user agent through the DevTools protocol");
2318  }
2319}
2320
2321pub(crate) fn add_dev_tools_observer(
2322  browser: &Browser,
2323  handlers: Arc<Mutex<Vec<Arc<DevToolsProtocolHandler>>>>,
2324  pending_initial_loads: PendingInitialLoads,
2325  dialogs: crate::dialog::DialogState,
2326) -> Option<cef::Registration> {
2327  browser.host().and_then(|host| {
2328    let mut observer = TauriDevToolsProtocolObserver::new(handlers, pending_initial_loads, dialogs);
2329    let registration = host.add_dev_tools_message_observer(Some(&mut observer))?;
2330    if let Ok(id) = crate::devtools::allocate_runtime_devtools_message_id() {
2331      let message = serde_json::json!({"id":id,"method":"Page.enable","params":{}}).to_string();
2332      let _ = host.send_dev_tools_message(Some(message.as_bytes()));
2333    }
2334    Some(registration)
2335  })
2336}
2337
2338fn devtools_initialization_script_source(
2339  initialization_scripts: &[CefInitScript],
2340  custom_protocol_scheme: &str,
2341  custom_scheme_domain_names: &[String],
2342) -> Option<String> {
2343  if initialization_scripts.is_empty() {
2344    return None;
2345  }
2346
2347  let custom_protocol = serde_json::to_string(&format!("{custom_protocol_scheme}:")).ok()?;
2348  let custom_domains = serde_json::to_string(custom_scheme_domain_names).ok()?;
2349  let mut source = format!(
2350    r#"{{
2351  const __TAURI_CEF_INIT_CUSTOM_PROTOCOL__ = {custom_protocol};
2352  const __TAURI_CEF_INIT_CUSTOM_DOMAINS__ = new Set({custom_domains});
2353  const __TAURI_CEF_INIT_IS_CUSTOM_PROTOCOL__ =
2354    location.protocol === __TAURI_CEF_INIT_CUSTOM_PROTOCOL__
2355    && __TAURI_CEF_INIT_CUSTOM_DOMAINS__.has(location.hostname);
2356  const __TAURI_CEF_INIT_IS_MAIN_FRAME__ = (() => {{
2357    try {{
2358      return window.top === window;
2359    }} catch (_) {{
2360      return false;
2361    }}
2362  }})();
2363"#
2364  );
2365
2366  for init_script in initialization_scripts {
2367    source.push_str("  if (!__TAURI_CEF_INIT_IS_CUSTOM_PROTOCOL__");
2368    if init_script.for_main_frame_only {
2369      source.push_str(" && __TAURI_CEF_INIT_IS_MAIN_FRAME__");
2370    }
2371    source.push_str(") {\n");
2372    source.push_str(init_script.script.as_str());
2373    source.push_str("\n  }\n");
2374  }
2375
2376  source.push_str("}\n");
2377  Some(source)
2378}
2379
2380fn register_initialization_scripts(
2381  browser: &Browser,
2382  initialization_scripts: &[CefInitScript],
2383  custom_protocol_scheme: &str,
2384  custom_scheme_domain_names: &[String],
2385  initial_url: String,
2386  pending_initial_loads: &PendingInitialLoads,
2387) -> std::result::Result<bool, crate::DevToolsMessageIdExhausted> {
2388  let Some(source) = devtools_initialization_script_source(
2389    initialization_scripts,
2390    custom_protocol_scheme,
2391    custom_scheme_domain_names,
2392  ) else {
2393    return Ok(false);
2394  };
2395  let Some(host) = browser.host() else {
2396    return Ok(false);
2397  };
2398
2399  let message_id = crate::devtools::allocate_runtime_devtools_message_id()?;
2400  let message = serde_json::json!({
2401    "id": message_id,
2402    "method": "Page.addScriptToEvaluateOnNewDocument",
2403    "params": {
2404      "source": source,
2405    }
2406  })
2407  .to_string();
2408
2409  pending_initial_loads
2410    .lock()
2411    .unwrap()
2412    .insert(message_id, (browser.clone(), initial_url));
2413  if host.send_dev_tools_message(Some(message.as_bytes())) == 1 {
2414    Ok(true)
2415  } else {
2416    pending_initial_loads.lock().unwrap().remove(&message_id);
2417    Ok(false)
2418  }
2419}
2420
2421wrap_task! {
2422  struct LoadInitialUrlTask {
2423    browser: Browser,
2424    initial_url: String,
2425  }
2426
2427  impl Task {
2428    fn execute(&self) {
2429      load_initial_url(&self.browser, &self.initial_url);
2430    }
2431  }
2432}
2433
2434fn post_load_initial_url(browser: Browser, initial_url: String) {
2435  let mut task = LoadInitialUrlTask::new(browser, initial_url);
2436  cef::post_task(sys::cef_thread_id_t::TID_UI.into(), Some(&mut task));
2437}
2438
2439// Browsers are created with an inert internal document so the BrowserHost exists
2440// before the app's real first navigation starts. That gives us a chance to
2441// register the CDP document-start script for remote/cross-site navigations; the
2442// custom-protocol path still injects into HTML because CEF does not apply this
2443// CDP hook to those documents reliably.
2444//
2445// The real load is posted as a CEF UI task instead of performed inline. This
2446// keeps the browser creation/CDP setup stack from re-entering navigation.
2447pub(crate) fn load_initial_url_after_registering_initialization_scripts(
2448  browser: &Browser,
2449  initialization_scripts: &[CefInitScript],
2450  custom_protocol_scheme: &str,
2451  custom_scheme_domain_names: &[String],
2452  initial_url: &str,
2453  pending_initial_loads: &PendingInitialLoads,
2454) {
2455  let browser_for_callback = browser.clone();
2456  let initial_url = initial_url.to_string();
2457  let is_waiting_for_initialization_scripts = register_initialization_scripts(
2458    browser,
2459    initialization_scripts,
2460    custom_protocol_scheme,
2461    custom_scheme_domain_names,
2462    initial_url.clone(),
2463    pending_initial_loads,
2464  );
2465
2466  match is_waiting_for_initialization_scripts {
2467    Ok(false) => post_load_initial_url(browser_for_callback, initial_url),
2468    Ok(true) => {}
2469    Err(error) => {
2470      // Exhaustion cannot fall through to a navigation without the requested
2471      // document-start scripts or reuse another operation's acknowledgment.
2472      log::error!("CEF initialization failed: {error}");
2473      if let Some(host) = browser.host() {
2474        host.close_browser(1);
2475      }
2476    }
2477  }
2478}
2479
2480fn load_initial_url(browser: &Browser, initial_url: &str) {
2481  if let Some(frame) = browser.main_frame() {
2482    frame.load_url(Some(&CefString::from(initial_url)));
2483  }
2484}
2485
2486#[cfg(test)]
2487mod tests {
2488  use super::*;
2489
2490  #[test]
2491  fn offscreen_background_is_transparent_only_on_request() {
2492    let mut attributes = WebviewAttributes::new(tauri_utils::config::WebviewUrl::default());
2493    assert_eq!(
2494      browser_settings_from_webview_attributes(&attributes, false).background_color,
2495      0
2496    );
2497    assert_eq!(
2498      browser_settings_from_webview_attributes(&attributes, true).background_color,
2499      0xFFFF_FFFF
2500    );
2501
2502    attributes.transparent = true;
2503    assert_eq!(
2504      browser_settings_from_webview_attributes(&attributes, true).background_color,
2505      0
2506    );
2507
2508    attributes.background_color = Some(Color(1, 2, 3, 255));
2509    assert_eq!(
2510      browser_settings_from_webview_attributes(&attributes, true).background_color,
2511      0xFF01_0203
2512    );
2513  }
2514}