Skip to main content

tauri_runtime_cef/
runtime.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5#![allow(clippy::arc_with_non_send_sync)]
6#![allow(clippy::too_many_arguments)]
7
8use std::{
9  collections::HashMap,
10  fmt,
11  fs::create_dir_all,
12  path::PathBuf,
13  sync::{
14    Arc, Mutex,
15    atomic::{AtomicBool, AtomicPtr, AtomicU32, Ordering},
16    mpsc::{self, Receiver, Sender},
17  },
18  time::Duration,
19};
20
21use cef::*;
22use raw_window_handle::{DisplayHandle, HasDisplayHandle};
23use tauri_runtime::{
24  DeviceEventFilter, Error, EventLoopProxy, ExitRequestedEventAction, Result, RunEvent, Runtime,
25  RuntimeHandle, RuntimeInitArgs, UserEvent,
26  dpi::PhysicalPosition,
27  monitor::Monitor,
28  webview::{DetachedWebview, PendingWebview},
29  window::{
30    DetachedWindow, DragDropEvent, PendingWindow, RawWindow, WebviewEvent, WindowEvent, WindowId,
31  },
32};
33use tauri_utils::Theme;
34use winit::{
35  application::ApplicationHandler,
36  data_transfer::{DataTransferId, TypeHint},
37  event::{StartCause, WindowEvent as WinitWindowEvent},
38  event_loop::{
39    ActiveEventLoop, DndAction, EventLoop, EventLoopBuilder, EventLoopProxy as WinitEventLoopProxy,
40  },
41  window::WindowId as WinitWindowId,
42};
43
44use crate::DebugEnvironment;
45use crate::external_message_pump::CefExternalPump;
46use crate::platform::EventLoopExt;
47use crate::{
48  cef_impl::{client as browser_client, ipc, request_handler},
49  macros::wrap_with_args,
50  webview::{
51    self, AppWebview, CefWebviewAttributes, CefWebviewDispatcher, Webview, WebviewMessage,
52    create_webview_detached,
53  },
54  window::{
55    AppWindow, CefWindowDispatcher, WindowMessage, create_window_detached,
56    winit_monitor_to_tauri_monitor, winit_theme_to_tauri_theme,
57  },
58  window_handle::SendRawDisplayHandle,
59};
60#[cfg(any(
61  target_os = "linux",
62  target_os = "dragonfly",
63  target_os = "freebsd",
64  target_os = "netbsd",
65  target_os = "openbsd"
66))]
67use winit::platform::gtk4::EventLoopBuilderExtGtk4;
68#[cfg(target_os = "macos")]
69use winit::platform::macos::EventLoopBuilderExtMacOS;
70#[cfg(windows)]
71use winit::platform::windows::EventLoopBuilderExtWindows;
72
73/// Native events for application rendering and input forwarding, on the event-loop thread.
74/// `Destroyed` is delivered before the runtime releases the window, including on exit.
75pub type NativeWindowEventHandler = dyn Fn(&str, &dyn ActiveEventLoop, Arc<dyn winit::window::Window>, &WinitWindowEvent)
76  + Send
77  + Sync;
78
79/// Customizes the CEF settings before initialization, see [`Cef::with_settings`].
80type SettingsCallback = dyn FnOnce(&mut cef::Settings) + Send + Sync;
81
82/// The `cef` crate used by this runtime, re-exported for convenience.
83///
84/// # Stability
85///
86/// The cef crate follows the Chromium Embedded Framework interface and there is
87/// no API stability guarantees. The crate will be updated frequently, usually
88/// in minor releases when a known breaking change is discovered.
89pub use cef;
90
91/// Which key Chromium uses to encrypt the little it stores encrypted.
92///
93/// Chromium's `os_crypt` layer encrypts **cookies and saved passwords** only. Every
94/// other piece of web storage — `localStorage`, IndexedDB, Cache Storage, service worker
95/// registrations — is written to the cache directory unencrypted whichever variant you
96/// pick here, exactly as it is under wry's WebKitGTK and WebView2 backends.
97///
98/// The default, [`SecretStorage::Auto`], skips the OS secret store in development builds
99/// (`tauri::is_dev()`) and keeps it in release builds. What it skips differs per
100/// platform:
101///
102/// - on macOS, `os_crypt` stores a random key in a shared "Chromium Safe Storage"
103///   keychain item whose ACL is bound to the code signature of the process that reads
104///   it. Ad-hoc-signed development builds get a new signature on every rebuild, so macOS
105///   puts up the keychain password prompt again after every `cargo build`.
106/// - on Linux, `os_crypt` asks the D-Bus secret portal, libsecret or KWallet for the
107///   key, which pops a keyring-unlock dialog the first time an app runs.
108///
109/// A release build that has to run where there is no secret store at all — a headless
110/// session, a container, a CI image — needs [`SecretStorage::Mock`], because there `Auto`
111/// asks for a store that is not there.
112///
113/// # Security
114///
115/// The mock keychain (`--use-mock-keychain`) and the Linux `basic` password store do not
116/// derive a secret key: they encrypt with a key derived from a **hard-coded constant**
117/// compiled into Chromium (`mock_password` and `peanuts` respectively). Both constants
118/// are public, so cookies encrypted with them have **no meaningful protection at rest** —
119/// anyone who can read the cache directory can decrypt them.
120///
121/// # Switching modes invalidates stored cookies
122///
123/// Cookies encrypted with one key cannot be read back with another, and development and
124/// release builds share the same default cache directory
125/// (`{user cache}/{identifier}/cef`). Moving an app between [`SecretStorage::Mock`] and
126/// [`SecretStorage::System`] — including the implicit move [`SecretStorage::Auto`] makes
127/// when a dev build is followed by a release build — therefore drops the cookies stored
128/// under the previous key, logging users out. Set [`Cef::root_cache_path`] to separate
129/// the two if that matters.
130#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
131#[non_exhaustive]
132pub enum SecretStorage {
133  /// Skip the OS secret store in development builds (`tauri::is_dev()`) and use it in
134  /// release builds: `--use-mock-keychain` on macOS and `--password-store=basic` on
135  /// Linux, in development only. Windows is untouched and keeps using DPAPI.
136  #[default]
137  Auto,
138  /// Always encrypt with Chromium's hard-coded constant: `--use-mock-keychain` on macOS,
139  /// `--password-store=basic` on Linux. Windows is untouched and keeps using DPAPI.
140  ///
141  /// Read the security note on [`SecretStorage`] before shipping this in a release
142  /// build: the key is a public constant, so the cookie jar is effectively unprotected.
143  Mock,
144  /// Always use the operating system secret store, on every platform and in every build
145  /// profile. Appends no switch at all.
146  System,
147}
148
149/// What to do with Chromium's process sandbox.
150///
151/// Defaults to [`SandboxPolicy::Auto`], which keeps the sandbox wherever the runtime can.
152///
153/// # Windows does not have a sandbox here yet
154///
155/// **Whatever this policy says, a Windows build currently runs unsandboxed.** CEF wants a
156/// sandbox broker pointer that, since Chromium M138, only a binary built with Chromium's
157/// own toolchain can create; CEF supplies prebuilt `bootstrap.exe` hosts for that, and
158/// they load the application as a DLL, which a Tauri application is not. Given a null
159/// broker CEF sets `no_sandbox` itself, so there is no configuration here that changes
160/// the outcome — only whether the runtime warns about it ([`Self::Auto`]) or refuses to
161/// start ([`Self::Required`]).
162#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
163#[non_exhaustive]
164pub enum SandboxPolicy {
165  /// Keep the sandbox wherever it can be kept, and log a warning naming the reason
166  /// wherever it cannot.
167  ///
168  /// It cannot be kept in two situations: on Windows, always, for the reason above; and
169  /// on Linux or BSD when the application runs from an AppImage on a system that has
170  /// neither the setuid `chrome-sandbox` helper nor usable unprivileged user namespaces,
171  /// where the alternative is not an unsandboxed application but no application at all,
172  /// since Chromium aborts with "No usable sandbox!".
173  ///
174  /// macOS always keeps it.
175  #[default]
176  Auto,
177  /// Never run without a sandbox: fail startup instead.
178  ///
179  /// Pick this when running unsandboxed is not an acceptable outcome and a hard failure
180  /// is preferable. On Linux the user can then install the setuid helper, point
181  /// `CHROME_DEVEL_SANDBOX` at one, or re-enable unprivileged user namespaces; on Windows
182  /// there is nothing they can do, so this always fails there.
183  Required,
184  /// Always run without a sandbox, on every platform.
185  ///
186  /// Every renderer then runs with the full privileges of the user, so a compromised
187  /// renderer is a compromised account. Useful for containers and CI images that cannot
188  /// provide a sandbox, not for shipped applications.
189  Disabled,
190}
191
192/// Whether Chromium's DevTools protocol server is reachable, and how.
193///
194/// This is the server behind `chrome://inspect`, not the DevTools window: it drives the
195/// browser from outside the process, so anything that can reach it can read and rewrite
196/// every page the application shows.
197#[derive(Debug, Default, Clone, PartialEq, Eq)]
198#[non_exhaustive]
199pub enum RemoteDebugging {
200  /// No server. The runtime additionally pins Chromium's
201  /// `devtools.remote_debugging.allowed` preference off, so a `--remote-debugging-port`
202  /// or `--remote-debugging-pipe` that reaches Chromium another way is refused too.
203  #[default]
204  Disabled,
205  /// Listen on a TCP port, as `--remote-debugging-port` does.
206  ///
207  /// The port must be between 1024 and 65535; CEF ignores anything else. The port number
208  /// is also written to `DevToolsActivePort` in the cache directory.
209  ///
210  /// Chromium accepts a WebSocket connection from `localhost` and from any origin named
211  /// in `allowed_origins` (`--remote-allow-origins`); leave that empty unless a browser
212  /// page has to attach.
213  ///
214  /// A listening port is reachable by every process on the machine, and the protocol has
215  /// no authentication. Prefer [`Self::Pipe`] where the debugger is a child process.
216  Port {
217    /// TCP port to listen on.
218    port: u16,
219    /// Origins allowed to open a WebSocket connection, beyond `localhost`.
220    allowed_origins: Vec<String>,
221  },
222  /// Speak the protocol over inherited file descriptors instead of a socket, as
223  /// `--remote-debugging-pipe` does.
224  ///
225  /// Reachable only by the process that launched this one, so it exposes nothing to the
226  /// rest of the machine.
227  Pipe,
228}
229
230/// Whether this application may open a DevTools window at all.
231///
232/// Application-wide, and combined with the per-webview `WebviewAttributes::devtools`:
233/// either one saying no is a no. Both are enforced on the paths this runtime owns — the
234/// context menu entries, the F12 and Ctrl+Shift+I chords, the `IDC_DEV_TOOLS` commands
235/// and `Webview::open_devtools`.
236///
237/// # Why it is not Chromium's own preference
238///
239/// Chromium has a profile preference for exactly this, `devtools.availability`, and
240/// `DevToolsWindow::AllowDevToolsFor` consults it on every path that opens a DevTools
241/// window — including the ones this runtime does not own, such as a Chrome-owned popup.
242/// This policy deliberately does not reach for it, and the runtime pins it to its
243/// default instead.
244///
245/// CEF gates more than the window on that preference: `SendDevToolsMessage` is refused on
246/// a profile carrying `kDisallowed`, and refused *silently* — the send still reports
247/// success, and no result or event ever reaches a registered observer. This runtime
248/// drives its own startup over the DevTools protocol (the document-start scripts, the
249/// per-webview user agent) and holds each webview's first navigation until that round
250/// trip answers, so setting the preference leaves every window stuck on a blank
251/// placeholder. A webview's `on_dev_tools_protocol` would go silent with it.
252///
253/// So the DevTools *protocol* stays available whatever this policy says. An application
254/// that has to close that path too wants [`RemoteDebugging`], which is what exposes the
255/// protocol to anything outside the process.
256#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
257#[non_exhaustive]
258pub enum DevToolsPolicy {
259  /// Allow DevTools in a build that could open them anyway — a debug build, or one with
260  /// the `devtools` feature — and refuse them otherwise.
261  #[default]
262  Auto,
263  /// Allow DevTools, whatever the build profile.
264  ///
265  /// Per-webview `WebviewAttributes::devtools` still applies; this only stops the runtime
266  /// from refusing DevTools application-wide.
267  Allowed,
268  /// Refuse DevTools, whatever the build profile.
269  Disallowed,
270}
271
272/// What to do about a navigation to a server whose TLS certificate does not validate.
273#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
274#[non_exhaustive]
275pub enum CertificateErrorPolicy {
276  /// Show Chrome's SSL interstitial, which offers the user a way to proceed anyway.
277  ///
278  /// This is Chromium's own behaviour and the runtime's default, because an application
279  /// that loads third-party content — an OAuth or SSO flow is the standing case — behaves
280  /// the way the user's browser would.
281  #[default]
282  ChromeInterstitial,
283  /// Cancel the request. No interstitial, and no way for the user to override.
284  ///
285  /// The hardened choice for an application that only ever loads origins it controls:
286  /// there, a certificate error is either a misconfiguration or an interception, and
287  /// neither is something to let a user click through.
288  Cancel,
289}
290
291/// How Chromium resolves the proxy for every request.
292///
293/// Written as the Chromium `proxy` preference, the same one the `ProxySettings`
294/// enterprise policy sets. `WebviewAttributes::proxy_url` sets the same preference for one
295/// webview; whichever is applied last to a given request context wins.
296#[derive(Debug, Default, Clone, PartialEq, Eq)]
297#[non_exhaustive]
298pub enum ProxyConfig {
299  /// Use the operating system's proxy configuration.
300  ///
301  /// Chromium's default, and on Linux the one that reads `http_proxy` and its siblings
302  /// out of the environment.
303  #[default]
304  System,
305  /// Connect directly, ignoring any system proxy.
306  Direct,
307  /// Discover a proxy through WPAD.
308  AutoDetect,
309  /// Fetch a proxy auto-config script from `url`.
310  PacScript {
311    /// URL of the PAC script.
312    url: String,
313  },
314  /// Use a fixed proxy.
315  FixedServers {
316    /// Proxy server, as `scheme://host:port` — for example `socks5://127.0.0.1:9050`.
317    /// A bare `host:port` means HTTP.
318    server: String,
319    /// Semicolon-delimited hosts that bypass the proxy, as the `--proxy-bypass-list`
320    /// switch spells them.
321    bypass_list: Option<String>,
322  },
323}
324
325impl ProxyConfig {
326  /// The `proxy` preference value Chromium expects for this configuration.
327  fn to_preference(&self) -> serde_json::Value {
328    match self {
329      Self::System => serde_json::json!({ "mode": "system" }),
330      Self::Direct => serde_json::json!({ "mode": "direct" }),
331      Self::AutoDetect => serde_json::json!({ "mode": "auto_detect" }),
332      Self::PacScript { url } => serde_json::json!({ "mode": "pac_script", "pac_url": url }),
333      Self::FixedServers {
334        server,
335        bypass_list,
336      } => {
337        let mut value = serde_json::json!({ "mode": "fixed_servers", "server": server });
338        if let Some(bypass_list) = bypass_list
339          && let Some(object) = value.as_object_mut()
340        {
341          object.insert(
342            "bypass_list".to_string(),
343            serde_json::Value::String(bypass_list.clone()),
344          );
345        }
346        value
347      }
348    }
349  }
350}
351
352/// When a page may start playing media on its own.
353///
354/// Applied through Chromium's `--autoplay-policy` switch.
355#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
356#[non_exhaustive]
357pub enum AutoplayPolicy {
358  /// Chromium's own default, which on desktop requires the user to have interacted with
359  /// the document before audible media plays.
360  #[default]
361  Default,
362  /// Let a page play media without any user interaction.
363  ///
364  /// What a kiosk, a media player or a signage application wants, and what makes a
365  /// hostile page able to make noise on its own.
366  NoUserGestureRequired,
367  /// Require a gesture on the media element itself.
368  UserGestureRequired,
369  /// Require the user to have interacted with the document.
370  DocumentUserActivationRequired,
371}
372
373impl AutoplayPolicy {
374  /// The `--autoplay-policy` value, or [`None`] to leave the switch off.
375  fn as_switch_value(self) -> Option<&'static str> {
376    match self {
377      Self::Default => None,
378      Self::NoUserGestureRequired => Some("no-user-gesture-required"),
379      Self::UserGestureRequired => Some("user-gesture-required"),
380      Self::DocumentUserActivationRequired => Some("document-user-activation-required"),
381    }
382  }
383}
384
385/// Which local network interfaces WebRTC may reveal to a page.
386///
387/// Applied through Chromium's `--webrtc-ip-handling-policy` switch. Chromium's default
388/// already hides local IP addresses behind mDNS hostnames, so this only matters for an
389/// application that wants to go further.
390#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
391#[non_exhaustive]
392pub enum WebRtcIpHandling {
393  /// Chromium's default.
394  #[default]
395  Default,
396  /// Offer both public and private interfaces, which reveals the machine's LAN address.
397  DefaultPublicAndPrivateInterfaces,
398  /// Offer only the public interface.
399  DefaultPublicInterfaceOnly,
400  /// Refuse any UDP that does not go through the configured proxy. The strongest of the
401  /// four, and the one most likely to break a call outright.
402  DisableNonProxiedUdp,
403}
404
405impl WebRtcIpHandling {
406  /// The `--webrtc-ip-handling-policy` value, or [`None`] to leave the switch off.
407  fn as_switch_value(self) -> Option<&'static str> {
408    match self {
409      Self::Default => None,
410      Self::DefaultPublicAndPrivateInterfaces => Some("default_public_and_private_interfaces"),
411      Self::DefaultPublicInterfaceOnly => Some("default_public_interface_only"),
412      Self::DisableNonProxiedUdp => Some("disable_non_proxied_udp"),
413    }
414  }
415}
416
417/// What to do with the profile on disk when a newer Chromium milestone than the one this
418/// binary embeds last used it: the application was rolled back to a release on an older
419/// CEF.
420///
421/// Defaults to [`DowngradePolicy::KeepProfile`].
422///
423/// # What a rollback does to the profile
424///
425/// Chromium migrates its profile forward only, and Chrome calls a launch on a profile from
426/// a higher milestone an unsupported downgrade (`chrome/browser/downgrade/`). In practice a
427/// rollback to the previous milestone usually just works; what can go wrong is bounded by
428/// the stores whose format changed in between. A SQLite database whose schema is newer
429/// than the older Chromium can read — cookies, autofill, history — is left on disk and
430/// unused, so cookies stop persisting until the application is upgraded again, and for
431/// some of them Chromium shows its own "profile is from a newer version" dialog; an
432/// IndexedDB store whose data format is newer may be discarded as corrupt.
433///
434/// Chrome keeps a `Last Version` breadcrumb in its user data directory to recognize a
435/// downgrade. CEF does not write it, so the runtime does, in [`Cef::root_cache_path`], and
436/// applies this policy when the breadcrumb names a higher milestone. A downgrade within a
437/// milestone, `152.0.7977.83` to `152.0.7977.50`, is left alone either way, as Chrome
438/// leaves it, and a profile another running instance of the application holds is never
439/// touched.
440#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
441#[non_exhaustive]
442pub enum DowngradePolicy {
443  /// Keep the profile and let Chromium open it, logging a warning that names both
444  /// versions.
445  ///
446  /// Nothing is deleted: whatever the older Chromium cannot read stays on disk and is back
447  /// once the application is upgraded again. This is what Chrome does for a downgrade
448  /// nobody administered.
449  #[default]
450  KeepProfile,
451  /// Move the root cache path aside and start on an empty profile, deleting the old one in
452  /// the background.
453  ///
454  /// The application starts as if freshly installed, on every platform and whatever
455  /// changed between the two versions: cookies and sessions, local storage, IndexedDB,
456  /// caches and granted permissions are gone, and a warning naming both versions is
457  /// logged. For an application whose state lives on a server, or that would rather have
458  /// every user start clean than some run on a half-readable profile.
459  ResetProfile,
460}
461
462/// Selects and configures the CEF runtime.
463///
464/// Pass it to `tauri::Builder::runtime` to run the application with CEF:
465///
466/// ```rust,no_run
467/// tauri::Builder::default().runtime(
468///   tauri_runtime_cef::Cef::default().command_line_arg("disable-gpu", None::<String>),
469/// );
470/// ```
471#[derive(Default)]
472pub struct Cef {
473  command_line_args: Vec<(String, Option<String>)>,
474  disabled_features: Vec<String>,
475  enabled_features: Vec<String>,
476  deep_link_schemes: Vec<String>,
477  cache_path: Option<PathBuf>,
478  api_version: Option<i32>,
479  secret_storage: SecretStorage,
480  profile_preferences: Vec<(String, serde_json::Value)>,
481  global_preferences: Vec<(String, serde_json::Value)>,
482  content_settings: Vec<(cef::ContentSettingTypes, cef::ContentSettingValues)>,
483  allow_chromium_command_line_args: bool,
484  log_file: Option<PathBuf>,
485  log_severity: Option<LogSeverity>,
486  log_items: Option<LogItems>,
487  locale: Option<String>,
488  accept_language_list: Option<String>,
489  user_agent: Option<String>,
490  user_agent_product: Option<String>,
491  javascript_flags: Option<String>,
492  chrome_policy_id: Option<String>,
493  persist_session_cookies: bool,
494  remote_debugging: RemoteDebugging,
495  devtools: DevToolsPolicy,
496  debug_environment: DebugEnvironment,
497  certificate_errors: CertificateErrorPolicy,
498  sandbox: SandboxPolicy,
499  downgrade: DowngradePolicy,
500  settings_callback: Option<Box<SettingsCallback>>,
501  window_event_handler: Option<Arc<NativeWindowEventHandler>>,
502}
503
504impl fmt::Debug for Cef {
505  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
506    f.debug_struct("Cef")
507      .field("command_line_args", &self.command_line_args)
508      .field("disabled_features", &self.disabled_features)
509      .field("enabled_features", &self.enabled_features)
510      .field("deep_link_schemes", &self.deep_link_schemes)
511      .field("cache_path", &self.cache_path)
512      .field("api_version", &self.api_version)
513      .field("secret_storage", &self.secret_storage)
514      .field("profile_preferences", &self.profile_preferences)
515      .field("global_preferences", &self.global_preferences)
516      .field("content_settings", &self.content_settings)
517      .field(
518        "allow_chromium_command_line_args",
519        &self.allow_chromium_command_line_args,
520      )
521      .field("log_file", &self.log_file)
522      .field("log_severity", &self.log_severity)
523      .field("log_items", &self.log_items)
524      .field("locale", &self.locale)
525      .field("accept_language_list", &self.accept_language_list)
526      // The user agent can carry an application identifier but nothing secret; the
527      // JavaScript flags and policy id are likewise plain configuration.
528      .field("user_agent", &self.user_agent)
529      .field("user_agent_product", &self.user_agent_product)
530      .field("javascript_flags", &self.javascript_flags)
531      .field("chrome_policy_id", &self.chrome_policy_id)
532      .field("persist_session_cookies", &self.persist_session_cookies)
533      .field("remote_debugging", &self.remote_debugging)
534      .field("devtools", &self.devtools)
535      .field("debug_environment", &self.debug_environment)
536      .field("certificate_errors", &self.certificate_errors)
537      .field("sandbox", &self.sandbox)
538      .field("downgrade", &self.downgrade)
539      .field("settings_callback", &self.settings_callback.is_some())
540      .field("window_event_handler", &self.window_event_handler.is_some())
541      .finish()
542  }
543}
544
545impl Cef {
546  /// Observes native input, resize and redraw events without replacing Tauri's handling.
547  ///
548  /// Runs before Tauri handles the event. Offscreen embedders forward input, focus and IME
549  /// to CEF and present their own GPU surface here.
550  /// Release GPU resources and retained native window references on `Destroyed`.
551  /// Registering this callback does not enable OSR or forward input automatically.
552  #[must_use]
553  pub fn on_window_event<F>(mut self, handler: F) -> Self
554  where
555    F: Fn(&str, &dyn ActiveEventLoop, Arc<dyn winit::window::Window>, &WinitWindowEvent)
556      + Send
557      + Sync
558      + 'static,
559  {
560    self.window_event_handler = Some(Arc::new(handler));
561    self
562  }
563
564  /// Sets a callback to customize the settings passed to [`cef::initialize`].
565  ///
566  /// If called more than once, only the last callback is used.
567  #[must_use]
568  pub fn with_settings<F>(mut self, callback: F) -> Self
569  where
570    F: FnOnce(&mut cef::Settings) + Send + Sync + 'static,
571  {
572    self.settings_callback = Some(Box::new(callback));
573    self
574  }
575
576  /// Appends one command line argument passed to CEF.
577  ///
578  /// The argument is applied to the **browser process only**. CEF warns that modifying
579  /// the command line of a non-browser process "may result in undefined behavior
580  /// including crashes", and Chromium already forwards to each child process the
581  /// switches it needs.
582  #[must_use]
583  pub fn command_line_arg<K: Into<String>, V: Into<String>>(
584    mut self,
585    key: K,
586    value: Option<V>,
587  ) -> Self {
588    self
589      .command_line_args
590      .push((key.into(), value.map(Into::into)));
591    self
592  }
593
594  /// Appends a list of command line arguments passed to CEF.
595  ///
596  /// Like [`Self::command_line_arg`], these are applied to the browser process only.
597  #[must_use]
598  pub fn command_line_args<K: Into<String>, V: Into<String>>(
599    mut self,
600    args: impl IntoIterator<Item = (K, Option<V>)>,
601  ) -> Self {
602    self
603      .command_line_args
604      .extend(args.into_iter().map(|(k, v)| (k.into(), v.map(Into::into))));
605    self
606  }
607
608  /// Appends a list of deep link schemes detected by CEF's on_already_running_app_relaunch hook.
609  ///
610  /// Deep links defined by the core deep-link plugin on the Tauri configuration are automatically added.
611  #[must_use]
612  pub fn deep_link_schemes<S: Into<String>>(
613    mut self,
614    schemes: impl IntoIterator<Item = S>,
615  ) -> Self {
616    self
617      .deep_link_schemes
618      .extend(schemes.into_iter().map(Into::into));
619    self
620  }
621
622  /// Directory used for CEF disk cache (`Settings::cache_path`): Chromium's user data
623  /// directory, holding `Local State`, every profile and the caches.
624  ///
625  /// If unspecified, defaults to `{user cache}/{app identifier}/cef`.
626  ///
627  /// Give CEF a directory of its own. The runtime keeps a `Last Version` file in it naming
628  /// the Chromium that ran last, and [`DowngradePolicy::ResetProfile`] moves the whole
629  /// directory aside when a newer Chromium milestone last used it — see [`Self::downgrade`].
630  #[must_use]
631  pub fn root_cache_path<P: AsRef<std::path::Path>>(mut self, path: P) -> Self {
632    self.cache_path = Some(path.as_ref().to_path_buf());
633    self
634  }
635
636  /// What to do with the profile on disk when a newer Chromium milestone last used it,
637  /// which is what a release rolled back to an older CEF finds.
638  ///
639  /// Defaults to [`DowngradePolicy::KeepProfile`]: the profile is opened as is and a
640  /// warning is logged, as Chrome does. [`DowngradePolicy::ResetProfile`] moves it aside
641  /// and starts clean instead. See [`DowngradePolicy`] for what each one costs.
642  #[must_use]
643  pub fn downgrade(mut self, policy: DowngradePolicy) -> Self {
644    self.downgrade = policy;
645    self
646  }
647
648  /// CEF API version this process declares (`cef_api_hash`), defaulting to
649  /// `cef::sys::CEF_API_VERSION_LAST`. Child processes declare the same one.
650  ///
651  /// The Rust bindings are compiled for `CEF_API_VERSION_LAST` whatever is declared
652  /// here. Declaring another version is only sound if the API the application uses is
653  /// the same in both.
654  #[must_use]
655  pub fn cef_api_version(mut self, version: i32) -> Self {
656    self.api_version = Some(version);
657    self
658  }
659
660  /// Which key Chromium uses to encrypt cookies and saved passwords at rest.
661  ///
662  /// Defaults to [`SecretStorage::Auto`]: development builds skip the OS secret store —
663  /// the macOS keychain prompt is replaced by a mock keychain, and Linux skips the D-Bus
664  /// secret portal, libsecret and KWallet — while release builds use it. Windows keeps
665  /// using DPAPI throughout.
666  ///
667  /// Nothing but cookies and saved passwords is affected. The mock keychain and the
668  /// Linux `basic` store encrypt with a hard-coded, publicly known constant, so reach
669  /// for [`SecretStorage::Mock`] only when a release build has to run where no secret
670  /// store exists at all. Switching between key sources makes previously stored cookies
671  /// unreadable. See [`SecretStorage`] for the details.
672  #[must_use]
673  pub fn secret_storage(mut self, storage: SecretStorage) -> Self {
674    self.secret_storage = storage;
675    self
676  }
677
678  /// Sets one boolean Chromium profile preference on every webview's request context.
679  ///
680  /// Applied after the runtime's own defaults, so it can turn a preference the runtime
681  /// disabled back on as well as turn something else off. Calling it twice for the same
682  /// preference keeps the last value.
683  ///
684  /// The runtime disables a handful of Chrome browser features that have no place in an
685  /// application webview — the "Save password?" and address and credit-card bubbles
686  /// (`credentials_enable_service`, `profile.password_manager_leak_detection`,
687  /// `autofill.profile_enabled`, `autofill.credit_card_enabled`), the translate bubble
688  /// (`translate.enabled`), and two background requests to Google
689  /// (`alternate_error_pages.enabled`, `search.suggest_enabled`). An application that
690  /// wants one of them names it here.
691  ///
692  /// Safe Browsing (`safebrowsing.enabled`) is left on by the runtime; an application
693  /// whose webview only ever loads its own content can switch it off here.
694  ///
695  /// Preference names are Chromium's own, and which ones a given Chrome build registers
696  /// as writable varies. A preference this build refuses is logged at debug and skipped.
697  ///
698  /// ```no_run
699  /// # use tauri_runtime_cef::Cef;
700  /// Cef::default()
701  ///   .profile_preference("credentials_enable_service", true)
702  ///   .profile_preference("safebrowsing.enabled", false);
703  /// ```
704  #[must_use]
705  pub fn profile_preference<K: Into<String>>(mut self, name: K, enabled: bool) -> Self {
706    self
707      .profile_preferences
708      .push((name.into(), serde_json::Value::Bool(enabled)));
709    self
710  }
711
712  /// Sets one Chromium profile preference of any type on every webview's request context.
713  ///
714  /// [`Self::profile_preference`] covers the common boolean case; this one takes the
715  /// integers, strings, lists and dictionaries the rest of Chromium's preferences are made
716  /// of. The value must have the type Chromium registered the preference with — a string
717  /// where an integer belongs is refused and logged at debug.
718  ///
719  /// Preferences worth knowing about, beyond the ones with a typed option of their own:
720  ///
721  /// | Preference | Type | What it does |
722  /// |---|---|---|
723  /// | `printing.enabled` | bool | whether `window.print()` opens Chrome's print preview |
724  /// | `download.default_directory` | string | where downloads land |
725  /// | `download.prompt_for_download` | bool | whether every download asks first |
726  /// | `enable_do_not_track` | bool | sends the `DNT` header |
727  /// | `enable_referrers` | bool | sends `Referer` at all |
728  /// | `dns_over_https.mode` | string | `off`, `automatic` or `secure` |
729  /// | `dns_over_https.templates` | string | space-delimited DoH server templates |
730  /// | `profile.cookie_controls_mode` | int | `1` blocks third-party cookies |
731  /// | `hardware.audio_capture_enabled` | bool | a hard kill switch for the microphone |
732  /// | `hardware.video_capture_enabled` | bool | the same for the camera |
733  ///
734  /// ```no_run
735  /// # use tauri_runtime_cef::Cef;
736  /// Cef::default()
737  ///   .profile_preference_value("dns_over_https.mode", "secure")
738  ///   .profile_preference_value("profile.cookie_controls_mode", 1);
739  /// ```
740  #[must_use]
741  pub fn profile_preference_value<K: Into<String>, V: Into<serde_json::Value>>(
742    mut self,
743    name: K,
744    value: V,
745  ) -> Self {
746    self.profile_preferences.push((name.into(), value.into()));
747    self
748  }
749
750  /// Sets one preference in Chromium's local state, the store shared by every profile.
751  ///
752  /// Most preferences belong to a profile and are set with
753  /// [`Self::profile_preference_value`]; a handful — `devtools.remote_debugging.allowed`
754  /// and `hardware_acceleration_mode.enabled` among them — live here instead. Applied once
755  /// the CEF context is initialized.
756  #[must_use]
757  pub fn global_preference<K: Into<String>, V: Into<serde_json::Value>>(
758    mut self,
759    name: K,
760    value: V,
761  ) -> Self {
762    self.global_preferences.push((name.into(), value.into()));
763    self
764  }
765
766  /// Sets the default value of one Chromium content setting for every origin.
767  ///
768  /// A content setting is the stored answer behind a permission: with a default of
769  /// [`ContentSettingValues::BLOCK`](cef::ContentSettingValues::BLOCK) a page cannot ask
770  /// at all, and with [`ALLOW`](cef::ContentSettingValues::ALLOW) it is granted without a
771  /// prompt. That makes this the application-wide policy that
772  /// `WebviewAttributes::on_permission_request` is not: the handler answers one request at
773  /// a time, this decides what can be requested.
774  ///
775  /// Applied to every webview's request context after it initializes, and stored in the
776  /// profile, so it also governs the origins the user has already answered for.
777  ///
778  /// Chromium refuses to write a default onto an off-the-record profile, so this has no
779  /// effect on a webview built with `WebviewAttributes::incognito`. Such a webview keeps
780  /// Chromium's own defaults, and `WebviewAttributes::on_permission_request` is the way to
781  /// answer for it.
782  ///
783  /// ```no_run
784  /// # use tauri_runtime_cef::Cef;
785  /// use tauri_runtime_cef::cef::{ContentSettingTypes, ContentSettingValues};
786  ///
787  /// Cef::default()
788  ///   // An application webview has no business asking for these.
789  ///   .default_content_setting(ContentSettingTypes::NOTIFICATIONS, ContentSettingValues::BLOCK)
790  ///   .default_content_setting(ContentSettingTypes::GEOLOCATION, ContentSettingValues::BLOCK)
791  ///   // Device access a desktop application rarely wants a page to reach.
792  ///   .default_content_setting(ContentSettingTypes::USB_GUARD, ContentSettingValues::BLOCK)
793  ///   .default_content_setting(ContentSettingTypes::SERIAL_GUARD, ContentSettingValues::BLOCK)
794  ///   .default_content_setting(ContentSettingTypes::HID_GUARD, ContentSettingValues::BLOCK);
795  /// ```
796  ///
797  /// Blocking [`JAVASCRIPT_JIT`](cef::ContentSettingTypes::JAVASCRIPT_JIT) is worth
798  /// knowing about separately: it runs V8 without its optimizing compilers, which removes
799  /// the largest single source of exploitable memory bugs in a renderer at a real cost in
800  /// JavaScript performance. It is the same lever as Chrome's `DefaultJavaScriptJitSetting`
801  /// policy.
802  #[must_use]
803  pub fn default_content_setting(
804    mut self,
805    content_type: cef::ContentSettingTypes,
806    value: cef::ContentSettingValues,
807  ) -> Self {
808    self.content_settings.push((content_type, value));
809    self
810  }
811
812  /// Lets Chromium read switches off the process command line in release builds.
813  ///
814  /// Release builds ignore them by default (`Settings::command_line_args_disabled`),
815  /// because otherwise anyone who can start the shipped executable can also start it
816  /// with `--remote-debugging-port` and drive the app over the DevTools protocol, or
817  /// with `--disable-web-security`, `--proxy-server`, `--host-resolver-rules` or
818  /// `--ssl-key-log-file` — Chromium honours every one of them. Development builds
819  /// (`tauri::is_dev()`) always keep the command line enabled.
820  ///
821  /// Enable this only if the application genuinely needs users to pass Chromium
822  /// switches. It is not a complete lockdown either way: the network service reads the
823  /// `SSLKEYLOGFILE` environment variable regardless of this setting.
824  ///
825  /// Switches configured through [`Self::command_line_arg`] are unaffected, because CEF
826  /// clears Chromium's command line before applying its own settings and before calling
827  /// `on_before_command_line_processing`. Tauri's own CLI parsing and its cold-start deep
828  /// link handling read `std::env::args()`, which Chromium never touches, and are
829  /// unaffected too. Deep links delivered to an *already running* instance do go through
830  /// Chromium's process singleton, so the runtime restores the deep link URL onto the
831  /// cleared command line to keep them working.
832  #[must_use]
833  pub fn allow_chromium_command_line_args(mut self, allow: bool) -> Self {
834    self.allow_chromium_command_line_args = allow;
835    self
836  }
837
838  /// File Chromium and CEF write their log to (`Settings::log_file`).
839  ///
840  /// Defaults to `cef.log` inside the cache directory (see [`Self::root_cache_path`]).
841  /// With no log file configured, CEF writes a `debug.log` into the *main executable
842  /// directory* on Windows and Linux, which for an installed application is often not
843  /// even writable.
844  ///
845  /// The default also overrides the macOS convention of
846  /// `~/Library/Logs/<app name>_debug.log`; pass that path explicitly to keep it.
847  #[must_use]
848  pub fn log_file<P: AsRef<std::path::Path>>(mut self, path: P) -> Self {
849    self.log_file = Some(path.as_ref().to_path_buf());
850    self
851  }
852
853  /// Lowest severity Chromium and CEF write to the log file (`Settings::log_severity`).
854  ///
855  /// Defaults to [`cef::LogSeverity::WARNING`] in release builds — CEF's own default is
856  /// `INFO`, which is chatty enough to grow the log file of a long-running application —
857  /// and to [`cef::LogSeverity::DEFAULT`] in development builds (`tauri::is_dev()`),
858  /// where the informational messages are usually what you want.
859  ///
860  /// [`cef::LogSeverity::DISABLE`] does not turn logging off entirely: CEF maps it to a
861  /// FATAL-only minimum level, so nothing is written to the log file but FATAL messages
862  /// still go to stderr.
863  #[must_use]
864  pub fn log_severity(mut self, severity: LogSeverity) -> Self {
865    self.log_severity = Some(severity);
866    self
867  }
868
869  /// Locale Chromium loads its own localized resources for (`Settings::locale`),
870  /// as an ISO language code such as `en-US` or `pt-BR`.
871  ///
872  /// Leave unset — the default — unless you know the matching pak file ships with the
873  /// application. Tauri's bundler packages **only the `en-US` locale pak**, so naming any
874  /// other locale leaves Chromium unable to load the localized strings it uses for its
875  /// own UI (context menus, error pages, form controls). This does not affect the
876  /// application's own content, nor which languages a website is asked for — that is
877  /// [`Self::accept_language_list`].
878  #[must_use]
879  pub fn locale<S: Into<String>>(mut self, locale: S) -> Self {
880    self.locale = Some(locale.into());
881    self
882  }
883
884  /// Comma-delimited list of languages sent as the `Accept-Language` header and reported
885  /// through `navigator.language` (`Settings::accept_language_list`), for example
886  /// `en-US,en,pt-BR`.
887  ///
888  /// Defaults to CEF's own value, which is derived from [`Self::locale`].
889  #[must_use]
890  pub fn accept_language_list<S: Into<String>>(mut self, languages: S) -> Self {
891    self.accept_language_list = Some(languages.into());
892    self
893  }
894
895  /// What to do with Chromium's process sandbox.
896  ///
897  /// Defaults to [`SandboxPolicy::Auto`], which keeps the sandbox except when the
898  /// application runs from an AppImage on a Linux or BSD system that offers no way to
899  /// sandbox at all — AppImages cannot ship the setuid `chrome-sandbox` helper the deb
900  /// and rpm bundlers install, and distributions such as Ubuntu 23.10 and later restrict
901  /// the unprivileged user namespaces Chromium would otherwise fall back to. Without the
902  /// escape hatch Chromium aborts at startup with "No usable sandbox!".
903  ///
904  /// See [`SandboxPolicy`] for the other variants.
905  #[must_use]
906  pub fn sandbox(mut self, policy: SandboxPolicy) -> Self {
907    self.sandbox = policy;
908    self
909  }
910
911  /// Adds names to Chromium's `--disable-features` list, keeping what is already there.
912  ///
913  /// Use this rather than `command_line_arg("disable-features", ...)`. Chromium stores a
914  /// switch by name and the last value appended replaces the previous one, so a raw
915  /// `--disable-features` does not add to the list — it *becomes* the list, dropping the
916  /// entries CEF put there to keep Chrome from crashing at startup and to keep renderers
917  /// from being killed on the runtime's own requests.
918  #[must_use]
919  pub fn disable_features<S: Into<String>>(
920    mut self,
921    features: impl IntoIterator<Item = S>,
922  ) -> Self {
923    self
924      .disabled_features
925      .extend(features.into_iter().map(Into::into));
926    self
927  }
928
929  /// Adds names to Chromium's `--enable-features` list, keeping what is already there.
930  ///
931  /// See [`Self::disable_features`] for why the raw switch is the wrong tool.
932  #[must_use]
933  pub fn enable_features<S: Into<String>>(mut self, features: impl IntoIterator<Item = S>) -> Self {
934    self
935      .enabled_features
936      .extend(features.into_iter().map(Into::into));
937    self
938  }
939
940  /// Whether Chromium and CEF may read their diagnostic environment variables.
941  ///
942  /// Chromium honours `SSLKEYLOGFILE` — which writes the keys that decrypt every TLS
943  /// session the application makes — and CEF honours three variables that redirect crash
944  /// reports, whose minidumps carry process memory. Neither group is reachable through a
945  /// CEF setting, so each is refused where Chromium reads it: the key log through an
946  /// empty `--ssl-key-log-file`, which Chromium consults ahead of the variable, and the
947  /// crash overrides by taking them out of the environment before CEF starts.
948  ///
949  /// Defaults to [`DebugEnvironment::Auto`]: honoured in development builds
950  /// (`tauri::is_dev()`), refused in release builds.
951  #[must_use]
952  pub fn debug_environment(mut self, policy: DebugEnvironment) -> Self {
953    self.debug_environment = policy;
954    self
955  }
956
957  /// Whether Chromium's DevTools protocol server runs, and how it is reached.
958  ///
959  /// Defaults to [`RemoteDebugging::Disabled`], which additionally pins Chromium's
960  /// `devtools.remote_debugging.allowed` preference off so the server is refused even if
961  /// the switch reaches Chromium another way.
962  ///
963  /// See [`RemoteDebugging`] for what each transport exposes.
964  #[must_use]
965  pub fn remote_debugging(mut self, remote_debugging: RemoteDebugging) -> Self {
966    self.remote_debugging = remote_debugging;
967    self
968  }
969
970  /// Whether this application may open a DevTools window at all.
971  ///
972  /// Defaults to [`DevToolsPolicy::Auto`], which refuses them in a build that could not
973  /// open them anyway. See [`DevToolsPolicy`] for how this combines with the per-webview
974  /// `WebviewAttributes::devtools`, and for why it leaves the DevTools protocol alone.
975  #[must_use]
976  pub fn devtools(mut self, policy: DevToolsPolicy) -> Self {
977    self.devtools = policy;
978    self
979  }
980
981  /// What to do about a navigation to a server whose TLS certificate does not validate.
982  ///
983  /// Defaults to [`CertificateErrorPolicy::ChromeInterstitial`], which is Chromium's own
984  /// behaviour: an interstitial the user can click through.
985  #[must_use]
986  pub fn certificate_errors(mut self, policy: CertificateErrorPolicy) -> Self {
987    self.certificate_errors = policy;
988    self
989  }
990
991  /// How Chromium resolves the proxy for every request.
992  ///
993  /// Defaults to [`ProxyConfig::System`], Chromium's own behaviour.
994  #[must_use]
995  pub fn proxy(mut self, proxy: ProxyConfig) -> Self {
996    self
997      .profile_preferences
998      .push(("proxy".to_string(), proxy.to_preference()));
999    self
1000  }
1001
1002  /// When a page may start playing media on its own.
1003  #[must_use]
1004  pub fn autoplay(mut self, policy: AutoplayPolicy) -> Self {
1005    if let Some(value) = policy.as_switch_value() {
1006      self
1007        .command_line_args
1008        .push(("--autoplay-policy".to_string(), Some(value.to_string())));
1009    }
1010    self
1011  }
1012
1013  /// Which local network interfaces WebRTC may reveal to a page.
1014  #[must_use]
1015  pub fn webrtc_ip_handling(mut self, policy: WebRtcIpHandling) -> Self {
1016    if let Some(value) = policy.as_switch_value() {
1017      self.command_line_args.push((
1018        "--webrtc-ip-handling-policy".to_string(),
1019        Some(value.to_string()),
1020      ));
1021    }
1022    self
1023  }
1024
1025  /// Whether Chromium's spell checker runs.
1026  ///
1027  /// On by default. The first use of a language downloads its dictionary from Google's
1028  /// `redirector.gvt1.com`, which is the only reason an application that never shows an
1029  /// editable field might want it off. The remote spelling *service*, which would send
1030  /// typed text to Google, is off either way.
1031  #[must_use]
1032  pub fn spell_checking(mut self, enabled: bool) -> Self {
1033    self.profile_preferences.push((
1034      "browser.enable_spellchecking".to_string(),
1035      serde_json::Value::Bool(enabled),
1036    ));
1037    self
1038  }
1039
1040  /// Whether Chromium's Safe Browsing protection runs.
1041  ///
1042  /// On by default, and worth keeping on for any webview that loads content the
1043  /// application does not control — an OAuth or SSO flow, an embedded third-party page.
1044  /// Standard protection checks a locally stored hash-prefix database rather than calling
1045  /// Google per navigation, and keeping it updated is the periodic request an application
1046  /// that only ever loads its own content might want to be rid of.
1047  #[must_use]
1048  pub fn safe_browsing(mut self, enabled: bool) -> Self {
1049    self.profile_preferences.push((
1050      "safebrowsing.enabled".to_string(),
1051      serde_json::Value::Bool(enabled),
1052    ));
1053    self
1054  }
1055
1056  /// Whether Chromium's component updater runs.
1057  ///
1058  /// On by default, and it is the mechanism that keeps the certificate revocation set,
1059  /// the Certificate Transparency log list and the download file-type policies current —
1060  /// security data that goes stale. Turn it off only for a deployment that has no route
1061  /// to `update.googleapis.com` at all.
1062  #[must_use]
1063  pub fn component_updates(mut self, enabled: bool) -> Self {
1064    if !enabled {
1065      self
1066        .command_line_args
1067        .push(("--disable-component-update".to_string(), None));
1068    }
1069    self
1070  }
1071
1072  /// Value returned as the `User-Agent` header and `navigator.userAgent`, for every
1073  /// webview in the process (`CefSettings.user_agent`).
1074  ///
1075  /// Replacing the whole string drops the Chrome and platform tokens sites branch on, so
1076  /// prefer [`Self::user_agent_product`], which keeps them. A single webview can override
1077  /// this through `WebviewAttributes::user_agent`.
1078  #[must_use]
1079  pub fn user_agent<S: Into<String>>(mut self, user_agent: S) -> Self {
1080    self.user_agent = Some(user_agent.into());
1081    self
1082  }
1083
1084  /// Product token spliced into Chromium's own User-Agent string, such as `MyApp/1.2.0`
1085  /// (`CefSettings.user_agent_product`).
1086  ///
1087  /// Ignored when [`Self::user_agent`] is set.
1088  #[must_use]
1089  pub fn user_agent_product<S: Into<String>>(mut self, product: S) -> Self {
1090    self.user_agent_product = Some(product.into());
1091    self
1092  }
1093
1094  /// Whether session cookies survive a restart (`CefSettings.persist_session_cookies`).
1095  ///
1096  /// Off by default, matching a browser: a session cookie is dropped when the application
1097  /// exits. A desktop application that should keep users signed in across restarts wants
1098  /// this on, and should know that it writes those cookies to the cache directory, where
1099  /// they are only as protected as [`SecretStorage`] makes them.
1100  #[must_use]
1101  pub fn persist_session_cookies(mut self, persist: bool) -> Self {
1102    self.persist_session_cookies = persist;
1103    self
1104  }
1105
1106  /// Flags passed to V8 (`CefSettings.javascript_flags`), such as
1107  /// `--max-old-space-size=512`.
1108  ///
1109  /// Use this rather than `command_line_arg("js-flags", ...)`, which replaces the value
1110  /// CEF derives from this setting instead of adding to it.
1111  #[must_use]
1112  pub fn javascript_flags<S: Into<String>>(mut self, flags: S) -> Self {
1113    self.javascript_flags = Some(flags.into());
1114    self
1115  }
1116
1117  /// Enables Chrome policy management, reading policies from the platform location this
1118  /// identifier names (`CefSettings.chrome_policy_id`).
1119  ///
1120  /// The identifier is a registry key on Windows (`SOFTWARE\\Policies\\Vendor\\App`), a
1121  /// bundle identifier on macOS, and a directory on Linux (`/etc/opt/vendor/app/policies`).
1122  /// Set it for an application deployed by an IT department that has to configure it
1123  /// centrally; leave it unset otherwise, since it lets whoever controls that location
1124  /// change the application's behaviour.
1125  #[must_use]
1126  pub fn chrome_policy_id<S: Into<String>>(mut self, policy_id: S) -> Self {
1127    self.chrome_policy_id = Some(policy_id.into());
1128    self
1129  }
1130
1131  /// Which fields CEF prepends to each line of the log file (`CefSettings.log_items`).
1132  ///
1133  /// Defaults to CEF's own choice.
1134  #[must_use]
1135  pub fn log_items(mut self, items: LogItems) -> Self {
1136    self.log_items = Some(items);
1137    self
1138  }
1139}
1140
1141impl<T: UserEvent> tauri_runtime::RuntimeInitAttrs<T> for Cef {
1142  type Runtime = CefRuntime<T>;
1143
1144  fn apply_config(&mut self, config: &tauri_utils::config::Config) -> Result<()> {
1145    if let Some(plugin_config) = config
1146      .plugins
1147      .0
1148      .get("deep-link")
1149      .and_then(|config| config.get("desktop").cloned())
1150    {
1151      #[derive(serde::Deserialize)]
1152      #[serde(untagged)]
1153      enum DesktopDeepLinks {
1154        One(tauri_utils::config::DeepLinkProtocol),
1155        List(Vec<tauri_utils::config::DeepLinkProtocol>),
1156      }
1157
1158      let protocols: DesktopDeepLinks =
1159        serde_json::from_value(plugin_config).map_err(tauri_runtime::Error::Json)?;
1160      let schemes = match protocols {
1161        DesktopDeepLinks::One(protocol) => protocol.schemes,
1162        DesktopDeepLinks::List(protocols) => protocols
1163          .into_iter()
1164          .flat_map(|protocol| protocol.schemes)
1165          .collect(),
1166      };
1167
1168      self.deep_link_schemes.extend(schemes);
1169    }
1170    Ok(())
1171  }
1172}
1173
1174impl<T: UserEvent> From<Cef> for tauri_runtime::dynamic::DynRuntimeInitAttrs<T> {
1175  fn from(attrs: Cef) -> Self {
1176    Self::new(attrs)
1177  }
1178}
1179
1180/// Information about the CEF webview that requested a new window.
1181pub struct NewWindowOpener {
1182  source_url: Option<url::Url>,
1183}
1184
1185impl NewWindowOpener {
1186  pub(crate) fn new(source_url: Option<url::Url>) -> Self {
1187    Self { source_url }
1188  }
1189
1190  /// The opener's main-frame URL at the native popup request, when available.
1191  ///
1192  /// CEF supplies this directly from the callback's browser. Reading a blocking
1193  /// webview getter from that callback can deadlock the UI thread because CEF's
1194  /// external message pump may run outside a winit dispatch callback.
1195  pub fn source_url(&self) -> Option<&url::Url> {
1196    self.source_url.as_ref()
1197  }
1198}
1199
1200impl std::fmt::Debug for NewWindowOpener {
1201  fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1202    // The URL can carry credentials and tokens, so only its presence is shown.
1203    formatter
1204      .debug_struct("NewWindowOpener")
1205      .field("source_url_observed", &self.source_url.is_some())
1206      .finish()
1207  }
1208}
1209
1210#[derive(Clone, Debug)]
1211pub struct EventProxy<T: UserEvent> {
1212  context: RuntimeContext<T>,
1213}
1214
1215impl<T: UserEvent> EventLoopProxy<T> for EventProxy<T> {
1216  fn send_event(&self, event: T) -> Result<()> {
1217    self.context.send_message(Message::UserEvent(event))
1218  }
1219}
1220
1221#[derive(Clone)]
1222pub(crate) struct RuntimeContext<T: UserEvent> {
1223  pub(crate) sender: Sender<Message<T>>,
1224  /// Only ever woken through [`Self::wake_event_loop`].
1225  #[cfg(not(target_os = "macos"))]
1226  proxy: WinitEventLoopProxy,
1227  /// Pokes the main run loop, which then delivers pending wake-ups at the start of its next
1228  /// pass through [`MainThreadDispatchSlot::deliver_wake`], the only place the proxy is
1229  /// woken on macOS. See [`Self::wake_event_loop`].
1230  #[cfg(target_os = "macos")]
1231  wake: crate::platform::macos::MainThreadWake,
1232  main_thread_id: std::thread::ThreadId,
1233  next_window_id: Arc<AtomicU32>,
1234  next_webview_id: Arc<AtomicU32>,
1235  next_window_event_id: Arc<AtomicU32>,
1236  next_webview_event_id: Arc<AtomicU32>,
1237  current_dispatch: Arc<MainThreadDispatchSlot<T>>,
1238  pub(crate) app_wide_theme: Arc<Mutex<Option<Theme>>>,
1239  pub(crate) cef_pump: CefExternalPump,
1240  /// Root cache path passed to [`cef::Settings::cache_path`] during
1241  /// [`cef::initialize`]. Per-webview `data_directory` profiles must resolve
1242  /// under this root for CEF request contexts to be accepted.
1243  pub(crate) cache_path: Arc<PathBuf>,
1244  /// Chromium profile preferences the application asked for, applied to every
1245  /// webview's request context after the runtime's own defaults. See
1246  /// [`Cef::profile_preference`].
1247  pub(crate) profile_preferences: Arc<Vec<(String, serde_json::Value)>>,
1248  /// Default content settings the application asked for, applied to every webview's
1249  /// request context. See [`Cef::default_content_setting`].
1250  pub(crate) content_settings: Arc<Vec<(cef::ContentSettingTypes, cef::ContentSettingValues)>>,
1251  /// What to do about a navigation whose TLS certificate does not validate. See
1252  /// [`Cef::certificate_errors`].
1253  pub(crate) certificate_errors: CertificateErrorPolicy,
1254  /// Whether [`Cef::devtools`] lets this application open DevTools at all. Combined with
1255  /// the per-webview `WebviewAttributes::devtools`, which can only narrow it further.
1256  pub(crate) devtools_allowed: bool,
1257  window_event_handler: Option<Arc<NativeWindowEventHandler>>,
1258}
1259
1260/// Scoped access to the current winit callback state.
1261///
1262/// `ActiveEventLoop` is only borrowed during `ApplicationHandler` callbacks, but
1263/// setup-time runtime messages may synchronously need it. While a callback is
1264/// active, this slot lets main-thread `send_message` handle work immediately;
1265/// other threads still queue and wake the loop. The slot stores an atomic
1266/// pointer to guard-owned state so lookup is lock-free. The guard restores the
1267/// previous pointer before dropping that state, so the raw pointers are never
1268/// treated as valid beyond their callback.
1269#[derive(Clone, Copy)]
1270struct MainThreadDispatch<T: UserEvent> {
1271  app: *mut WinitCefApp<T>,
1272  event_loop: *const dyn ActiveEventLoop,
1273}
1274
1275struct MainThreadDispatchSlot<T: UserEvent> {
1276  current: AtomicPtr<MainThreadDispatch<T>>,
1277  /// Whether winit's proxy still has to be signalled, see [`Self::deliver_wake`].
1278  #[cfg(target_os = "macos")]
1279  wake_pending: AtomicBool,
1280}
1281
1282impl<T: UserEvent> MainThreadDispatchSlot<T> {
1283  fn install(&self, dispatch: &mut MainThreadDispatch<T>) -> *mut MainThreadDispatch<T> {
1284    self.current.swap(dispatch, Ordering::AcqRel)
1285  }
1286
1287  /// Whether a winit callback is running on the main thread right now.
1288  #[cfg(target_os = "macos")]
1289  fn is_active(&self) -> bool {
1290    !self.current.load(Ordering::Acquire).is_null()
1291  }
1292
1293  /// Signals winit's `proxy` if a wake-up is pending and no callback is running; the wake-up
1294  /// stays pending otherwise. Runs on the main thread at the start of every run-loop pass,
1295  /// see [`RuntimeContext::wake_event_loop`] for why it must happen there.
1296  #[cfg(target_os = "macos")]
1297  fn deliver_wake(&self, proxy: &WinitEventLoopProxy) {
1298    if self.is_active() || !self.wake_pending.load(Ordering::Acquire) {
1299      return;
1300    }
1301    // A read-modify-write rather than a store: a request racing with this pass is either
1302    // consumed here, and its message is then visible to the drain that follows, or stays
1303    // pending for the next pass.
1304    self.wake_pending.swap(false, Ordering::AcqRel);
1305    proxy.wake_up();
1306  }
1307
1308  fn restore(&self, current: *mut MainThreadDispatch<T>, previous: *mut MainThreadDispatch<T>) {
1309    let installed = self.current.swap(previous, Ordering::AcqRel);
1310    debug_assert_eq!(installed, current);
1311  }
1312
1313  fn current(&self) -> Option<&MainThreadDispatch<T>> {
1314    let current = self.current.load(Ordering::Acquire);
1315    if current.is_null() {
1316      None
1317    } else {
1318      // SAFETY: the pointer targets the boxed dispatch state owned by
1319      // `MainThreadDispatchGuard`, whose allocation remains stable while the
1320      // guard is moved. The slot is restored before that state is dropped, and
1321      // it is only read by `send_message` after verifying that it is running on
1322      // the runtime main thread.
1323      Some(unsafe { &*current })
1324    }
1325  }
1326}
1327
1328impl<T: UserEvent> Default for MainThreadDispatchSlot<T> {
1329  fn default() -> Self {
1330    Self {
1331      current: AtomicPtr::new(std::ptr::null_mut()),
1332      #[cfg(target_os = "macos")]
1333      wake_pending: AtomicBool::new(false),
1334    }
1335  }
1336}
1337
1338struct MainThreadDispatchGuard<T: UserEvent> {
1339  context: RuntimeContext<T>,
1340  dispatch: Box<MainThreadDispatch<T>>,
1341  previous: *mut MainThreadDispatch<T>,
1342}
1343
1344impl<T: UserEvent> Drop for MainThreadDispatchGuard<T> {
1345  fn drop(&mut self) {
1346    self
1347      .context
1348      .current_dispatch
1349      .restore(self.dispatch.as_mut(), self.previous);
1350  }
1351}
1352
1353/// Whether handling `message` ends in the application's `RunEvent` callback.
1354///
1355/// Those messages must never be handled inline by [`handle_main_thread_message`]: see
1356/// the deadlock it documents.
1357fn dispatches_run_event<T: UserEvent>(message: &Message<T>) -> bool {
1358  match message {
1359    Message::UserEvent(_) | Message::Opened(_) => true,
1360    #[cfg(target_os = "macos")]
1361    Message::Reopen { .. } => true,
1362    _ => false,
1363  }
1364}
1365
1366#[allow(clippy::result_large_err)]
1367fn handle_main_thread_message<T: UserEvent>(
1368  context: &RuntimeContext<T>,
1369  message: Message<T>,
1370) -> std::result::Result<(), Message<T>> {
1371  // A message that reaches the application's run callback goes through the event loop
1372  // even on the main thread, because the main thread may be several frames deep inside
1373  // a callback already — anything that spins a nested loop, such as muda's GTK4 context
1374  // menu, gets here from the closure it is blocking on. Running the callback from there
1375  // re-enters the application while it holds locks meant for one pass of the loop:
1376  // `Window::popup_menu` reaches the runtime through a plugin command, which holds
1377  // Tauri's plugin store lock across the popup and blocks a worker on the main thread,
1378  // while `on_event_loop_event` takes that same lock to deliver the menu event —
1379  // deadlocking both threads. Queueing keeps the callback where the loop can only be in
1380  // one pass at a time, and matches what the wry runtime's proxy does.
1381  if dispatches_run_event(&message) {
1382    return Err(message);
1383  }
1384
1385  let Some(dispatch) = context.current_dispatch.current() else {
1386    return Err(message);
1387  };
1388
1389  // SAFETY: `WinitCefApp::install_current_dispatch` stores pointers to the currently
1390  // executing winit application handler and event-loop callback. This function
1391  // is only called on the runtime main thread while that callback is active.
1392  let app = unsafe { &mut *dispatch.app };
1393  let event_loop = unsafe { &*dispatch.event_loop };
1394
1395  app.handle_message(event_loop, message);
1396
1397  Ok(())
1398}
1399
1400impl<T: UserEvent> fmt::Debug for RuntimeContext<T> {
1401  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1402    f.debug_struct("RuntimeContext").finish()
1403  }
1404}
1405
1406impl<T: UserEvent> RuntimeContext<T> {
1407  pub(crate) fn send_message(&self, message: Message<T>) -> Result<()> {
1408    let message = if self.is_main_thread() {
1409      match handle_main_thread_message(self, message) {
1410        Ok(()) => return Ok(()),
1411        Err(message) => message,
1412      }
1413    } else {
1414      message
1415    };
1416
1417    self
1418      .sender
1419      .send(message)
1420      .map_err(|_| Error::FailedToSendMessage)?;
1421    self.wake_event_loop();
1422    Ok(())
1423  }
1424
1425  /// Has winit call [`WinitCefApp::proxy_wake_up`], which drains the message queue.
1426  ///
1427  /// Every wake-up of the event loop goes through here; on macOS the context does not even
1428  /// hold the proxy, only [`MainThreadDispatchSlot::deliver_wake`] signals it (see below).
1429  pub(crate) fn wake_event_loop(&self) {
1430    #[cfg(not(target_os = "macos"))]
1431    self.proxy.wake_up();
1432
1433    // winit's proxy is a run-loop source in the common modes, so it also fires while AppKit
1434    // tracks a menu or runs a modal panel. If that nested loop was started from a winit
1435    // callback — a `run_on_main_thread` closure showing a context menu, say — winit finds its
1436    // handler still borrowed and panics ("tried to handle event while another event is
1437    // currently being handled"). So the proxy is not signalled here. The request is recorded
1438    // and the main run loop poked; at the start of its next pass, `deliver_wake` signals the
1439    // proxy if no callback is running, or keeps the request pending until the callback
1440    // returns. A proxy signalled at that point of a pass is performed in the same pass, before
1441    // any AppKit event can reach a callback (see `platform::macos::MainThreadWake`).
1442    //
1443    // Consequently, messages from other threads wait while a nested loop started from a
1444    // callback runs: a blocking getter called from a worker returns once the menu or panel
1445    // closes, and a task meant to close it from a worker cannot run before it does.
1446    #[cfg(target_os = "macos")]
1447    {
1448      self
1449        .current_dispatch
1450        .wake_pending
1451        .store(true, Ordering::Release);
1452      self.wake.wake_up();
1453    }
1454  }
1455
1456  pub(crate) fn is_main_thread(&self) -> bool {
1457    std::thread::current().id() == self.main_thread_id
1458  }
1459
1460  /// Waits for the result of a `CreateWindow` or `CreateWebview` message just sent.
1461  ///
1462  /// On the main thread outside a winit callback the message is queued, not handled
1463  /// inline, and blocking on it would deadlock the loop that has to handle it. CEF calls
1464  /// back from its external message pump that way: a new-window handler building a window
1465  /// from `on_before_popup` would freeze the app. The creation then completes on the
1466  /// loop's next pass and a failure is only logged (see `Message::CreateWindow`).
1467  pub(crate) fn wait_for_creation(&self, result_rx: Receiver<Result<()>>) -> Result<()> {
1468    if self.is_main_thread() {
1469      return match result_rx.try_recv() {
1470        Ok(result) => result,
1471        Err(mpsc::TryRecvError::Empty) => Ok(()),
1472        Err(mpsc::TryRecvError::Disconnected) => Err(Error::FailedToReceiveMessage),
1473      };
1474    }
1475    result_rx
1476      .recv()
1477      .map_err(|_| Error::FailedToReceiveMessage)?
1478  }
1479
1480  /// Run `f` on the main (event-loop) thread.
1481  ///
1482  /// When called from the main thread we execute `f` inline instead of posting
1483  /// it to the channel. Tauri implements several blocking getters as
1484  /// `run_on_main_thread(|| { .. tx.send(..) }); rx.recv()` (e.g.
1485  /// `Window::add_child`). Those run during `setup`, which the runtime drives
1486  /// from winit's `can_create_surfaces`; posting the closure instead of running
1487  /// it inline would deadlock because the loop cannot drain the task while the
1488  /// main thread blocks on `rx.recv()`.
1489  pub(crate) fn run_on_main_thread<F: FnOnce() + Send + 'static>(&self, f: F) -> Result<()> {
1490    if self.is_main_thread() {
1491      f();
1492      Ok(())
1493    } else {
1494      self.send_message(Message::Task(Box::new(f)))
1495    }
1496  }
1497
1498  pub(crate) fn next_window_id(&self) -> WindowId {
1499    self.next_window_id.fetch_add(1, Ordering::Relaxed).into()
1500  }
1501
1502  pub(crate) fn next_webview_id(&self) -> u32 {
1503    self.next_webview_id.fetch_add(1, Ordering::Relaxed)
1504  }
1505
1506  pub(crate) fn next_window_event_id(&self) -> u32 {
1507    self.next_window_event_id.fetch_add(1, Ordering::Relaxed)
1508  }
1509
1510  pub(crate) fn next_webview_event_id(&self) -> u32 {
1511    self.next_webview_event_id.fetch_add(1, Ordering::Relaxed)
1512  }
1513}
1514
1515pub(crate) type AfterWindowCreationCallback = Box<dyn for<'a> Fn(RawWindow<'a>) + Send>;
1516
1517pub(crate) enum Message<T: UserEvent> {
1518  EventLoop(EventLoopMessage),
1519  BrowserClosed(WindowId, u32),
1520  PopupPending(crate::popup::PopupRequest, Arc<crate::popup::PopupFamily>),
1521  PopupCreated(
1522    crate::popup::PopupRequest,
1523    i32,
1524    Arc<crate::popup::PopupFamily>,
1525  ),
1526  PopupAborted(crate::popup::PopupRequest),
1527  PopupClosed(i32),
1528  /// CEF handed us the teardown of a webview's browser, keyed by the webview's
1529  /// process-unique id. See `TauriCefChildLifeSpanHandler::do_close`.
1530  #[cfg(any(target_os = "macos", windows))]
1531  DestroyWebviewHostWindow(u32),
1532  Opened(Vec<url::Url>),
1533  #[cfg(target_os = "macos")]
1534  Reopen {
1535    has_visible_windows: bool,
1536  },
1537  #[cfg(target_os = "macos")]
1538  AccessibilityChanged {
1539    enabled: bool,
1540  },
1541  CreateWindow {
1542    window_id: WindowId,
1543    webview_id: Option<u32>,
1544    pending: Box<PendingWindow<T, CefRuntime<T>>>,
1545    after_window_creation: Option<AfterWindowCreationCallback>,
1546    result_tx: Sender<Result<()>>,
1547  },
1548  CreateWebview {
1549    window_id: WindowId,
1550    webview_id: u32,
1551    pending: Box<PendingWebview<T, CefRuntime<T>>>,
1552    result_tx: Sender<Result<()>>,
1553  },
1554  Window {
1555    window_id: WindowId,
1556    message: WindowMessage,
1557  },
1558  Webview {
1559    window_id: WindowId,
1560    webview_id: u32,
1561    message: WebviewMessage,
1562  },
1563  NavigateFirstWebview {
1564    window_id: WindowId,
1565    url: String,
1566  },
1567  DragDropScriptEvent {
1568    window_id: WindowId,
1569    webview_id: u32,
1570    target: browser_client::DragDropEventTarget,
1571    drag_drop_state: Arc<Mutex<browser_client::DragDropState>>,
1572    event: browser_client::DragDropScriptEvent,
1573  },
1574  Task(Box<dyn FnOnce() + Send>),
1575  RequestExit(i32),
1576  UserEvent(T),
1577}
1578
1579fn device_event_filter_to_winit(filter: DeviceEventFilter) -> winit::event_loop::DeviceEvents {
1580  match filter {
1581    DeviceEventFilter::Always => winit::event_loop::DeviceEvents::Never,
1582    DeviceEventFilter::Unfocused => winit::event_loop::DeviceEvents::WhenFocused,
1583    DeviceEventFilter::Never => winit::event_loop::DeviceEvents::Always,
1584  }
1585}
1586
1587pub(crate) enum EventLoopMessage {
1588  SetTheme(Option<Theme>),
1589  SetDeviceEventFilter(DeviceEventFilter),
1590  PrimaryMonitor(Sender<Result<Option<Monitor>>>),
1591  MonitorFromPoint(Sender<Result<Option<Monitor>>>, f64, f64),
1592  AvailableMonitors(Sender<Result<Vec<Monitor>>>),
1593  CursorPosition(Sender<Result<PhysicalPosition<f64>>>),
1594  DisplayHandle(Sender<std::result::Result<SendRawDisplayHandle, raw_window_handle::HandleError>>),
1595  #[cfg(target_os = "macos")]
1596  SetActivationPolicy(tauri_runtime::ActivationPolicy),
1597  #[cfg(target_os = "macos")]
1598  SetDockVisibility(bool),
1599  #[cfg(target_os = "macos")]
1600  ShowApplication,
1601  #[cfg(target_os = "macos")]
1602  HideApplication,
1603}
1604
1605#[derive(Debug)]
1606pub(crate) struct WinitDragDropState {
1607  id: DataTransferId,
1608  paths: Option<Vec<PathBuf>>,
1609  paths_requested: bool,
1610  enter_position: Option<PhysicalPosition<f64>>,
1611  latest_position: Option<PhysicalPosition<f64>>,
1612  enter_emitted: bool,
1613  drop_pending: bool,
1614}
1615
1616impl WinitDragDropState {
1617  fn position(&self) -> PhysicalPosition<f64> {
1618    self
1619      .latest_position
1620      .or(self.enter_position)
1621      .unwrap_or_default()
1622  }
1623}
1624
1625fn pending_native_drag_enter(
1626  native_drag_drop: &mut Option<WinitDragDropState>,
1627) -> Option<DragDropEvent> {
1628  native_drag_drop.as_mut().and_then(|state| {
1629    if state.enter_emitted {
1630      return None;
1631    }
1632
1633    let paths = state.paths.clone()?;
1634    let position = state.position();
1635
1636    state.enter_emitted = true;
1637    Some(DragDropEvent::Enter { paths, position })
1638  })
1639}
1640
1641fn pending_native_drag_drop(
1642  native_drag_drop: &mut Option<WinitDragDropState>,
1643) -> Option<DragDropEvent> {
1644  native_drag_drop.as_mut().and_then(|state| {
1645    if !state.drop_pending || !state.enter_emitted {
1646      return None;
1647    }
1648
1649    let paths = state.paths.clone()?;
1650    let position = state.position();
1651
1652    Some(DragDropEvent::Drop { paths, position })
1653  })
1654}
1655
1656fn request_native_drag_paths(
1657  event_loop: &dyn ActiveEventLoop,
1658  native_drag_drop: &mut Option<WinitDragDropState>,
1659) {
1660  let Some(state) = native_drag_drop else {
1661    return;
1662  };
1663  let id = state.id;
1664  if state.paths.is_some() || state.paths_requested {
1665    return;
1666  }
1667
1668  if event_loop
1669    .fetch_data_transfer(id, &TypeHint::UriList)
1670    .is_err()
1671  {
1672    *native_drag_drop = None;
1673    let _ = event_loop.set_valid_dnd_actions(id, &[]);
1674  } else if let Some(state) = native_drag_drop {
1675    state.paths_requested = true;
1676  }
1677}
1678
1679macro_rules! event_loop_getter {
1680  ($self:ident, $variant:ident) => {{
1681    let (tx, rx) = mpsc::channel();
1682    match $self
1683      .context
1684      .send_message(Message::EventLoop(EventLoopMessage::$variant(tx)))
1685    {
1686      Ok(()) => rx.recv().map_err(|_| Error::FailedToReceiveMessage),
1687      Err(error) => Err(error),
1688    }
1689  }};
1690}
1691
1692fn find_monitor_from_point(
1693  monitors: impl Iterator<Item = winit::monitor::MonitorHandle>,
1694  x: f64,
1695  y: f64,
1696) -> Option<winit::monitor::MonitorHandle> {
1697  monitors.into_iter().find(|monitor| {
1698    let pos = monitor.position().unwrap_or_default();
1699    let size = monitor
1700      .current_video_mode()
1701      .map(|mode| mode.size())
1702      .unwrap_or_default();
1703    x >= pos.x as f64
1704      && x <= pos.x as f64 + size.width as f64
1705      && y >= pos.y as f64
1706      && y <= pos.y as f64 + size.height as f64
1707  })
1708}
1709
1710#[cfg(target_os = "macos")]
1711fn is_cef_helper_process() -> bool {
1712  const HELPER_SUFFIXES: &[&str] = &[
1713    " Helper (GPU)",
1714    " Helper (Renderer)",
1715    " Helper (Plugin)",
1716    " Helper (Alerts)",
1717    " Helper",
1718  ];
1719
1720  std::env::current_exe()
1721    .ok()
1722    .and_then(|path| {
1723      path
1724        .file_name()
1725        .and_then(|name| name.to_str())
1726        .map(|name| HELPER_SUFFIXES.iter().any(|suffix| name.ends_with(suffix)))
1727    })
1728    .unwrap_or_default()
1729}
1730
1731pub(crate) struct AppState<T: UserEvent> {
1732  pub(crate) windows: HashMap<WindowId, AppWindow>,
1733  /// Windows that are already closed as far as the application is concerned,
1734  /// kept alive only so their native handle outlives the CEF browsers they
1735  /// host. See [`WinitCefApp::close_window`].
1736  pub(crate) closing_windows: Vec<AppWindow>,
1737  pub(crate) winid_id_to_window_id_map: HashMap<WinitWindowId, WindowId>,
1738  pub(crate) callback: Box<dyn FnMut(RunEvent<T>)>,
1739  pub(crate) live_browsers: usize,
1740  live_popups: HashMap<i32, Arc<crate::popup::PopupFamily>>,
1741  pending_popups: Vec<(crate::popup::PopupRequest, Arc<crate::popup::PopupFamily>)>,
1742  pub(crate) exiting: bool,
1743}
1744
1745pub(crate) struct WinitCefApp<T: UserEvent> {
1746  pub(crate) context: RuntimeContext<T>,
1747  receiver: Receiver<Message<T>>,
1748  pub(crate) state: AppState<T>,
1749  pub(crate) scheme_registry: request_handler::SchemeRegistry,
1750}
1751
1752impl<T: UserEvent> WinitCefApp<T> {
1753  fn new(
1754    context: RuntimeContext<T>,
1755    receiver: Receiver<Message<T>>,
1756    callback: Box<dyn FnMut(RunEvent<T>)>,
1757    scheme_registry: request_handler::SchemeRegistry,
1758  ) -> Self {
1759    Self {
1760      context,
1761      receiver,
1762      state: AppState {
1763        windows: HashMap::new(),
1764        closing_windows: Vec::new(),
1765        winid_id_to_window_id_map: HashMap::new(),
1766        callback,
1767        live_browsers: 0,
1768        live_popups: HashMap::new(),
1769        pending_popups: Vec::new(),
1770        exiting: false,
1771      },
1772      scheme_registry,
1773    }
1774  }
1775
1776  fn run_callback(&mut self, event: RunEvent<T>) {
1777    (self.state.callback)(event);
1778  }
1779
1780  fn install_current_dispatch(
1781    &mut self,
1782    event_loop: &dyn ActiveEventLoop,
1783  ) -> MainThreadDispatchGuard<T> {
1784    let mut dispatch = Box::new(MainThreadDispatch {
1785      app: self as *mut _,
1786      event_loop: event_loop as *const _,
1787    });
1788
1789    let previous = self.context.current_dispatch.install(dispatch.as_mut());
1790
1791    MainThreadDispatchGuard {
1792      context: self.context.clone(),
1793      dispatch,
1794      previous,
1795    }
1796  }
1797
1798  fn drain_messages(&mut self, event_loop: &dyn ActiveEventLoop) {
1799    while let Ok(message) = self.receiver.try_recv() {
1800      self.handle_message(event_loop, message);
1801    }
1802  }
1803
1804  fn handle_message(&mut self, event_loop: &dyn ActiveEventLoop, message: Message<T>) {
1805    match message {
1806      Message::EventLoop(message) => self.handle_event_loop_message(event_loop, message),
1807      Message::PopupPending(request, family) => {
1808        self.state.pending_popups.push((request, family));
1809      }
1810      Message::PopupCreated(request, id, family) => {
1811        self
1812          .state
1813          .pending_popups
1814          .retain(|(pending, _)| !pending.is_same(&request));
1815        self.state.live_popups.insert(id, family);
1816      }
1817      Message::PopupAborted(request) => {
1818        self
1819          .state
1820          .pending_popups
1821          .retain(|(pending, _)| !pending.is_same(&request));
1822        self.exit_if_done(event_loop);
1823      }
1824      Message::PopupClosed(id) => {
1825        self.state.live_popups.remove(&id);
1826        self.exit_if_done(event_loop);
1827      }
1828      Message::BrowserClosed(_window_id, webview_id) => {
1829        // Standalone webview.close() and app shutdown keep the child in state
1830        // until this callback, so cleanup happens here. Individual window
1831        // teardown removes child bookkeeping first; then this message is only
1832        // the lifecycle acknowledgement that lets live_browsers drain.
1833        //
1834        // The window_id baked into the browser's handlers can be stale after a
1835        // reparent, so locate the webview by its process-unique id across every
1836        // window rather than trusting the message's window_id — otherwise a
1837        // reparented webview's scheme-handler entries would leak and its
1838        // AppWebview would linger in the target window forever.
1839        let closed = self.state.windows.iter_mut().find_map(|(id, appwindow)| {
1840          appwindow
1841            .children
1842            .iter()
1843            .position(|child| child.webview_id == webview_id)
1844            .map(|index| {
1845              let child = appwindow.children.remove(index);
1846              if let Some(view) = &child.offscreen {
1847                view.visible.store(false, Ordering::Relaxed);
1848                view.request_redraw();
1849              }
1850              (*id, child, appwindow.children.is_empty())
1851            })
1852        });
1853
1854        let mut emptied_window = None;
1855        if let Some((window_id, child, was_last)) = closed {
1856          self.remove_scheme_handler_entries(&child);
1857          if was_last {
1858            emptied_window = Some(window_id);
1859          }
1860        } else {
1861          // The webview belonged to a window that is already closing: its
1862          // registry entries went with `close_window`, and the native window is
1863          // only being held open for CEF. This acknowledgement is what releases
1864          // it, once it is the last browser the window was hosting.
1865          for appwindow in &mut self.state.closing_windows {
1866            appwindow
1867              .children
1868              .retain(|child| child.webview_id != webview_id);
1869          }
1870          self
1871            .state
1872            .closing_windows
1873            .retain(|appwindow| !appwindow.children.is_empty());
1874        }
1875
1876        self.state.live_browsers = self.state.live_browsers.saturating_sub(1);
1877
1878        // A window that just lost its last webview has nothing left to show, so
1879        // it follows the webview out through the regular close path — listeners
1880        // still get `CloseRequested` and can keep the empty window around.
1881        // `close_window` runs the exit check itself.
1882        if let Some(window_id) = emptied_window {
1883          self.request_window_close(window_id, event_loop);
1884        } else {
1885          self.exit_if_done(event_loop);
1886        }
1887      }
1888      #[cfg(any(target_os = "macos", windows))]
1889      Message::DestroyWebviewHostWindow(webview_id) => {
1890        // Destroying the browser's own child view/window is what completes the
1891        // close CEF handed over in `do_close`; CEF acknowledges it with
1892        // `BrowserClosed`, which is where the bookkeeping is dropped. Same
1893        // reasoning as there for searching every window by webview id.
1894        //
1895        // Closing windows are searched too: they hold their children until CEF
1896        // acknowledges them, and this destruction is what makes CEF do that.
1897        if let Some(child) = self
1898          .state
1899          .windows
1900          .values()
1901          .chain(self.state.closing_windows.iter())
1902          .flat_map(|appwindow| appwindow.children.iter())
1903          .find(|child| child.webview_id == webview_id)
1904        {
1905          child.destroy_host_window();
1906        }
1907      }
1908      Message::CreateWindow {
1909        window_id,
1910        webview_id,
1911        pending,
1912        after_window_creation,
1913        result_tx,
1914      } => {
1915        let result = self.create_window(
1916          event_loop,
1917          window_id,
1918          webview_id,
1919          pending,
1920          after_window_creation,
1921        );
1922        // The caller stopped waiting: see `RuntimeContext::wait_for_creation`.
1923        if let Err(mpsc::SendError(Err(error))) = result_tx.send(result) {
1924          log::error!("failed to create window: {error}");
1925        }
1926      }
1927      Message::CreateWebview {
1928        window_id,
1929        webview_id,
1930        pending,
1931        result_tx,
1932      } => {
1933        let result = self.create_webview(window_id, webview_id, *pending);
1934        if let Err(mpsc::SendError(Err(error))) = result_tx.send(result) {
1935          log::error!("failed to create webview: {error}");
1936        }
1937      }
1938      Message::Window { window_id, message } => {
1939        self.handle_window_message(event_loop, window_id, message)
1940      }
1941      Message::Webview {
1942        window_id,
1943        webview_id,
1944        message,
1945      } => self.handle_webview_message(window_id, webview_id, message),
1946      Message::NavigateFirstWebview { window_id, url } => {
1947        self.navigate_first_webview(window_id, &url)
1948      }
1949      Message::DragDropScriptEvent {
1950        window_id,
1951        webview_id,
1952        target,
1953        drag_drop_state,
1954        event,
1955      } => {
1956        if let Some(event) = browser_client::event_from_script_event(&drag_drop_state, event) {
1957          self.emit_drag_drop_event(window_id, webview_id, target, event);
1958        }
1959      }
1960      Message::Task(task) => task(),
1961      Message::RequestExit(code) => {
1962        if self.request_exit(Some(code)) {
1963          self.close_all_browsers();
1964          self.exit_if_done(event_loop);
1965        }
1966      }
1967      Message::Opened(urls) => self.run_callback(RunEvent::Opened { urls }),
1968      #[cfg(target_os = "macos")]
1969      Message::Reopen {
1970        has_visible_windows,
1971      } => self.run_callback(RunEvent::Reopen {
1972        has_visible_windows,
1973      }),
1974      #[cfg(target_os = "macos")]
1975      Message::AccessibilityChanged { enabled } => self.set_browsers_accessibility_state(enabled),
1976      Message::UserEvent(event) => self.run_callback(RunEvent::UserEvent(event)),
1977    }
1978  }
1979
1980  fn handle_event_loop_message(
1981    &mut self,
1982    event_loop: &dyn ActiveEventLoop,
1983    message: EventLoopMessage,
1984  ) {
1985    match message {
1986      EventLoopMessage::SetTheme(theme) => {
1987        *self.context.app_wide_theme.lock().unwrap() = theme;
1988        for appwindow in self.state.windows.values_mut() {
1989          appwindow.set_theme(theme);
1990        }
1991      }
1992      EventLoopMessage::PrimaryMonitor(tx) => {
1993        let monitor = event_loop
1994          .primary_monitor()
1995          .map(|monitor| winit_monitor_to_tauri_monitor(&monitor));
1996        let _ = tx.send(Ok(monitor));
1997      }
1998      EventLoopMessage::MonitorFromPoint(tx, x, y) => {
1999        let monitor = find_monitor_from_point(event_loop.available_monitors(), x, y)
2000          .map(|monitor| winit_monitor_to_tauri_monitor(&monitor));
2001        let _ = tx.send(Ok(monitor));
2002      }
2003      EventLoopMessage::AvailableMonitors(tx) => {
2004        let monitors = event_loop
2005          .available_monitors()
2006          .map(|monitor| winit_monitor_to_tauri_monitor(&monitor))
2007          .collect();
2008        let _ = tx.send(Ok(monitors));
2009      }
2010      EventLoopMessage::SetDeviceEventFilter(filter) => {
2011        event_loop.listen_device_events(device_event_filter_to_winit(filter));
2012      }
2013      EventLoopMessage::CursorPosition(tx) => {
2014        let _ = tx.send(event_loop.cursor_position());
2015      }
2016      EventLoopMessage::DisplayHandle(tx) => {
2017        let handle = event_loop
2018          .display_handle()
2019          .map(|handle| SendRawDisplayHandle(handle.as_raw()));
2020        let _ = tx.send(handle);
2021      }
2022      #[cfg(target_os = "macos")]
2023      EventLoopMessage::SetActivationPolicy(activation_policy) => {
2024        event_loop.set_activation_policy(activation_policy)
2025      }
2026      #[cfg(target_os = "macos")]
2027      EventLoopMessage::SetDockVisibility(visible) => event_loop.set_dock_visibility(visible),
2028      #[cfg(target_os = "macos")]
2029      EventLoopMessage::ShowApplication => event_loop.show_application(),
2030      #[cfg(target_os = "macos")]
2031      EventLoopMessage::HideApplication => event_loop.hide_application(),
2032    }
2033  }
2034
2035  /// Removes the webview's `(browser_id, scheme)` entries from the scheme registry.
2036  fn remove_scheme_handler_entries(&self, child: &AppWebview) {
2037    let mut registry = self.scheme_registry.lock().unwrap();
2038    for scheme in child.uri_scheme_protocols.keys() {
2039      registry.remove(&(child.browser_id, scheme.clone()));
2040    }
2041  }
2042
2043  fn emit_drag_drop_event(
2044    &mut self,
2045    window_id: WindowId,
2046    webview_id: u32,
2047    target: browser_client::DragDropEventTarget,
2048    event: DragDropEvent,
2049  ) {
2050    match target {
2051      browser_client::DragDropEventTarget::Window => {
2052        self.emit_window_event(window_id, WindowEvent::DragDrop(event));
2053      }
2054      browser_client::DragDropEventTarget::Webview => {
2055        self.emit_webview_event(window_id, webview_id, WebviewEvent::DragDrop(event));
2056      }
2057    }
2058  }
2059
2060  fn emit_window_event(&mut self, window_id: WindowId, event: WindowEvent) {
2061    let Some(appwindow) = self.state.windows.get(&window_id) else {
2062      return;
2063    };
2064    let label = appwindow.label.clone();
2065    let listeners = appwindow.listeners.clone();
2066
2067    self.run_callback(RunEvent::WindowEvent {
2068      label,
2069      event: event.clone(),
2070    });
2071
2072    {
2073      let listeners = listeners.lock().unwrap();
2074      for handler in listeners.values() {
2075        handler(&event);
2076      }
2077    }
2078  }
2079
2080  fn emit_webview_event(&mut self, window_id: WindowId, webview_id: u32, event: WebviewEvent) {
2081    let Some(appwindow) = self.state.windows.get(&window_id) else {
2082      return;
2083    };
2084    let Some(child) = appwindow
2085      .children
2086      .iter()
2087      .find(|child| child.webview_id == webview_id)
2088    else {
2089      return;
2090    };
2091    let label = child.label.clone();
2092    let listeners = child.listeners.clone();
2093
2094    self.run_callback(RunEvent::WebviewEvent {
2095      label,
2096      event: event.clone(),
2097    });
2098
2099    {
2100      let listeners = listeners.lock().unwrap();
2101      for handler in listeners.values() {
2102        handler(&event);
2103      }
2104    }
2105  }
2106
2107  fn request_exit(&mut self, code: Option<i32>) -> bool {
2108    // if we already exiting, don't request exit again
2109    if self.state.exiting {
2110      return false;
2111    }
2112
2113    let (tx, rx) = mpsc::channel();
2114    self.run_callback(RunEvent::ExitRequested { code, tx });
2115
2116    if matches!(rx.try_recv(), Ok(ExitRequestedEventAction::Prevent)) {
2117      false
2118    } else {
2119      self.state.exiting = true;
2120      true
2121    }
2122  }
2123
2124  pub(crate) fn close_window(&mut self, window_id: WindowId, event_loop: &dyn ActiveEventLoop) {
2125    if !self.state.windows.contains_key(&window_id) {
2126      return;
2127    }
2128    // Every close path funnels through here, and this is the last point at which
2129    // the window can still be named: the maps below are what `emit_window_event`
2130    // and winit's own `Destroyed` both resolve a window through, and winit
2131    // reports the destruction only once the window below has been dropped.
2132    // Without this, `WindowEvent::Destroyed` never reaches the application, and
2133    // it is what Tauri unregisters a window on — so a window closed while others
2134    // stay open would keep its label taken and keep appearing in `Manager`'s
2135    // window list forever.
2136    if !self.state.exiting {
2137      self.emit_window_event(window_id, WindowEvent::Destroyed);
2138    }
2139    let Some(appwindow) = self.state.windows.remove(&window_id) else {
2140      return;
2141    };
2142    for child in &appwindow.children {
2143      if let Some(view) = &child.offscreen {
2144        view.visible.store(false, Ordering::Relaxed);
2145      }
2146    }
2147    if let Some(handler) = self.context.window_event_handler.clone() {
2148      handler(
2149        &appwindow.label,
2150        event_loop,
2151        appwindow.window.clone(),
2152        &WinitWindowEvent::Destroyed,
2153      );
2154    }
2155    self
2156      .state
2157      .winid_id_to_window_id_map
2158      .remove(&appwindow.window.id());
2159    // The window is gone from state, so BrowserClosed will not find these
2160    // children later. Clean registry entries while we still hold them; the CEF
2161    // shutdown drain is still enforced by live_browsers.
2162    for child in &appwindow.children {
2163      self.remove_scheme_handler_entries(child);
2164      child.popup_family.close_all();
2165      // DevTools is a browser of its own, living in a window CEF owns and
2166      // parents to this one. Closing the browser it inspects does not take it
2167      // down first, so ask for it explicitly — exactly what a webview-level
2168      // close does — instead of leaving CEF to discover its window is gone.
2169      child.host.close_dev_tools();
2170      child.host.close_browser(1);
2171    }
2172
2173    if appwindow.children.is_empty() {
2174      // Nothing is left to close, so the native window goes now.
2175      drop(appwindow);
2176    } else {
2177      // `close_browser` only *starts* the close: CEF still has to tear down the
2178      // browser's own child window, and it reports that back through
2179      // `on_before_close`. Destroying the native window we parented it to before
2180      // that point pulls the ground out from under a browser Chromium is still
2181      // compositing — with DevTools attached the surface outlives the window
2182      // long enough for the GPU process to fault on it and restart. So keep the
2183      // window alive until `BrowserClosed` accounts for every child, and only
2184      // hide it here: the application already saw `Destroyed`, so the wait must
2185      // not be visible to the user.
2186      appwindow.window.set_visible(false);
2187      self.state.closing_windows.push(appwindow);
2188    }
2189
2190    // A window still waiting on its browsers holds `live_browsers` above zero,
2191    // so this cannot exit the loop out from under a close in flight.
2192    self.exit_if_done(event_loop);
2193  }
2194
2195  pub(crate) fn request_window_close(
2196    &mut self,
2197    window_id: WindowId,
2198    event_loop: &dyn ActiveEventLoop,
2199  ) {
2200    // Avoid requesting window close if we already exisitng
2201    if self.state.exiting {
2202      self.close_window(window_id, event_loop);
2203      return;
2204    }
2205
2206    let (tx, rx) = mpsc::channel();
2207    let Some(appwindow) = self.state.windows.get(&window_id) else {
2208      return;
2209    };
2210    let label = appwindow.label.clone();
2211    let listeners = appwindow.listeners.clone();
2212
2213    {
2214      let listeners = listeners.lock().unwrap();
2215      for handler in listeners.values() {
2216        handler(&WindowEvent::CloseRequested {
2217          signal_tx: tx.clone(),
2218        });
2219      }
2220    }
2221
2222    self.run_callback(RunEvent::WindowEvent {
2223      label,
2224      event: WindowEvent::CloseRequested { signal_tx: tx },
2225    });
2226
2227    if !matches!(rx.try_recv(), Ok(true)) {
2228      self.close_window(window_id, event_loop);
2229    }
2230  }
2231
2232  fn navigate_first_webview(&self, window_id: WindowId, url: &str) {
2233    let Some(frame) = self
2234      .state
2235      .windows
2236      .get(&window_id)
2237      .and_then(|window| window.children.first())
2238      .and_then(|webview| webview.browser.main_frame())
2239    else {
2240      return;
2241    };
2242
2243    frame.load_url(Some(&CefString::from(url)));
2244  }
2245
2246  fn close_all_browsers(&mut self) {
2247    // Keep each child reachable until CEF acknowledges its close. On macOS and
2248    // Windows, do_close queues DestroyWebviewHostWindow, which needs this state
2249    // to destroy the native child view and trigger on_before_close. Dropping
2250    // the windows here can strand live_browsers and prevent process exit.
2251    for appwindow in self.state.windows.values() {
2252      for child in &appwindow.children {
2253        child.popup_family.close_all();
2254        child.host.close_dev_tools();
2255        child.host.close_browser(1);
2256      }
2257    }
2258  }
2259
2260  #[cfg(target_os = "macos")]
2261  fn set_browsers_accessibility_state(&self, enabled: bool) {
2262    let state = if enabled {
2263      State::ENABLED
2264    } else {
2265      State::DISABLED
2266    };
2267    for appwindow in self.state.windows.values() {
2268      for child in &appwindow.children {
2269        child.host.set_accessibility_state(state);
2270      }
2271    }
2272  }
2273
2274  fn exit_if_done(&mut self, event_loop: &dyn ActiveEventLoop) {
2275    // A reservation is normally resolved by `PopupCreated` or `PopupAborted`,
2276    // but CEF discards popups without always reporting the abort — the opener
2277    // can be torn down first, or the abort can arrive for a browser its opener
2278    // no longer matches. Teardown (window close, app shutdown, the root's own
2279    // native close) revokes the family, and a revoked family never admits a
2280    // popup again, so its reservations are dead and must not hold the process
2281    // open. Reservations of live families still gate the exit until CEF
2282    // resolves them.
2283    self
2284      .state
2285      .pending_popups
2286      .retain(|(_, family)| !family.is_revoked());
2287
2288    if self.state.live_browsers != 0
2289      || !self.state.live_popups.is_empty()
2290      || !self.state.pending_popups.is_empty()
2291    {
2292      return;
2293    }
2294
2295    if self.state.exiting || (self.state.windows.is_empty() && self.request_exit(None)) {
2296      self.run_callback(RunEvent::Exit);
2297      event_loop.exit();
2298    }
2299  }
2300}
2301
2302// Every overridden method installs the dispatch guard before anything else: besides enabling
2303// inline dispatch, the guard is how `MainThreadDispatchSlot::is_active` knows that winit's
2304// handler is borrowed, which the macOS wake-up deferral relies on.
2305impl<T: UserEvent> ApplicationHandler for WinitCefApp<T> {
2306  fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
2307    let _guard = self.install_current_dispatch(event_loop);
2308    self.drain_messages(event_loop);
2309  }
2310
2311  fn new_events(&mut self, event_loop: &dyn ActiveEventLoop, cause: StartCause) {
2312    let _guard = self.install_current_dispatch(event_loop);
2313    match cause {
2314      StartCause::Init => {
2315        self.run_callback(RunEvent::Ready);
2316        self.context.cef_pump.do_work();
2317      }
2318      // Match wry/tao, which emit `Resumed` on each `Poll` start cause.
2319      StartCause::Poll => self.run_callback(RunEvent::Resumed),
2320      _ => {}
2321    }
2322  }
2323
2324  fn proxy_wake_up(&mut self, event_loop: &dyn ActiveEventLoop) {
2325    let _guard = self.install_current_dispatch(event_loop);
2326    self.drain_messages(event_loop);
2327  }
2328
2329  fn about_to_wait(&mut self, event_loop: &dyn ActiveEventLoop) {
2330    let _guard = self.install_current_dispatch(event_loop);
2331    self.apply_pending_activations();
2332    #[cfg(any(
2333      target_os = "linux",
2334      target_os = "dragonfly",
2335      target_os = "freebsd",
2336      target_os = "netbsd",
2337      target_os = "openbsd"
2338    ))]
2339    self.apply_pending_host_layouts();
2340    self.run_callback(RunEvent::MainEventsCleared);
2341  }
2342
2343  fn window_event(
2344    &mut self,
2345    event_loop: &dyn ActiveEventLoop,
2346    winit_id: WinitWindowId,
2347    event: WinitWindowEvent,
2348  ) {
2349    let _guard = self.install_current_dispatch(event_loop);
2350    let Some(window_id) = self.state.winid_id_to_window_id_map.get(&winit_id).copied() else {
2351      return;
2352    };
2353    if !matches!(event, WinitWindowEvent::Destroyed)
2354      && let (Some(handler), Some((label, window))) = (
2355        self.context.window_event_handler.clone(),
2356        self
2357          .state
2358          .windows
2359          .get(&window_id)
2360          .map(|window| (window.label.clone(), window.window.clone())),
2361      )
2362    {
2363      handler(&label, event_loop, window, &event);
2364    }
2365    let Some(appwindow) = self.state.windows.get_mut(&window_id) else {
2366      return;
2367    };
2368
2369    match event {
2370      WinitWindowEvent::CloseRequested => self.request_window_close(window_id, event_loop),
2371
2372      // Reached only when the native window went away without a close request of
2373      // its own; `close_window` emits `Destroyed` for every path, this one
2374      // included.
2375      WinitWindowEvent::Destroyed => self.close_window(window_id, event_loop),
2376      WinitWindowEvent::SurfaceResized(size) => {
2377        webview::layout_app_window(appwindow);
2378        self.emit_window_event(window_id, WindowEvent::Resized(size));
2379      }
2380      WinitWindowEvent::ScaleFactorChanged {
2381        scale_factor,
2382        surface_size_writer,
2383      } => {
2384        let new_inner_size = surface_size_writer
2385          .surface_size()
2386          .unwrap_or_else(|_| appwindow.window.surface_size());
2387        webview::layout_app_window(appwindow);
2388        // A windowed browser follows its native view to the new display; an
2389        // offscreen one only re-reads `screen_info` when told to.
2390        for child in appwindow.children.iter().filter(|c| c.offscreen.is_some()) {
2391          child.host.notify_screen_info_changed();
2392          child.host.was_resized();
2393        }
2394        self.emit_window_event(
2395          window_id,
2396          WindowEvent::ScaleFactorChanged {
2397            scale_factor,
2398            new_inner_size,
2399          },
2400        );
2401      }
2402      WinitWindowEvent::Moved(pos) => {
2403        self.emit_window_event(
2404          window_id,
2405          WindowEvent::Moved(PhysicalPosition::new(pos.x, pos.y)),
2406        );
2407      }
2408      WinitWindowEvent::Focused(focused) => {
2409        self.emit_window_event(window_id, WindowEvent::Focused(focused));
2410      }
2411      WinitWindowEvent::ThemeChanged(theme) => {
2412        let system_theme = winit_theme_to_tauri_theme(theme);
2413        if let Some(explicit_theme) = appwindow.preferred_theme() {
2414          appwindow.set_theme(Some(explicit_theme));
2415        } else {
2416          // Following the system: the appearance changed without going through
2417          // `set_theme`, so the titlebar rebuild still has to be undone.
2418          #[cfg(target_os = "macos")]
2419          appwindow.reapply_traffic_light_position_after_appearance_change();
2420        }
2421        self.emit_window_event(window_id, WindowEvent::ThemeChanged(system_theme));
2422      }
2423      #[cfg(windows)]
2424      WinitWindowEvent::RedrawRequested => {
2425        appwindow.draw_background_surface();
2426      }
2427      WinitWindowEvent::DragEntered { id, position } => {
2428        let has_file_paths = event_loop
2429          .data_transfer(id)
2430          .map(|data_transfer| data_transfer.has_type(&TypeHint::UriList))
2431          .unwrap_or(false);
2432
2433        if has_file_paths {
2434          appwindow.native_drag_drop = Some(WinitDragDropState {
2435            id,
2436            paths: None,
2437            paths_requested: false,
2438            enter_position: position,
2439            latest_position: position,
2440            enter_emitted: false,
2441            drop_pending: false,
2442          });
2443          let _ = event_loop.set_valid_dnd_actions(id, &[DndAction::Copy]);
2444          request_native_drag_paths(event_loop, &mut appwindow.native_drag_drop);
2445        } else {
2446          appwindow.native_drag_drop = None;
2447          let _ = event_loop.set_valid_dnd_actions(id, &[]);
2448        }
2449      }
2450      WinitWindowEvent::DragPosition { id, position, .. } => {
2451        if let Some(state) = appwindow
2452          .native_drag_drop
2453          .as_mut()
2454          .filter(|state| state.id == id)
2455        {
2456          state.latest_position = Some(position);
2457          state.enter_position.get_or_insert(position);
2458        }
2459
2460        let enter_event = pending_native_drag_enter(&mut appwindow.native_drag_drop);
2461
2462        let over_event = appwindow
2463          .native_drag_drop
2464          .as_ref()
2465          .filter(|state| state.id == id && state.enter_emitted)
2466          .map(|_| DragDropEvent::Over { position });
2467
2468        if let Some(event) = enter_event {
2469          self.emit_window_event(window_id, WindowEvent::DragDrop(event));
2470        }
2471        if let Some(event) = over_event {
2472          self.emit_window_event(window_id, WindowEvent::DragDrop(event));
2473        }
2474      }
2475      WinitWindowEvent::DragDropped { id, .. } => {
2476        if let Some(state) = appwindow
2477          .native_drag_drop
2478          .as_mut()
2479          .filter(|state| state.id == id)
2480        {
2481          state.drop_pending = true;
2482        }
2483
2484        request_native_drag_paths(event_loop, &mut appwindow.native_drag_drop);
2485
2486        let enter_event = pending_native_drag_enter(&mut appwindow.native_drag_drop);
2487        let drop_event = pending_native_drag_drop(&mut appwindow.native_drag_drop);
2488
2489        let drop_emitted = drop_event.is_some();
2490        if drop_emitted {
2491          appwindow.native_drag_drop = None;
2492        }
2493
2494        if let Some(event) = enter_event {
2495          self.emit_window_event(window_id, WindowEvent::DragDrop(event));
2496        }
2497        if let Some(event) = drop_event {
2498          self.emit_window_event(window_id, WindowEvent::DragDrop(event));
2499        }
2500      }
2501      WinitWindowEvent::DragLeft { id } => {
2502        let entered = appwindow
2503          .native_drag_drop
2504          .as_ref()
2505          .is_some_and(|state| state.id == id && state.enter_emitted);
2506
2507        appwindow.native_drag_drop = None;
2508
2509        if entered {
2510          self.emit_window_event(window_id, WindowEvent::DragDrop(DragDropEvent::Leave));
2511        }
2512      }
2513      WinitWindowEvent::DataTransferReceived { id, value, .. } => {
2514        let mut reject_drag = false;
2515
2516        if let Some(state) = appwindow
2517          .native_drag_drop
2518          .as_mut()
2519          .filter(|state| state.id == id)
2520        {
2521          match value.try_as_file_paths() {
2522            Ok(paths) if !paths.is_empty() => state.paths = Some(paths),
2523            Ok(_) => reject_drag = state.drop_pending,
2524            Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {}
2525            Err(_) => reject_drag = state.drop_pending,
2526          }
2527        }
2528
2529        if reject_drag {
2530          appwindow.native_drag_drop = None;
2531          let _ = event_loop.set_valid_dnd_actions(id, &[]);
2532          return;
2533        }
2534
2535        let enter_event = pending_native_drag_enter(&mut appwindow.native_drag_drop);
2536        let drop_event = pending_native_drag_drop(&mut appwindow.native_drag_drop);
2537
2538        let drop_emitted = drop_event.is_some();
2539        if drop_emitted {
2540          appwindow.native_drag_drop = None;
2541        }
2542
2543        if let Some(event) = enter_event {
2544          self.emit_window_event(window_id, WindowEvent::DragDrop(event));
2545        }
2546        if let Some(event) = drop_event {
2547          self.emit_window_event(window_id, WindowEvent::DragDrop(event));
2548        }
2549      }
2550      _ => {}
2551    }
2552  }
2553}
2554
2555/// Picks the deep link URLs out of a process command line.
2556///
2557/// An argument qualifies when it parses as a URL whose scheme is one of `schemes`,
2558/// matched the same exact way `BrowserProcessHandler::on_already_running_app_relaunch`
2559/// matches it on the receiving end. Everything else is dropped: the point of
2560/// [`Cef::allow_chromium_command_line_args`] being off is that no other argument
2561/// survives onto Chromium's command line.
2562fn deep_link_arguments<I>(args: I, schemes: &[String]) -> Vec<String>
2563where
2564  I: IntoIterator<Item = String>,
2565{
2566  args
2567    .into_iter()
2568    .filter(|arg| {
2569      url::Url::parse(arg).is_ok_and(|url| schemes.iter().any(|scheme| scheme == url.scheme()))
2570    })
2571    .collect()
2572}
2573
2574/// Appends `args` to `command_line`, as a switch with a value, a bare switch or a
2575/// positional argument depending on how each entry looks.
2576///
2577/// A bare name with no value is only recognised as a switch when it is spelled with its
2578/// `--` prefix; without one it is a positional argument. This runtime's own entries are
2579/// therefore all spelled `--switch`, values included — Chromium strips the prefix off the
2580/// key it stores, so both spellings reach the same switch.
2581fn append_command_line_args(command_line: &mut CommandLine, args: &[(String, Option<String>)]) {
2582  for (arg, value) in args {
2583    if let Some(value) = value {
2584      command_line.append_switch_with_value(
2585        Some(&CefString::from(arg.as_str())),
2586        Some(&CefString::from(value.as_str())),
2587      );
2588    } else if arg.starts_with("-") {
2589      command_line.append_switch(Some(&CefString::from(arg.as_str())));
2590    } else {
2591      command_line.append_argument(Some(&CefString::from(arg.as_str())));
2592    }
2593  }
2594}
2595
2596wrap_with_args! {
2597  wrap_app => TauriCefAppArgs;
2598
2599  struct TauriCefApp<T: UserEvent> {
2600    context: RuntimeContext<T>,
2601    context_initialized: Arc<AtomicBool>,
2602    deep_link_schemes: Vec<String>,
2603    // Whether the deep link URL this process was launched with has to be put back
2604    // onto Chromium's command line. See `on_before_command_line_processing`.
2605    restore_deep_link_arguments: bool,
2606    // Switches applied whatever process type `on_before_command_line_processing` reports.
2607    //
2608    // Deliberately tiny: `cef_app_t::on_before_command_line_processing` warns that
2609    // "modifying the command-line arguments for non-browser processes may result in
2610    // undefined behavior including crashes", so only switches we know a child process
2611    // must see itself belong here.
2612    internal_command_line_args: Vec<(String, Option<String>)>,
2613    // Switches applied only when the reported process type is the browser one.
2614    //
2615    // Chromium already forwards to each child the switches it needs, so anything that
2616    // is only read in the browser process - and everything the embedding application
2617    // supplied through `Cef::command_line_arg` - goes here. The application's own
2618    // switches are appended last so they win over the runtime's defaults.
2619    browser_command_line_args: Vec<(String, Option<String>)>,
2620    // The `Cef::cef_api_version` override, passed on to child processes. See
2621    // `API_VERSION_SWITCH`.
2622    api_version: Option<i32>,
2623    // Names merged into `--disable-features` and `--enable-features` rather than
2624    // appended over them.
2625    //
2626    // CEF fills `--disable-features` before it calls this hook with a list that keeps
2627    // Chrome from crashing at startup and renderers from being killed on the runtime's
2628    // own requests, and Chromium's command line replaces a switch value rather than
2629    // extending it. See `crate::switches::append_merged_switch`.
2630    disabled_features: Vec<String>,
2631    enabled_features: Vec<String>,
2632  }
2633
2634  impl App {
2635    fn render_process_handler(&self) -> Option<RenderProcessHandler> {
2636      Some(ipc::TauriRenderProcessHandler::new())
2637    }
2638
2639    fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
2640      Some(browser_client::TauriCefBrowserProcessHandler::new(
2641        self.context.clone(),
2642        self.context_initialized.clone(),
2643        self.deep_link_schemes.clone(),
2644        self.api_version,
2645      ))
2646    }
2647
2648    fn on_before_command_line_processing(
2649      &self,
2650      process_type: Option<&CefString>,
2651      command_line: Option<&mut CommandLine>,
2652    ) {
2653      let Some(command_line) = command_line else {
2654        return;
2655      };
2656
2657      append_command_line_args(command_line, &self.internal_command_line_args);
2658
2659      // The browser process is the one launched without a `--type` switch, so CEF hands
2660      // us an empty (or absent) process type for it.
2661      let is_browser_process = process_type.is_none_or(|ty| ty.to_string().is_empty());
2662      if is_browser_process {
2663        // A second launch of an already-running application is a browser process too,
2664        // so `Settings::command_line_args_disabled` clears its command line before
2665        // Chromium's process singleton relays it to the first instance, losing the
2666        // `myapp://...` URL. Putting it back here happens after CEF's clear and before
2667        // the singleton. Only deep links are restored; every other argument stays
2668        // dropped, which is the point of the lockdown.
2669        if self.restore_deep_link_arguments {
2670          for deep_link in deep_link_arguments(std::env::args().skip(1), &self.deep_link_schemes) {
2671            command_line.append_argument(Some(&CefString::from(deep_link.as_str())));
2672          }
2673        }
2674
2675        append_command_line_args(command_line, &self.browser_command_line_args);
2676
2677        // Last, and merging rather than replacing: CEF has already written its own
2678        // `--disable-features` by this point, and appending over it would drop entries
2679        // Chrome needs to start at all.
2680        crate::switches::append_merged_switch(
2681          command_line,
2682          "disable-features",
2683          &self.disabled_features,
2684        );
2685        crate::switches::append_merged_switch(
2686          command_line,
2687          "enable-features",
2688          &self.enabled_features,
2689        );
2690      }
2691    }
2692  }
2693}
2694
2695/// Switch that passes the browser process's CEF API version to its child processes.
2696///
2697/// A child must declare its version before any other CEF call, and it never sees the
2698/// `Cef` builder, so it reads the version from its own command line. The browser
2699/// process appends the switch in
2700/// `BrowserProcessHandler::on_before_child_process_launch`.
2701///
2702/// Not an environment variable: that would also be inherited by anything else the
2703/// application spawns, including other CEF applications.
2704///
2705/// Only appended when [`Cef::cef_api_version`] was called.
2706///
2707/// Only children launched through `on_before_child_process_launch` get it: the
2708/// processes Chromium starts itself (renderer, GPU, utility and, on Linux, the
2709/// zygote). A process CEF starts another way, such as the crashpad handler if crash
2710/// reporting is enabled, falls back to `CEF_API_VERSION_LAST`.
2711///
2712/// On macOS, child processes run the `tauri-cef-helper` binary the bundler compiles
2713/// (`crates/tauri-bundler/cef-helper/main.rs`), which parses this switch itself. Keep
2714/// both in sync.
2715pub(crate) const API_VERSION_SWITCH: &str = "tauri-cef-api-version";
2716
2717/// The CEF API version the browser process passed to this child process.
2718///
2719/// `None` if this is not a child process (no `--type=` switch), if no version was
2720/// passed, or if the value is not a number. The last occurrence wins.
2721///
2722/// Must not call CEF: nothing may be called before the version is declared.
2723fn child_process_api_version<I>(args: I) -> Option<i32>
2724where
2725  I: IntoIterator<Item = std::ffi::OsString>,
2726{
2727  let prefix = format!("--{API_VERSION_SWITCH}=");
2728  let mut is_child_process = false;
2729  let mut version = None;
2730  // Non-Unicode arguments are skipped; `std::env::args` would panic on them.
2731  for arg in args {
2732    let Some(arg) = arg.to_str() else {
2733      continue;
2734    };
2735    if arg.starts_with("--type=") {
2736      is_child_process = true;
2737    } else if let Some(value) = arg.strip_prefix(&prefix) {
2738      version = value.parse().ok();
2739    }
2740  }
2741  version.filter(|_| is_child_process)
2742}
2743
2744pub fn run_cef_helper_process() {
2745  let args = cef::args::Args::new();
2746
2747  // A helper the browser process launched with `--no-sandbox` must not enter the sandbox
2748  // here: the browser dropped it deliberately, and entering it anyway would only make the
2749  // library load below fail.
2750  #[cfg(target_os = "macos")]
2751  let _sandbox = (!crate::sandbox::launched_without_sandbox()).then(|| {
2752    let mut sandbox = cef::sandbox::Sandbox::new();
2753    sandbox.initialize(args.as_main_args());
2754    sandbox
2755  });
2756
2757  #[cfg(target_os = "macos")]
2758  let _loader = {
2759    let loader = cef::library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), true);
2760    assert!(loader.load());
2761    loader
2762  };
2763
2764  // Declare the version the browser process passed, if any.
2765  let version = child_process_api_version(std::env::args_os()).unwrap_or(sys::CEF_API_VERSION_LAST);
2766  // `cef_api_hash` returns NULL if the loaded libcef does not support the version, and
2767  // the next CEF call would crash.
2768  if cef::api_hash(version, 0).is_null() {
2769    panic!("the loaded CEF library does not support CEF API version {version}");
2770  }
2771  let mut app = TauriCefHelperApp::new();
2772  let _ = cef::execute_process(
2773    Some(args.as_main_args()),
2774    Some(&mut app),
2775    std::ptr::null_mut(),
2776  );
2777}
2778
2779wrap_app! {
2780  struct TauriCefHelperApp;
2781
2782  impl App {
2783    fn render_process_handler(&self) -> Option<RenderProcessHandler> {
2784      Some(ipc::TauriRenderProcessHandler::new())
2785    }
2786  }
2787}
2788
2789#[derive(Debug, Clone)]
2790pub struct CefRuntimeHandle<T: UserEvent> {
2791  context: RuntimeContext<T>,
2792}
2793
2794impl<T: UserEvent> RuntimeHandle<T> for CefRuntimeHandle<T> {
2795  type Runtime = CefRuntime<T>;
2796
2797  fn create_proxy(&self) -> <Self::Runtime as Runtime<T>>::EventLoopProxy {
2798    EventProxy {
2799      context: self.context.clone(),
2800    }
2801  }
2802
2803  #[cfg(target_os = "macos")]
2804  fn set_activation_policy(
2805    &self,
2806    activation_policy: tauri_runtime::ActivationPolicy,
2807  ) -> Result<()> {
2808    let message = Message::EventLoop(EventLoopMessage::SetActivationPolicy(activation_policy));
2809    self.context.send_message(message)
2810  }
2811
2812  #[cfg(target_os = "macos")]
2813  fn set_dock_visibility(&self, visible: bool) -> Result<()> {
2814    let message = Message::EventLoop(EventLoopMessage::SetDockVisibility(visible));
2815    self.context.send_message(message)
2816  }
2817
2818  fn request_exit(&self, code: i32) -> Result<()> {
2819    self.context.send_message(Message::RequestExit(code))
2820  }
2821
2822  /// Returns the URL for a custom scheme.
2823  ///
2824  /// CEF always uses `http://<scheme>.localhost` or `https://<scheme>.localhost`.
2825  fn custom_scheme_url(&self, scheme: &str, https: bool) -> String {
2826    format!(
2827      "{}://{scheme}.localhost",
2828      if https { "https" } else { "http" }
2829    )
2830  }
2831
2832  fn webview_version(&self) -> Result<String> {
2833    crate::webview_version()
2834  }
2835
2836  fn create_window<F: Fn(RawWindow<'_>) + Send + 'static>(
2837    &self,
2838    pending: PendingWindow<T, Self::Runtime>,
2839    after_window_creation: Option<F>,
2840  ) -> Result<DetachedWindow<T, Self::Runtime>> {
2841    create_window_detached(&self.context, pending, after_window_creation)
2842  }
2843
2844  fn create_webview(
2845    &self,
2846    window_id: WindowId,
2847    pending: PendingWebview<T, Self::Runtime>,
2848  ) -> Result<DetachedWebview<T, Self::Runtime>> {
2849    create_webview_detached(&self.context, window_id, pending)
2850  }
2851
2852  fn run_on_main_thread<F: FnOnce() + Send + 'static>(&self, f: F) -> Result<()> {
2853    self.context.run_on_main_thread(f)
2854  }
2855
2856  fn display_handle(
2857    &self,
2858  ) -> std::result::Result<DisplayHandle<'_>, raw_window_handle::HandleError> {
2859    let raw = event_loop_getter!(self, DisplayHandle)
2860      .map_err(|_| raw_window_handle::HandleError::Unavailable)??;
2861    // SAFETY: the descriptor was produced by the live event loop on its own
2862    // thread; the borrowed handle is valid for as long as the runtime is.
2863    Ok(unsafe { DisplayHandle::borrow_raw(raw.0) })
2864  }
2865
2866  fn primary_monitor(&self) -> Result<Option<Monitor>> {
2867    event_loop_getter!(self, PrimaryMonitor)?
2868  }
2869
2870  fn monitor_from_point(&self, x: f64, y: f64) -> Result<Option<Monitor>> {
2871    let (tx, rx) = mpsc::channel();
2872    self
2873      .context
2874      .send_message(Message::EventLoop(EventLoopMessage::MonitorFromPoint(
2875        tx, x, y,
2876      )))?;
2877    rx.recv().map_err(|_| Error::FailedToReceiveMessage)?
2878  }
2879
2880  fn available_monitors(&self) -> Result<Vec<Monitor>> {
2881    event_loop_getter!(self, AvailableMonitors)?
2882  }
2883
2884  fn cursor_position(&self) -> Result<PhysicalPosition<f64>> {
2885    event_loop_getter!(self, CursorPosition)?
2886  }
2887
2888  fn set_theme(&self, theme: Option<Theme>) {
2889    let message = Message::EventLoop(EventLoopMessage::SetTheme(theme));
2890    let _ = self.context.send_message(message);
2891  }
2892
2893  #[cfg(target_os = "macos")]
2894  fn show(&self) -> Result<()> {
2895    let message = Message::EventLoop(EventLoopMessage::ShowApplication);
2896    self.context.send_message(message)
2897  }
2898
2899  #[cfg(target_os = "macos")]
2900  fn hide(&self) -> Result<()> {
2901    let message = Message::EventLoop(EventLoopMessage::HideApplication);
2902    self.context.send_message(message)
2903  }
2904
2905  fn set_device_event_filter(&self, filter: DeviceEventFilter) {
2906    let message = Message::EventLoop(EventLoopMessage::SetDeviceEventFilter(filter));
2907    let _ = self.context.send_message(message);
2908  }
2909
2910  #[cfg(any(target_os = "macos", target_os = "ios"))]
2911  fn fetch_data_store_identifiers<F: FnOnce(Vec<[u8; 16]>) + Send + 'static>(
2912    &self,
2913    cb: F,
2914  ) -> Result<()> {
2915    cb(Vec::new());
2916    Ok(())
2917  }
2918
2919  #[cfg(any(target_os = "macos", target_os = "ios"))]
2920  fn remove_data_store<F: FnOnce(Result<()>) + Send + 'static>(
2921    &self,
2922    _uuid: [u8; 16],
2923    cb: F,
2924  ) -> Result<()> {
2925    cb(Ok(()));
2926    Ok(())
2927  }
2928}
2929
2930pub struct CefRuntime<T: UserEvent = tauri::EventLoopMessage> {
2931  event_loop: EventLoop,
2932  receiver: Receiver<Message<T>>,
2933  context: RuntimeContext<T>,
2934  scheme_registry: request_handler::SchemeRegistry,
2935  #[cfg(target_os = "macos")]
2936  _app_delegate: Option<objc2::rc::Retained<crate::platform::macos::AppDelegate>>,
2937}
2938
2939impl<T: UserEvent> fmt::Debug for CefRuntime<T> {
2940  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2941    f.debug_struct("CefRuntime").finish()
2942  }
2943}
2944
2945/// CEF installs Chromium's shutdown signal handlers for SIGINT/SIGTERM/SIGHUP
2946/// inside `cef::initialize`. Those handlers hand the signal to Chrome's own exit
2947/// machinery, which never quits the winit loop this runtime owns, so the process
2948/// just ignores the signal. Because the handler disarms itself on delivery, it
2949/// takes a second Ctrl+C to kill the app by default disposition.
2950///
2951/// Put the process's original signal policy back so termination signals behave
2952/// the way they do for any other app (and the way they already do under
2953/// `tauri-runtime-wry`, which installs no handlers at all).
2954#[cfg(any(
2955  target_os = "linux",
2956  target_os = "dragonfly",
2957  target_os = "freebsd",
2958  target_os = "openbsd",
2959  target_os = "netbsd"
2960))]
2961struct TerminationSignals {
2962  sigint: Option<libc::sigaction>,
2963  sigterm: Option<libc::sigaction>,
2964  sighup: Option<libc::sigaction>,
2965}
2966
2967#[cfg(any(
2968  target_os = "linux",
2969  target_os = "dragonfly",
2970  target_os = "freebsd",
2971  target_os = "openbsd",
2972  target_os = "netbsd"
2973))]
2974impl TerminationSignals {
2975  fn capture() -> Self {
2976    Self {
2977      sigint: Self::capture_one(libc::SIGINT),
2978      sigterm: Self::capture_one(libc::SIGTERM),
2979      sighup: Self::capture_one(libc::SIGHUP),
2980    }
2981  }
2982
2983  /// Must run *after* `cef::initialize`, which is the only place CEF installs
2984  /// these handlers; nothing reinstalls them later, so one restore is enough.
2985  fn restore(&self) {
2986    Self::restore_one(libc::SIGINT, self.sigint);
2987    Self::restore_one(libc::SIGTERM, self.sigterm);
2988    Self::restore_one(libc::SIGHUP, self.sighup);
2989  }
2990
2991  fn capture_one(sig: libc::c_int) -> Option<libc::sigaction> {
2992    let mut action = std::mem::MaybeUninit::<libc::sigaction>::uninit();
2993    if unsafe { libc::sigaction(sig, std::ptr::null(), action.as_mut_ptr()) } == 0 {
2994      Some(unsafe { action.assume_init() })
2995    } else {
2996      None
2997    }
2998  }
2999
3000  fn restore_one(sig: libc::c_int, previous: Option<libc::sigaction>) {
3001    let Some(previous) = previous else {
3002      return;
3003    };
3004
3005    unsafe { libc::sigaction(sig, &previous, std::ptr::null_mut()) };
3006  }
3007}
3008
3009impl<T: UserEvent> CefRuntime<T> {
3010  fn init(
3011    mut event_loop_builder: EventLoopBuilder,
3012    runtime_args: RuntimeInitArgs<Cef>,
3013  ) -> Result<Self> {
3014    // Snapshot before CEF can touch anything, so we can tell an embedder's own
3015    // signal policy apart from the handlers CEF installs in `cef::initialize`.
3016    #[cfg(any(
3017      target_os = "linux",
3018      target_os = "dragonfly",
3019      target_os = "freebsd",
3020      target_os = "openbsd",
3021      target_os = "netbsd"
3022    ))]
3023    let pre_cef_signals = TerminationSignals::capture();
3024
3025    let args = cef::args::Args::new();
3026
3027    #[cfg(target_os = "macos")]
3028    let is_helper = is_cef_helper_process();
3029
3030    #[cfg(target_os = "macos")]
3031    let (_sandbox, _loader) = {
3032      // As in `run_cef_helper_process`: only a helper enters the sandbox, and only when
3033      // the browser process that launched it did not already drop the sandbox.
3034      let sandbox = if is_helper && !crate::sandbox::launched_without_sandbox() {
3035        let mut sandbox = cef::sandbox::Sandbox::new();
3036        sandbox.initialize(args.as_main_args());
3037        Some(sandbox)
3038      } else {
3039        None
3040      };
3041
3042      let loader =
3043        cef::library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), is_helper);
3044      assert!(loader.load());
3045
3046      (sandbox, loader)
3047    };
3048
3049    #[cfg(target_os = "macos")]
3050    if !is_helper {
3051      crate::platform::macos::setup_application();
3052    }
3053
3054    // The CEF API version table must be initialized before any other CEF call
3055    // (e.g. `args.as_cmd_line()` below), otherwise the process crashes with no
3056    // diagnostics.
3057    //
3058    // Child processes normally never get here: `#[cef_entry_point]` sends them to
3059    // `run_cef_helper_process` first. An app that does not use the macro runs its
3060    // children through this path, and they use the version the browser process
3061    // passed.
3062    let api_version = runtime_args.runtime_init_attrs.api_version;
3063    let version = child_process_api_version(std::env::args_os())
3064      .or(api_version)
3065      .unwrap_or(sys::CEF_API_VERSION_LAST);
3066    // `cef_api_hash` returns NULL if the loaded libcef does not support the version.
3067    // Log the details and return the same error as for a missing runtime.
3068    if cef::api_hash(version, 0).is_null() {
3069      log::error!("the loaded CEF library does not support CEF API version {version}");
3070      return Err(Error::WebviewRuntimeNotInstalled);
3071    }
3072
3073    // Handle CEF subprocesses (renderer/GPU/utility) before any browser-only
3074    // setup such as building the event loop, creating cache directories, or the
3075    // runtime context. The browser (main) process has no `type` switch;
3076    // subprocesses are launched with one (e.g. `--type=renderer`).
3077    let is_browser_process = args
3078      .as_cmd_line()
3079      .map(|cmd| cmd.has_switch(Some(&CefString::from("type"))) != 1)
3080      .unwrap_or(true);
3081
3082    if !is_browser_process {
3083      let mut helper_app = TauriCefHelperApp::new();
3084      let ret = cef::execute_process(
3085        Some(args.as_main_args()),
3086        Some(&mut helper_app),
3087        std::ptr::null_mut(),
3088      );
3089      // A subprocess finished its work; exit with its exit code instead of
3090      // falling through to browser runtime initialization.
3091      std::process::exit(ret.max(0));
3092    }
3093
3094    let Cef {
3095      command_line_args,
3096      disabled_features,
3097      enabled_features,
3098      deep_link_schemes,
3099      cache_path: cache_path_override,
3100      secret_storage,
3101      mut profile_preferences,
3102      mut global_preferences,
3103      content_settings,
3104      allow_chromium_command_line_args,
3105      log_file,
3106      log_severity,
3107      log_items,
3108      locale,
3109      accept_language_list,
3110      user_agent,
3111      user_agent_product,
3112      javascript_flags,
3113      chrome_policy_id,
3114      persist_session_cookies,
3115      remote_debugging,
3116      devtools: devtools_policy,
3117      debug_environment,
3118      certificate_errors,
3119      sandbox: sandbox_policy,
3120      downgrade: downgrade_policy,
3121      settings_callback,
3122      window_event_handler,
3123      // Already applied, above, before the first CEF call.
3124      api_version: _,
3125    } = runtime_args.runtime_init_attrs;
3126
3127    // CEF reads its crash-reporter overrides from `BasicStartupComplete`, which
3128    // `cef::initialize` below reaches, and every child process inherits this environment.
3129    // `SSLKEYLOGFILE` is answered further down, on the command line.
3130    crate::environment::remove_crash_reporter_overrides(debug_environment, tauri::is_dev());
3131
3132    // The application's own switches are the only ones that reach Chromium in a release
3133    // build, so a switch that turns off a security boundary got there deliberately —
3134    // say so rather than silently obeying.
3135    crate::switches::warn_about_dangerous_switches(&command_line_args);
3136    crate::switches::warn_about_replacing_switches(&command_line_args);
3137
3138    // Switches every process gets, and switches only the browser process gets. See the
3139    // `TauriCefApp` fields for why the split exists.
3140    #[allow(unused_mut)]
3141    let mut internal_command_line_args: Vec<(String, Option<String>)> = Vec::new();
3142    #[allow(unused_mut)]
3143    let mut browser_command_line_args: Vec<(String, Option<String>)> = Vec::new();
3144
3145    // `os_crypt` only ever runs in the browser process, so these are browser-only
3146    // switches. See `SecretStorage` for what each one costs.
3147    #[cfg(target_os = "macos")]
3148    {
3149      let mock_keychain = match secret_storage {
3150        SecretStorage::Auto => tauri::is_dev(),
3151        SecretStorage::Mock => true,
3152        SecretStorage::System => false,
3153      };
3154      if mock_keychain {
3155        browser_command_line_args.push(("--use-mock-keychain".to_string(), None));
3156      }
3157    }
3158    #[cfg(any(
3159      target_os = "linux",
3160      target_os = "dragonfly",
3161      target_os = "freebsd",
3162      target_os = "netbsd",
3163      target_os = "openbsd"
3164    ))]
3165    {
3166      // `basic` skips the D-Bus secret portal, libsecret and KWallet key providers, any
3167      // of which can block startup on a keyring-unlock dialog or fail outright in a
3168      // headless session. `Auto` splits the same way it does for the macOS keychain.
3169      let basic_password_store = match secret_storage {
3170        SecretStorage::Auto => tauri::is_dev(),
3171        SecretStorage::Mock => true,
3172        SecretStorage::System => false,
3173      };
3174      if basic_password_store {
3175        browser_command_line_args.push(("--password-store".to_string(), Some("basic".to_string())));
3176      }
3177    }
3178
3179    // One decision on every platform, so a lost sandbox is always something the policy
3180    // asked for and is always logged. On Linux and the BSDs the policy is also weighed
3181    // against the system, because Chromium aborts with "No usable sandbox!" when its
3182    // zygote host finds neither usable unprivileged user namespaces nor the setuid
3183    // `chrome-sandbox` helper, so an AppImage on a system that restricts namespaces
3184    // cannot start at all.
3185    let no_sandbox = {
3186      let decision = crate::sandbox::resolve_sandbox_decision(sandbox_policy);
3187      match decision {
3188        crate::sandbox::SandboxDecision::Keep => false,
3189        crate::sandbox::SandboxDecision::Disable(reason) => {
3190          log::warn!(
3191            "running Chromium without a sandbox: {}. A compromised renderer process runs with the full privileges of the current user.",
3192            reason.message()
3193          );
3194          true
3195        }
3196        crate::sandbox::SandboxDecision::Refuse(reason) => {
3197          log::error!(
3198            "refusing to start: SandboxPolicy::Required asked for a Chromium sandbox, but {}.",
3199            reason.message()
3200          );
3201          return Err(Error::CreateWebview(
3202            format!(
3203              "SandboxPolicy::Required cannot be honored: {}",
3204              reason.message()
3205            )
3206            .into(),
3207          ));
3208        }
3209      }
3210    };
3211    // Windows encrypts with DPAPI, which needs no switch and prompts for nothing.
3212    #[cfg(windows)]
3213    let _ = secret_storage;
3214
3215    // The DevTools gate, applied to every path this runtime owns: the context menu
3216    // entries, the F12 and Ctrl+Shift+I chords, the `IDC_DEV_TOOLS` commands and
3217    // `Webview::open_devtools`. See `DevToolsPolicy` for why it stops there.
3218    let devtools_allowed = match devtools_policy {
3219      DevToolsPolicy::Auto => cfg!(debug_assertions) || cfg!(feature = "devtools"),
3220      DevToolsPolicy::Allowed => true,
3221      DevToolsPolicy::Disallowed => false,
3222    };
3223
3224    // Never the disallowing value, whatever the policy says: CEF gates
3225    // `SendDevToolsMessage` on this same preference, and refuses it silently — the send
3226    // reports success and no result or event is ever delivered. This runtime drives its
3227    // own startup over the DevTools protocol (the document-start scripts, the per-webview
3228    // user agent) and defers each webview's first navigation until that round trip
3229    // answers, so a profile carrying `kDisallowed` leaves every window stuck on its blank
3230    // placeholder.
3231    //
3232    // Written as the default rather than simply left alone because Chromium persists it
3233    // in the profile on disk: a profile an earlier version wrote `kDisallowed` into stays
3234    // broken on every later run, in a debug build as much as a release one, until
3235    // something writes it back.
3236    profile_preferences.insert(
3237      0,
3238      (
3239        crate::cef_impl::preferences::DEVTOOLS_AVAILABILITY.to_string(),
3240        crate::cef_impl::preferences::DEVTOOLS_ALLOWED.into(),
3241      ),
3242    );
3243
3244    // The DevTools protocol server drives the browser from outside the process, so it is
3245    // refused two ways: the switch is only appended when the application asked for it,
3246    // and the local-state preference is pinned off otherwise so a switch that reaches
3247    // Chromium another way is refused as well.
3248    let remote_debugging_enabled = match &remote_debugging {
3249      RemoteDebugging::Disabled => false,
3250      RemoteDebugging::Pipe => {
3251        browser_command_line_args.push(("--remote-debugging-pipe".to_string(), None));
3252        true
3253      }
3254      RemoteDebugging::Port {
3255        port,
3256        allowed_origins,
3257      } => {
3258        // Chromium ignores a port below 1024, which would otherwise leave the application
3259        // believing it had a debugger it does not have.
3260        if *port < 1024 {
3261          log::warn!(
3262            "ignoring RemoteDebugging::Port {{ port: {port} }}: only ports between 1024 and 65535 are accepted"
3263          );
3264          false
3265        } else {
3266          browser_command_line_args.push((
3267            "--remote-debugging-port".to_string(),
3268            Some(port.to_string()),
3269          ));
3270          if !allowed_origins.is_empty() {
3271            browser_command_line_args.push((
3272              "--remote-allow-origins".to_string(),
3273              Some(allowed_origins.join(",")),
3274            ));
3275          }
3276          log::warn!(
3277            "the Chrome DevTools protocol is listening on port {port}. Anything that can \
3278             reach it can read and rewrite every page this application shows."
3279          );
3280          true
3281        }
3282      }
3283    };
3284    // Chromium consults `SSLKEYLOGFILE` only when this switch is absent, and an empty
3285    // value creates no key logger, so this is how the variable is refused without writing
3286    // to the process environment. Appended before the application's own switches so an
3287    // application that deliberately passes `--ssl-key-log-file` still wins.
3288    if crate::environment::neutralizes_tls_key_log(debug_environment, tauri::is_dev()) {
3289      browser_command_line_args.push(("--ssl-key-log-file".to_string(), Some(String::new())));
3290    }
3291
3292    // Pinned off whenever no transport was actually configured, including the rejected
3293    // port above: `RemoteDebuggingServer` consults this before it starts a server for
3294    // either transport, so it also refuses a switch that reaches Chromium another way.
3295    if !remote_debugging_enabled {
3296      global_preferences.insert(
3297        0,
3298        (
3299          crate::cef_impl::preferences::REMOTE_DEBUGGING_ALLOWED.to_string(),
3300          serde_json::Value::Bool(false),
3301        ),
3302      );
3303    }
3304
3305    let cache_path = cache_path_override.unwrap_or_else(|| {
3306      let cache_base = dirs::cache_dir().unwrap_or_else(std::env::temp_dir);
3307      cache_base.join(&runtime_args.identifier).join("cef")
3308    });
3309    let _ = create_dir_all(&cache_path);
3310    // Chromium migrates its profile forward only, so a release rolled back to an older CEF
3311    // milestone finds a profile a newer Chromium wrote. Recording which one ran, and what
3312    // becomes of such a profile, is the `downgrade` module; it must run before the first
3313    // CEF call that opens anything in the directory, which is `cef::initialize` below.
3314    crate::downgrade::prepare_root_cache_path(&cache_path, downgrade_policy);
3315
3316    // Force X11 usage on Linux.
3317    //
3318    // Applied to every process type rather than only the browser one: it is not certain
3319    // that Chromium propagates `ozone-platform` to the GPU process, and getting it wrong
3320    // there breaks rendering outright.
3321    #[cfg(any(
3322      target_os = "linux",
3323      target_os = "dragonfly",
3324      target_os = "freebsd",
3325      target_os = "netbsd",
3326      target_os = "openbsd"
3327    ))]
3328    {
3329      internal_command_line_args.push(("--ozone-platform".to_string(), Some("x11".to_string())));
3330      // CEF integration below uses XIDs for child windows/reparenting, so GDK must not honor an
3331      // inherited `GDK_BACKEND=wayland`. `set_allowed_backends` alone is not enough: GDK reads
3332      // `GDK_BACKEND` first and only intersects it with the allowed list, so an inherited
3333      // `wayland` would leave no backend to open a display with.
3334      //
3335      // SAFETY: `std::env::set_var` is only unsafe because another thread may be reading the
3336      // environment concurrently. This runs during runtime initialization, before any GTK, CEF or
3337      // Tauri thread that could read it has been spawned. Note the value is inherited by child
3338      // processes the app spawns later, which is intended for CEF's own subprocesses.
3339      unsafe { std::env::set_var("GDK_BACKEND", "x11") };
3340      gtk::gdk::set_allowed_backends("x11");
3341      event_loop_builder.with_gtk4();
3342
3343      // the GTK pointers this runtime hands out are GTK 4 objects, whichever bindings the `tauri`
3344      // crate was compiled against.
3345      tauri_runtime::gtk::declare_version(tauri_runtime::gtk::Version::V4);
3346    }
3347
3348    #[cfg(windows)]
3349    if let Some(hook) = runtime_args.msg_hook {
3350      use winit::platform::windows::EventLoopBuilderExtWindows;
3351      event_loop_builder.with_msg_hook(hook);
3352    }
3353
3354    #[cfg(target_os = "macos")]
3355    event_loop_builder.with_default_menu(false);
3356
3357    let event_loop = event_loop_builder
3358      .build()
3359      .map_err(|e| Error::CreateWindow(Box::new(e)))?;
3360    let proxy = event_loop.create_proxy();
3361    let (sender, receiver) = mpsc::channel();
3362    let context_initialized = Arc::new(AtomicBool::new(false));
3363    let cef_pump = CefExternalPump::new();
3364    let current_dispatch: Arc<MainThreadDispatchSlot<T>> = Default::default();
3365    #[cfg(target_os = "macos")]
3366    let wake = {
3367      let slot = current_dispatch.clone();
3368      crate::platform::macos::MainThreadWake::new(move || slot.deliver_wake(&proxy))
3369    };
3370    let context = RuntimeContext {
3371      window_event_handler,
3372      sender: sender.clone(),
3373      #[cfg(not(target_os = "macos"))]
3374      proxy,
3375      #[cfg(target_os = "macos")]
3376      wake,
3377      main_thread_id: std::thread::current().id(),
3378      next_window_id: Default::default(),
3379      next_webview_id: Default::default(),
3380      next_window_event_id: Default::default(),
3381      next_webview_event_id: Default::default(),
3382      current_dispatch,
3383      app_wide_theme: Default::default(),
3384      cef_pump,
3385      cache_path: Arc::new(cache_path.clone()),
3386      profile_preferences: Arc::new(profile_preferences),
3387      content_settings: Arc::new(content_settings),
3388      certificate_errors,
3389      devtools_allowed,
3390    };
3391
3392    internal_command_line_args.push(("--no-first-run".to_string(), None));
3393
3394    // Appended last so an application switch overrides a runtime default with the same
3395    // name: Chromium's command line keeps the last value appended for a given switch.
3396    browser_command_line_args.extend(command_line_args);
3397
3398    // Shipped applications ignore Chromium switches passed on their own command line:
3399    // otherwise anyone able to launch the app can also launch it with
3400    // `--remote-debugging-port` and drive it over the DevTools protocol, or with
3401    // `--disable-web-security`, `--proxy-server`, `--host-resolver-rules` or
3402    // `--ssl-key-log-file`, all of which Chromium honours. CEF clears the command line
3403    // before applying `Settings` and before calling `on_before_command_line_processing`,
3404    // so the switches this runtime and the application configure still take effect, and
3405    // Tauri's own CLI parsing reads `std::env::args()`, which Chromium never touches.
3406    // The clear does break the *relaunch* deep link path, which
3407    // `TauriCefApp::on_before_command_line_processing` restores.
3408    let command_line_args_disabled = !(allow_chromium_command_line_args || tauri::is_dev());
3409
3410    let mut app = TauriCefApp::build(TauriCefAppArgs {
3411      context: context.clone(),
3412      context_initialized: context_initialized.clone(),
3413      deep_link_schemes,
3414      restore_deep_link_arguments: command_line_args_disabled,
3415      internal_command_line_args,
3416      browser_command_line_args,
3417      api_version,
3418      disabled_features,
3419      enabled_features,
3420    });
3421
3422    // Subprocesses already exited above, so this must be the browser process;
3423    // `execute_process` returns -1 there to signal normal startup should follow.
3424    let ret = cef::execute_process(
3425      Some(args.as_main_args()),
3426      Some(&mut app),
3427      std::ptr::null_mut(),
3428    );
3429    assert_eq!(
3430      ret, -1,
3431      "CEF browser process unexpectedly returned from execute_process"
3432    );
3433
3434    // Chromium drops a `debug.log` next to the *main executable* when no log file is
3435    // configured, which for an installed application is often not even writable. Keep it
3436    // next to the rest of the runtime's state instead.
3437    let log_file = log_file.unwrap_or_else(|| cache_path.join("cef.log"));
3438    // CEF logs at INFO by default, which grows that file quickly in a long-running app.
3439    let log_severity = log_severity.unwrap_or(if tauri::is_dev() {
3440      LogSeverity::DEFAULT
3441    } else {
3442      LogSeverity::WARNING
3443    });
3444
3445    let mut settings = cef::Settings {
3446      // Only this, never a `--no-sandbox` push of our own: CEF appends that switch itself
3447      // from the setting, before `on_before_command_line_processing` runs, so the setting
3448      // and the switch cannot end up disagreeing.
3449      no_sandbox: no_sandbox as std::os::raw::c_int,
3450      cache_path: cache_path.to_string_lossy().to_string().as_str().into(),
3451      command_line_args_disabled: command_line_args_disabled as std::os::raw::c_int,
3452      log_file: log_file.to_string_lossy().to_string().as_str().into(),
3453      log_severity,
3454      persist_session_cookies: persist_session_cookies as std::os::raw::c_int,
3455      external_message_pump: 1,
3456      ..Default::default()
3457    };
3458
3459    if let Some(log_items) = log_items {
3460      settings.log_items = log_items;
3461    }
3462
3463    // Left at CEF's defaults unless the application asked for something else: the bundler
3464    // only ships the `en-US` locale pak, so any other locale would leave Chromium without
3465    // its localized resources.
3466    if let Some(locale) = locale {
3467      settings.locale = locale.as_str().into();
3468    }
3469
3470    // With neither of these set CEF appends `--lang=en-US` and derives the accept-language
3471    // list from it, so every user of every CEF application reports `navigator.language ===
3472    // "en-US"` and asks servers for English. The locale pak constraint above does not
3473    // apply here — this is a list of language codes, not a resource bundle — so the
3474    // runtime answers with what the user actually asked their system for.
3475    let accept_language_list =
3476      accept_language_list.or_else(crate::locale::system_accept_language_list);
3477    if let Some(accept_language_list) = accept_language_list {
3478      settings.accept_language_list = accept_language_list.as_str().into();
3479    }
3480
3481    // `user_agent` wins over `user_agent_product` in CEF, so setting both is the
3482    // application contradicting itself; say so rather than silently dropping one.
3483    if let Some(user_agent) = &user_agent {
3484      settings.user_agent = user_agent.as_str().into();
3485      if user_agent_product.is_some() {
3486        log::warn!(
3487          "ignoring the CEF user agent product: Cef::user_agent replaces the whole User-Agent string, including the product token"
3488        );
3489      }
3490    } else if let Some(user_agent_product) = &user_agent_product {
3491      settings.user_agent_product = user_agent_product.as_str().into();
3492    }
3493
3494    if let Some(javascript_flags) = &javascript_flags {
3495      settings.javascript_flags = javascript_flags.as_str().into();
3496    }
3497    if let Some(chrome_policy_id) = &chrome_policy_id {
3498      settings.chrome_policy_id = chrome_policy_id.as_str().into();
3499    }
3500
3501    if let Some(callback) = settings_callback {
3502      callback(&mut settings);
3503    }
3504    if cef::initialize(
3505      Some(args.as_main_args()),
3506      Some(&settings),
3507      Some(&mut app),
3508      std::ptr::null_mut(),
3509    ) != 1
3510    {
3511      return Err(Error::WebviewRuntimeNotInstalled);
3512    }
3513
3514    #[cfg(any(
3515      target_os = "linux",
3516      target_os = "dragonfly",
3517      target_os = "freebsd",
3518      target_os = "openbsd",
3519      target_os = "netbsd"
3520    ))]
3521    pre_cef_signals.restore();
3522
3523    #[cfg(target_os = "macos")]
3524    let app_delegate = if !is_helper {
3525      use crate::platform::macos::AppDelegateEvent;
3526
3527      let context_ = context.clone();
3528      let handler = Box::new(move |event| match event {
3529        AppDelegateEvent::TryTerminate => {
3530          let _ = context_.send_message(Message::RequestExit(0));
3531        }
3532        AppDelegateEvent::Reopen {
3533          has_visible_windows,
3534        } => {
3535          let _ = context_.send_message(Message::Reopen {
3536            has_visible_windows,
3537          });
3538        }
3539        AppDelegateEvent::AccessibilityChanged { enabled } => {
3540          let _ = context_.send_message(Message::AccessibilityChanged { enabled });
3541        }
3542        AppDelegateEvent::OpenURLs { urls } => {
3543          let _ = context_.send_message(Message::Opened(urls));
3544        }
3545      });
3546      let app_delegate = crate::platform::macos::set_application_event_handler(handler);
3547      Some(app_delegate)
3548    } else {
3549      None
3550    };
3551
3552    // Wait for the CEF context to initialize before returning, so that the runtime is ready to create browsers.
3553    while !context_initialized.load(Ordering::SeqCst) {
3554      context.cef_pump.do_work();
3555      std::thread::sleep(Duration::from_millis(1));
3556    }
3557
3558    // Local state exists only once the context is initialized, and
3559    // `preference_manager_get_global` has to be called on the browser UI thread — which
3560    // is this one, since the runtime drives CEF from the main thread through an external
3561    // message pump.
3562    crate::cef_impl::preferences::apply_global_preferences(&global_preferences);
3563
3564    Ok(Self {
3565      event_loop,
3566      receiver,
3567      context,
3568      scheme_registry: Default::default(),
3569      #[cfg(target_os = "macos")]
3570      _app_delegate: app_delegate,
3571    })
3572  }
3573}
3574
3575impl<T: UserEvent> Runtime<T> for CefRuntime<T> {
3576  type WindowDispatcher = CefWindowDispatcher<T>;
3577  type WebviewDispatcher = CefWebviewDispatcher<T>;
3578  type Handle = CefRuntimeHandle<T>;
3579  type EventLoopProxy = EventProxy<T>;
3580  type RuntimeWebviewAttributes = CefWebviewAttributes;
3581  type Webview = Webview;
3582  type RuntimeInitAttrs = Cef;
3583  type WindowOpener = NewWindowOpener;
3584
3585  fn new(args: RuntimeInitArgs<Self::RuntimeInitAttrs>) -> Result<Self> {
3586    Self::init(EventLoopBuilder::default(), args)
3587  }
3588
3589  #[cfg(any(
3590    windows,
3591    target_os = "linux",
3592    target_os = "dragonfly",
3593    target_os = "freebsd",
3594    target_os = "netbsd",
3595    target_os = "openbsd"
3596  ))]
3597  fn new_any_thread(args: RuntimeInitArgs<Self::RuntimeInitAttrs>) -> Result<Self> {
3598    let mut event_loop_builder = EventLoopBuilder::default();
3599    event_loop_builder.with_any_thread(true);
3600    Self::init(event_loop_builder, args)
3601  }
3602
3603  fn create_proxy(&self) -> Self::EventLoopProxy {
3604    EventProxy {
3605      context: self.context.clone(),
3606    }
3607  }
3608
3609  fn handle(&self) -> Self::Handle {
3610    CefRuntimeHandle {
3611      context: self.context.clone(),
3612    }
3613  }
3614
3615  fn create_window<F: Fn(RawWindow<'_>) + Send + 'static>(
3616    &self,
3617    pending: PendingWindow<T, Self>,
3618    after_window_creation: Option<F>,
3619  ) -> Result<DetachedWindow<T, Self>> {
3620    create_window_detached(&self.context, pending, after_window_creation)
3621  }
3622
3623  fn create_webview(
3624    &self,
3625    window_id: WindowId,
3626    pending: PendingWebview<T, Self>,
3627  ) -> Result<DetachedWebview<T, Self>> {
3628    create_webview_detached(&self.context, window_id, pending)
3629  }
3630
3631  fn primary_monitor(&self) -> Option<Monitor> {
3632    event_loop_getter!(self, PrimaryMonitor)
3633      .flatten()
3634      .ok()
3635      .unwrap_or_default()
3636  }
3637
3638  fn monitor_from_point(&self, x: f64, y: f64) -> Option<Monitor> {
3639    let (tx, rx) = mpsc::channel();
3640    self
3641      .context
3642      .send_message(Message::EventLoop(EventLoopMessage::MonitorFromPoint(
3643        tx, x, y,
3644      )))
3645      .and_then(|_| rx.recv().map_err(|_| Error::FailedToReceiveMessage))
3646      .ok()?
3647      .ok()
3648      .unwrap_or_default()
3649  }
3650
3651  fn available_monitors(&self) -> Vec<Monitor> {
3652    event_loop_getter!(self, AvailableMonitors)
3653      .flatten()
3654      .ok()
3655      .unwrap_or_default()
3656  }
3657
3658  fn cursor_position(&self) -> Result<PhysicalPosition<f64>> {
3659    event_loop_getter!(self, CursorPosition)?
3660  }
3661
3662  fn set_theme(&self, theme: Option<Theme>) {
3663    let message = Message::EventLoop(EventLoopMessage::SetTheme(theme));
3664    let _ = self.context.send_message(message);
3665  }
3666
3667  #[cfg(target_os = "macos")]
3668  fn set_activation_policy(&mut self, activation_policy: tauri_runtime::ActivationPolicy) {
3669    let message = Message::EventLoop(EventLoopMessage::SetActivationPolicy(activation_policy));
3670    let _ = self.context.send_message(message);
3671  }
3672
3673  #[cfg(target_os = "macos")]
3674  fn set_activate_ignoring_other_apps(&mut self, ignore: bool) {
3675    // TODO: honor this flag. The winit event loop is already built by the time tauri calls this
3676    // (see `Self::init`), and launch-time activation is driven by the CEF application delegate
3677    // rather than winit's `with_activate_ignoring_other_apps`.
3678    if !ignore {
3679      log::warn!(
3680        "`Builder::activate_ignoring_other_apps(false)` is not supported by the CEF runtime yet"
3681      );
3682    }
3683  }
3684
3685  #[cfg(target_os = "macos")]
3686  fn set_dock_visibility(&mut self, visible: bool) {
3687    let message = Message::EventLoop(EventLoopMessage::SetDockVisibility(visible));
3688    let _ = self.context.send_message(message);
3689  }
3690
3691  #[cfg(target_os = "macos")]
3692  fn show(&self) {
3693    let message = Message::EventLoop(EventLoopMessage::ShowApplication);
3694    let _ = self.context.send_message(message);
3695  }
3696
3697  #[cfg(target_os = "macos")]
3698  fn hide(&self) {
3699    let message = Message::EventLoop(EventLoopMessage::HideApplication);
3700    let _ = self.context.send_message(message);
3701  }
3702
3703  fn set_device_event_filter(&mut self, filter: DeviceEventFilter) {
3704    self
3705      .event_loop
3706      .listen_device_events(device_event_filter_to_winit(filter));
3707  }
3708
3709  fn run_iteration<F: FnMut(RunEvent<T>) + 'static>(&mut self, mut callback: F) {
3710    while let Ok(message) = self.receiver.try_recv() {
3711      if let Message::UserEvent(event) = message {
3712        callback(RunEvent::UserEvent(event));
3713      }
3714    }
3715    self.context.cef_pump.do_work();
3716    callback(RunEvent::MainEventsCleared);
3717  }
3718
3719  fn run_return<F: FnMut(RunEvent<T>) + 'static>(self, callback: F) -> i32 {
3720    self.run(callback);
3721    // TODO: return the exit code from the runtime, if possible. For now, always return 0
3722    0
3723  }
3724
3725  fn run<F: FnMut(RunEvent<T>) + 'static>(self, callback: F) {
3726    let app = WinitCefApp::new(
3727      self.context,
3728      self.receiver,
3729      Box::new(callback),
3730      self.scheme_registry,
3731    );
3732    let _ = self.event_loop.run_app(app);
3733    cef::shutdown();
3734  }
3735}
3736
3737#[cfg(test)]
3738mod configuration_tests {
3739  use super::*;
3740
3741  #[test]
3742  fn a_fixed_proxy_is_spelled_the_way_chromium_spells_it() {
3743    let preference = ProxyConfig::FixedServers {
3744      server: "socks5://127.0.0.1:9050".to_string(),
3745      bypass_list: Some("*.internal".to_string()),
3746    }
3747    .to_preference();
3748
3749    assert_eq!(
3750      preference,
3751      serde_json::json!({
3752        "mode": "fixed_servers",
3753        "server": "socks5://127.0.0.1:9050",
3754        "bypass_list": "*.internal",
3755      })
3756    );
3757  }
3758
3759  #[test]
3760  fn a_fixed_proxy_without_a_bypass_list_omits_the_key() {
3761    // Chromium rejects the whole `proxy` dictionary when it carries a key the mode does
3762    // not accept, so an absent bypass list must be absent rather than empty.
3763    let preference = ProxyConfig::FixedServers {
3764      server: "http://proxy:8080".to_string(),
3765      bypass_list: None,
3766    }
3767    .to_preference();
3768
3769    assert_eq!(
3770      preference,
3771      serde_json::json!({ "mode": "fixed_servers", "server": "http://proxy:8080" })
3772    );
3773  }
3774
3775  #[test]
3776  fn the_modeless_proxy_configurations_carry_only_a_mode() {
3777    for (config, mode) in [
3778      (ProxyConfig::System, "system"),
3779      (ProxyConfig::Direct, "direct"),
3780      (ProxyConfig::AutoDetect, "auto_detect"),
3781    ] {
3782      assert_eq!(config.to_preference(), serde_json::json!({ "mode": mode }));
3783    }
3784    assert_eq!(
3785      ProxyConfig::PacScript {
3786        url: "http://wpad/proxy.pac".to_string()
3787      }
3788      .to_preference(),
3789      serde_json::json!({ "mode": "pac_script", "pac_url": "http://wpad/proxy.pac" })
3790    );
3791  }
3792
3793  #[test]
3794  fn the_default_policies_append_no_switch_at_all() {
3795    // Chromium's own default is not one of the named values, so `Default` has to mean
3796    // "leave the switch off" rather than "pass the default explicitly".
3797    assert_eq!(AutoplayPolicy::default().as_switch_value(), None);
3798    assert_eq!(WebRtcIpHandling::default().as_switch_value(), None);
3799  }
3800
3801  #[test]
3802  fn the_named_policies_use_chromiums_own_spelling() {
3803    assert_eq!(
3804      AutoplayPolicy::NoUserGestureRequired.as_switch_value(),
3805      Some("no-user-gesture-required"),
3806      "autoplay values are hyphenated"
3807    );
3808    assert_eq!(
3809      WebRtcIpHandling::DisableNonProxiedUdp.as_switch_value(),
3810      Some("disable_non_proxied_udp"),
3811      "WebRTC values are underscored"
3812    );
3813  }
3814
3815  #[test]
3816  fn remote_debugging_is_off_by_default() {
3817    assert_eq!(RemoteDebugging::default(), RemoteDebugging::Disabled);
3818  }
3819
3820  #[test]
3821  fn the_defaults_are_the_conservative_ones() {
3822    let cef = Cef::default();
3823    assert_eq!(cef.devtools, DevToolsPolicy::Auto);
3824    assert_eq!(cef.debug_environment, DebugEnvironment::Auto);
3825    assert_eq!(cef.sandbox, SandboxPolicy::Auto);
3826    assert_eq!(
3827      cef.downgrade,
3828      DowngradePolicy::KeepProfile,
3829      "a rollback keeps the user's data on disk, as Chrome does; wiping it is opted into"
3830    );
3831    assert!(
3832      !cef.allow_chromium_command_line_args,
3833      "a shipped application must ignore Chromium switches on its command line"
3834    );
3835    assert!(
3836      !cef.persist_session_cookies,
3837      "a session cookie is dropped on exit, as it is in a browser"
3838    );
3839    assert_eq!(
3840      cef.certificate_errors,
3841      CertificateErrorPolicy::ChromeInterstitial
3842    );
3843  }
3844
3845  #[test]
3846  fn a_typed_option_is_just_a_preference() {
3847    // The typed options and the escape hatch write the same store, so an application can
3848    // reach anything the typed set does not cover.
3849    let cef = Cef::default().safe_browsing(false).spell_checking(false);
3850    assert!(
3851      cef
3852        .profile_preferences
3853        .iter()
3854        .any(|(name, value)| name == "safebrowsing.enabled" && value == &serde_json::json!(false))
3855    );
3856    assert!(
3857      cef
3858        .profile_preferences
3859        .iter()
3860        .any(|(name, value)| name == "browser.enable_spellchecking"
3861          && value == &serde_json::json!(false))
3862    );
3863  }
3864
3865  #[test]
3866  fn a_later_preference_wins_over_an_earlier_one() {
3867    // They are applied in order, so the last one written is the one that sticks.
3868    let cef = Cef::default()
3869      .safe_browsing(false)
3870      .profile_preference("safebrowsing.enabled", true);
3871    let values: Vec<_> = cef
3872      .profile_preferences
3873      .iter()
3874      .filter(|(name, _)| name == "safebrowsing.enabled")
3875      .map(|(_, value)| value.clone())
3876      .collect();
3877    assert_eq!(
3878      values,
3879      [serde_json::json!(false), serde_json::json!(true)],
3880      "both are kept, in call order, so the application's last word wins"
3881    );
3882  }
3883}
3884
3885#[cfg(test)]
3886mod child_process_api_version_tests {
3887  use super::{API_VERSION_SWITCH, child_process_api_version};
3888
3889  fn version(args: &[&str]) -> Option<i32> {
3890    child_process_api_version(args.iter().map(std::ffi::OsString::from))
3891  }
3892
3893  #[test]
3894  fn reads_the_version_the_browser_process_passed() {
3895    assert_eq!(
3896      version(&[
3897        "app",
3898        "--type=renderer",
3899        "--tauri-cef-api-version=15101",
3900        "--lang=en-US"
3901      ]),
3902      Some(15101)
3903    );
3904    // Order relative to `--type` must not matter.
3905    assert_eq!(
3906      version(&["app", "--tauri-cef-api-version=15101", "--type=zygote"]),
3907      Some(15101)
3908    );
3909  }
3910
3911  #[test]
3912  fn the_switch_is_spelled_the_way_it_is_appended() {
3913    let arg = format!("--{API_VERSION_SWITCH}=15101");
3914    assert_eq!(version(&["app", "--type=gpu-process", &arg]), Some(15101));
3915  }
3916
3917  #[test]
3918  fn absent_means_the_default() {
3919    assert_eq!(version(&[]), None);
3920    assert_eq!(version(&["app", "--type=renderer"]), None);
3921    // A switch with a longer name is a different switch.
3922    assert_eq!(
3923      version(&["app", "--type=renderer", "--tauri-cef-api-version-x=15101"]),
3924      None
3925    );
3926  }
3927
3928  #[test]
3929  fn garbage_means_the_default() {
3930    for value in ["", "latest", "15101x", "1.5", "99999999999", " 15101"] {
3931      let arg = format!("--tauri-cef-api-version={value}");
3932      assert_eq!(
3933        version(&["app", "--type=renderer", &arg]),
3934        None,
3935        "{value:?}"
3936      );
3937    }
3938    // A bare switch has no value.
3939    assert_eq!(
3940      version(&["app", "--type=renderer", "--tauri-cef-api-version"]),
3941      None
3942    );
3943  }
3944
3945  #[test]
3946  fn the_last_occurrence_wins() {
3947    assert_eq!(
3948      version(&[
3949        "app",
3950        "--type=renderer",
3951        "--tauri-cef-api-version=15000",
3952        "--tauri-cef-api-version=15101"
3953      ]),
3954      Some(15101)
3955    );
3956    assert_eq!(
3957      version(&[
3958        "app",
3959        "--type=renderer",
3960        "--tauri-cef-api-version=15101",
3961        "--tauri-cef-api-version=latest"
3962      ]),
3963      None
3964    );
3965  }
3966
3967  #[test]
3968  fn a_browser_process_ignores_the_switch() {
3969    // Not a child process, so the switch is ignored. Users must not be able to set
3970    // the version from the command line.
3971    assert_eq!(version(&["app", "--tauri-cef-api-version=15101"]), None);
3972  }
3973
3974  #[cfg(unix)]
3975  #[test]
3976  fn an_argument_that_is_not_unicode_is_skipped() {
3977    use std::os::unix::ffi::OsStringExt;
3978
3979    let args = vec![
3980      std::ffi::OsString::from("app"),
3981      std::ffi::OsString::from_vec(vec![0xff, 0xfe]),
3982      std::ffi::OsString::from("--type=renderer"),
3983      std::ffi::OsString::from("--tauri-cef-api-version=15101"),
3984    ];
3985    assert_eq!(child_process_api_version(args), Some(15101));
3986  }
3987}
3988
3989#[cfg(test)]
3990mod deep_link_argument_tests {
3991  use super::deep_link_arguments;
3992
3993  fn schemes() -> Vec<String> {
3994    vec!["myapp".to_string(), "my-other-app".to_string()]
3995  }
3996
3997  fn filter(args: &[&str]) -> Vec<String> {
3998    deep_link_arguments(args.iter().map(|arg| (*arg).to_string()), &schemes())
3999  }
4000
4001  #[test]
4002  fn keeps_configured_deep_links_in_order() {
4003    assert_eq!(
4004      filter(&["myapp://open/one", "my-other-app://open/two"]),
4005      vec![
4006        "myapp://open/one".to_string(),
4007        "my-other-app://open/two".to_string(),
4008      ]
4009    );
4010  }
4011
4012  #[test]
4013  fn drops_everything_that_is_not_a_configured_deep_link() {
4014    // The lockdown exists so that none of these reach Chromium's command line, and a
4015    // URL with an unconfigured scheme is not this application's deep link either.
4016    assert!(
4017      filter(&[
4018        "--remote-debugging-port=9222",
4019        "--disable-web-security",
4020        "/home/user/document.txt",
4021        "not a url",
4022        "",
4023        "https://example.com",
4024        "otherapp://open",
4025      ])
4026      .is_empty()
4027    );
4028  }
4029
4030  #[test]
4031  fn an_empty_scheme_list_keeps_nothing() {
4032    assert!(deep_link_arguments(["myapp://open".to_string()], &[]).is_empty());
4033  }
4034
4035  #[test]
4036  fn scheme_matching_is_exact() {
4037    // `on_already_running_app_relaunch` compares schemes the same way, so anything
4038    // matched loosely here would be re-appended and then ignored on the other end.
4039    // `Url::parse` lowercases the scheme it reports, hence the upper-case spelling
4040    // below still matching.
4041    assert_eq!(filter(&["MYAPP://open"]), vec!["MYAPP://open".to_string()]);
4042    assert!(filter(&["myapp2://open", "myap://open"]).is_empty());
4043  }
4044}