diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..f685749 --- /dev/null +++ b/flake.lock @@ -0,0 +1,96 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1787498568, + "narHash": "sha256-9i/VTdusq/+NM/tz+J1Re+ojkMB8MBf0QshnYfzHz30=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "56c02bc00adcf003215cc4bd996d6efaf4cff188", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1744536153, + "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1787627053, + "narHash": "sha256-QdVaU2WSCKsxZ0sBDIRnaaukp6Z5IMUE0gDEDoSRJd0=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "132a10336af9ae819bdf640c0dd1c789b12d7107", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..ebc7dcd --- /dev/null +++ b/flake.nix @@ -0,0 +1,36 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + rust-overlay.url = "github:oxalica/rust-overlay"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ (import rust-overlay) ]; + }; + + rustToolchain = pkgs.rust-bin.stable."1.88.0".default.override { + extensions = [ "rust-src" "clippy" "rustfmt" ]; + }; + in { + devShell = pkgs.mkShell { + nativeBuildInputs = with pkgs; [ + rustToolchain + ]; + + buildInputs = with pkgs; [ + gtk3 + openssl + pkg-config + webkitgtk_4_1 + + rust-analyzer + ]; + }; + } + ); +} diff --git a/src/cef_impl/request_context.rs b/src/cef_impl/request_context.rs index af705ff..608ddf6 100644 --- a/src/cef_impl/request_context.rs +++ b/src/cef_impl/request_context.rs @@ -180,10 +180,10 @@ pub(crate) fn wait_for_deferred_init(flag: &Arc) { /// [`wait_for_deferred_init`] on this thread toggles the flag, which makes /// nesting (e.g. an `on_initialized` continuation that creates another /// webview) safe. -struct AllowNestableTasks; +pub(crate) struct AllowNestableTasks; impl AllowNestableTasks { - fn enter() -> Self { + pub(crate) fn enter() -> Self { NESTABLE_TASKS_DEPTH.with(|depth| { let current = depth.get(); if current == 0 { diff --git a/src/config.rs b/src/config.rs index 9d2d56d..dac743c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -23,6 +23,15 @@ use std::path::PathBuf; use std::sync::OnceLock; +/// Linux window system selected before CEF and the event loop start. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum LinuxWindowing { + #[default] + X11, + /// A CEF Views-owned top-level window using Ozone/Wayland. + Wayland, +} + /// CEF runtime configuration, applied at `CefRuntime` creation (browser /// process) and at custom-scheme registration (all processes). #[derive(Debug, Clone)] @@ -46,6 +55,8 @@ pub struct CefConfig { /// by default, so `document.cookie` and `Set-Cookie` are inert on custom /// schemes not listed here. The http/https defaults are preserved. pub cookieable_schemes: Vec, + /// Linux window system. Ignored on other platforms. + pub linux_windowing: LinuxWindowing, } impl Default for CefConfig { @@ -60,6 +71,7 @@ impl Default for CefConfig { deep_link_schemes: Vec::new(), custom_schemes: vec!["tauri".into(), "ipc".into(), "asset".into()], cookieable_schemes: Vec::new(), + linux_windowing: LinuxWindowing::X11, } } } @@ -76,3 +88,8 @@ pub fn configure(config: CefConfig) { pub(crate) fn config() -> &'static CefConfig { CONFIG.get_or_init(CefConfig::default) } + +#[cfg(target_os = "linux")] +pub(crate) fn native_wayland() -> bool { + config().linux_windowing == LinuxWindowing::Wayland +} diff --git a/src/lib.rs b/src/lib.rs index 67137c7..2619a31 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,12 +13,14 @@ mod platform; mod policy; mod runtime; mod streaming; +#[cfg(target_os = "linux")] +mod wayland; mod webview; mod window; mod window_builder; mod window_handle; -pub use config::{CefConfig, configure}; +pub use config::{CefConfig, LinuxWindowing, configure}; #[cfg(any( target_os = "linux", target_os = "dragonfly", diff --git a/src/platform/linux/utils.rs b/src/platform/linux/utils.rs index 74e77ca..a991953 100644 --- a/src/platform/linux/utils.rs +++ b/src/platform/linux/utils.rs @@ -108,6 +108,11 @@ unsafe extern "C" fn x_io_error_handler(_display: *mut xlib::Display) -> c_int { /// why cefclient installs its handlers *after* `gtk_init` rather than before. /// Calling this more than once is harmless. pub fn install_x_error_handlers() { + #[cfg(target_os = "linux")] + if crate::config::native_wayland() { + return; + } + let Some(xlib) = XLIB.as_ref() else { return; }; diff --git a/src/runtime.rs b/src/runtime.rs index 59343bf..439e443 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -53,6 +53,8 @@ use crate::{ }; #[cfg(target_os = "macos")] use winit::platform::macos::EventLoopBuilderExtMacOS; +#[cfg(target_os = "linux")] +use winit::platform::wayland::EventLoopBuilderExtWayland; #[cfg(windows)] use winit::platform::windows::EventLoopBuilderExtWindows; #[cfg(any( @@ -113,7 +115,8 @@ pub(crate) struct RuntimeContext { /// treated as valid beyond their callback. #[derive(Clone, Copy)] struct MainThreadDispatch { - app: *mut WinitCefApp, + app: *mut (), + handle: unsafe fn(*mut (), &dyn ActiveEventLoop, Message), event_loop: *const dyn ActiveEventLoop, } @@ -154,7 +157,7 @@ impl Default for MainThreadDispatchSlot { } } -struct MainThreadDispatchGuard { +pub(crate) struct MainThreadDispatchGuard { context: RuntimeContext, dispatch: Box>, previous: *mut MainThreadDispatch, @@ -178,13 +181,11 @@ fn handle_main_thread_message( return Err(message); }; - // SAFETY: `WinitCefApp::install_current_dispatch` stores pointers to the currently - // executing winit application handler and event-loop callback. This function - // is only called on the runtime main thread while that callback is active. - let app = unsafe { &mut *dispatch.app }; + // SAFETY: `install_current_dispatch` stores the currently executing application + // handler and event-loop callback. This only runs on the runtime main thread + // while that callback is active. let event_loop = unsafe { &*dispatch.event_loop }; - - app.handle_message(event_loop, message); + unsafe { (dispatch.handle)(dispatch.app, event_loop, message) }; Ok(()) } @@ -196,6 +197,25 @@ impl fmt::Debug for RuntimeContext { } impl RuntimeContext { + pub(crate) fn install_current_dispatch( + &self, + app: *mut (), + handle: unsafe fn(*mut (), &dyn ActiveEventLoop, Message), + event_loop: &dyn ActiveEventLoop, + ) -> MainThreadDispatchGuard { + let mut dispatch = Box::new(MainThreadDispatch { + app, + handle, + event_loop: event_loop as *const _, + }); + let previous = self.current_dispatch.install(dispatch.as_mut()); + MainThreadDispatchGuard { + context: self.clone(), + dispatch, + previous, + } + } + pub(crate) fn send_message(&self, message: Message) -> Result<()> { let message = if self.is_main_thread() { match handle_main_thread_message(self, message) { @@ -464,18 +484,18 @@ impl WinitCefApp { &mut self, event_loop: &dyn ActiveEventLoop, ) -> MainThreadDispatchGuard { - let mut dispatch = Box::new(MainThreadDispatch { - app: self as *mut _, - event_loop: event_loop as *const _, - }); - - let previous = self.context.current_dispatch.install(dispatch.as_mut()); - - MainThreadDispatchGuard { - context: self.context.clone(), - dispatch, - previous, + unsafe fn handle( + app: *mut (), + event_loop: &dyn ActiveEventLoop, + message: Message, + ) { + unsafe { &mut *app.cast::>() }.handle_message(event_loop, message); } + + let app = (self as *mut Self).cast(); + self + .context + .install_current_dispatch(app, handle::, event_loop) } fn drain_messages(&mut self, event_loop: &dyn ActiveEventLoop) { @@ -1486,9 +1506,16 @@ impl CefRuntime { )); } - // Force X11 usage on Linux + #[cfg(target_os = "linux")] + if crate::config::native_wayland() { + command_line_args.push(("ozone-platform".into(), Some("wayland".into()))); + event_loop_builder.with_wayland(); + } else { + command_line_args.push(("ozone-platform".into(), Some("x11".into()))); + event_loop_builder.with_x11(); + } + #[cfg(any( - target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", @@ -1599,8 +1626,11 @@ impl CefRuntime { // Baseline for embedders that never touch GTK. One that calls `gtk_init` // must call `install_x_error_handlers` again afterwards — GTK's X11 backend // replaces the handler during init. + #[cfg(target_os = "linux")] + if !crate::config::native_wayland() { + crate::platform::linux::install_x_error_handlers(); + } #[cfg(any( - target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", @@ -1674,6 +1704,13 @@ impl Runtime for CefRuntime { ))] fn new_any_thread(args: RuntimeInitArgs) -> Result { let mut event_loop_builder = EventLoopBuilder::default(); + #[cfg(target_os = "linux")] + if crate::config::native_wayland() { + EventLoopBuilderExtWayland::with_any_thread(&mut event_loop_builder, true); + } else { + EventLoopBuilderExtX11::with_any_thread(&mut event_loop_builder, true); + } + #[cfg(not(target_os = "linux"))] event_loop_builder.with_any_thread(true); Self::init(event_loop_builder, args) } @@ -1777,6 +1814,19 @@ impl Runtime for CefRuntime { fn run_return) + 'static>(self, callback: F) -> i32 { let exit_code = Arc::new(std::sync::atomic::AtomicI32::new(0)); + #[cfg(target_os = "linux")] + if crate::config::native_wayland() { + let app = crate::wayland::App::new( + self.context, + self.receiver, + Box::new(callback), + self.scheme_registry, + exit_code.clone(), + ); + let _ = self.event_loop.run_app(app); + cef::shutdown(); + return exit_code.load(Ordering::Acquire); + } let app = WinitCefApp::new( self.context, self.receiver, diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs new file mode 100644 index 0000000..f82c614 --- /dev/null +++ b/src/wayland/mod.rs @@ -0,0 +1,475 @@ +//! Native Wayland app backed by a CEF Views top-level window. + +mod webview; +mod window; + +use std::{ + sync::{ + Arc, + atomic::{AtomicI32, Ordering}, + mpsc::{self, Receiver, Sender}, + }, + time::{Duration, Instant}, +}; + +use cef::ImplBrowserHost; +use raw_window_handle::HasDisplayHandle; +use tauri_runtime::{ + DeviceEventFilter, Error, ExitRequestedEventAction, Result, RunEvent, UserEvent, + dpi::PhysicalSize, + window::{PendingWindow, WindowEvent, WindowId}, +}; +use winit::{ + application::ApplicationHandler, + event::StartCause, + event_loop::{ActiveEventLoop, ControlFlow}, +}; + +use crate::{ + cef_impl::request_handler, + runtime::{AfterWindowCreationCallback, CefRuntime, EventLoopMessage, Message, RuntimeContext}, + webview::AppWebview, + window::WindowMessage, + window_handle::SendRawDisplayHandle, +}; + +use window::{WaylandWindow, WindowConfig}; + +const CEF_WORK_INTERVAL: Duration = Duration::from_millis(4); + +pub(crate) struct App { + context: RuntimeContext, + receiver: Receiver>, + native_event_sender: Sender<(WindowId, window::Event)>, + native_event_receiver: Receiver<(WindowId, window::Event)>, + window: Option, + callback: Box)>, + scheme_registry: request_handler::SchemeRegistry, + browser_live: bool, + exiting: bool, + exit_code: Arc, +} + +impl App { + pub(crate) fn new( + context: RuntimeContext, + receiver: Receiver>, + callback: Box)>, + scheme_registry: request_handler::SchemeRegistry, + exit_code: Arc, + ) -> Self { + let (native_event_sender, native_event_receiver) = mpsc::channel(); + Self { + context, + receiver, + native_event_sender, + native_event_receiver, + window: None, + callback, + scheme_registry, + browser_live: false, + exiting: false, + exit_code, + } + } + + fn install_dispatch( + &mut self, + event_loop: &dyn ActiveEventLoop, + ) -> crate::runtime::MainThreadDispatchGuard { + unsafe fn handle( + app: *mut (), + event_loop: &dyn ActiveEventLoop, + message: Message, + ) { + unsafe { &mut *app.cast::>() }.handle_message(event_loop, message); + } + let app = (self as *mut Self).cast(); + self + .context + .install_current_dispatch(app, handle::, event_loop) + } + + fn run_callback(&mut self, event: RunEvent) { + (self.callback)(event); + } + + fn drain_messages(&mut self, event_loop: &dyn ActiveEventLoop) { + while let Ok(message) = self.receiver.try_recv() { + self.handle_message(event_loop, message); + } + while let Ok((window_id, event)) = self.native_event_receiver.try_recv() { + self.handle_window_event(event_loop, window_id, event); + } + } + + fn handle_message(&mut self, event_loop: &dyn ActiveEventLoop, message: Message) { + match message { + Message::EventLoop(message) => self.handle_event_loop_message(event_loop, message), + Message::BrowserClosed(..) => { + let child = self.window.as_mut().and_then(|window| window.child.take()); + if let Some(child) = child { + self.remove_scheme_entries(&child); + } + self.browser_live = false; + self.exit_if_done(event_loop); + } + Message::CreateWindow { + window_id, + webview_id, + pending, + after_window_creation, + result_tx, + } => { + let result = self.create_window( + event_loop, + window_id, + webview_id, + *pending, + after_window_creation, + ); + _ = result_tx.send(result); + } + Message::CreateWebview { + window_id: _, + webview_id: _, + pending: _, + result_tx, + } => { + _ = result_tx.send(Err(Error::CreateWebview( + "native Wayland only supports the webview created with its window".into(), + ))) + } + Message::Window { window_id, message } => { + self.handle_window_message(event_loop, window_id, message) + } + Message::Webview { + window_id, + webview_id: _, + message, + } => { + if !self.exiting + && let Some(window) = self.window.as_mut().filter(|window| window.id == window_id) + && window.child.is_some() + { + webview::handle_message(window_id, window, message); + } + } + Message::DragDropScriptEvent { .. } => {} + Message::Task(task) => task(), + Message::RequestExit(code) => { + if self.request_exit(Some(code)) { + self.exit_code.store(code, Ordering::Release); + match self.window.as_ref().map(|window| window.id) { + Some(window_id) => self.close_window(window_id, event_loop, true), + None => self.exit_if_done(event_loop), + } + } + } + Message::Opened(urls) => log::warn!( + "dropping deep-link open event {urls:?}: published tauri-runtime has no Linux RunEvent::Opened" + ), + Message::UserEvent(event) => self.run_callback(RunEvent::UserEvent(event)), + } + } + + fn create_window( + &mut self, + event_loop: &dyn ActiveEventLoop, + window_id: WindowId, + webview_id: Option, + pending: PendingWindow>, + after_window_creation: Option, + ) -> Result<()> { + if self.window.is_some() || after_window_creation.is_some() { + return Err(Error::CreateWindow); + } + let (Some(webview_id), Some(pending_webview)) = (webview_id, pending.webview) else { + return Err(Error::CreateWindow); + }; + + let attrs = pending.window_builder.attrs.clone(); + let scale = event_loop + .primary_monitor() + .map(|monitor| monitor.scale_factor()) + .unwrap_or(1.0); + let mut size = attrs + .inner + .surface_size + .unwrap_or_else(|| PhysicalSize::new(800, 600).into()) + .to_physical::(scale); + if let Some(min) = attrs.inner.min_surface_size { + let min = min.to_physical::(scale); + size.width = size.width.max(min.width); + size.height = size.height.max(min.height); + } + if let Some(max) = attrs.inner.max_surface_size { + let max = max.to_physical::(scale); + size.width = size.width.min(max.width); + size.height = size.height.min(max.height); + } + + let mut window = WaylandWindow { + id: window_id, + label: pending.label, + attrs, + child: None, + listeners: Default::default(), + native: None, + initial_surface_size: size, + initial_scale_factor: scale, + reported_focus: false, + }; + let sender = self.native_event_sender.clone(); + let proxy = self.context.proxy.clone(); + let config = WindowConfig::new( + &window.attrs, + window.initial_surface_size, + window.initial_scale_factor, + move |event| { + if sender.send((window_id, event)).is_ok() { + proxy.wake_up(); + } + }, + ); + // Theme is always resolved as system: nothing sets `preferred_theme` or + // calls `set_theme` in practice, so this never diverges from ColorVariant::SYSTEM. + let Some((child, native)) = webview::build( + &self.context, + &self.scheme_registry, + window.id, + webview_id, + config, + None, + pending_webview, + ) else { + return Err(Error::CreateWebview( + "failed to create CEF Views browser".into(), + )); + }; + window.native = Some(native); + window.child = Some(child); + self.browser_live = true; + self.window = Some(window); + Ok(()) + } + + fn handle_window_message( + &mut self, + event_loop: &dyn ActiveEventLoop, + window_id: WindowId, + message: WindowMessage, + ) { + match message { + WindowMessage::Close => return self.request_window_close(window_id, event_loop), + WindowMessage::Destroy => return self.close_window(window_id, event_loop, true), + _ => {} + } + if let Some(window) = self.window.as_mut().filter(|window| window.id == window_id) { + window::handle_window_message(window, event_loop, message); + } + } + + fn handle_window_event( + &mut self, + event_loop: &dyn ActiveEventLoop, + window_id: WindowId, + event: window::Event, + ) { + match event { + window::Event::CloseRequested => self.request_window_close(window_id, event_loop), + window::Event::Destroyed => self.close_window(window_id, event_loop, false), + window::Event::Focused(focused) => { + let Some(window) = self.window.as_mut().filter(|window| window.id == window_id) else { + return; + }; + if window.reported_focus == focused { + return; + } + window.reported_focus = focused; + log::debug!("native-wayland focus changed: window={window_id:?} focused={focused}"); + if let Some(child) = &window.child { + child.host.set_focus(i32::from(focused)); + if focused { + webview::take_focus(child); + } + } + self.emit_window_event(window_id, WindowEvent::Focused(focused)); + } + } + } + + fn close_window( + &mut self, + window_id: WindowId, + event_loop: &dyn ActiveEventLoop, + close_native: bool, + ) { + if self + .window + .as_ref() + .is_none_or(|window| window.id != window_id) + { + return; + } + if !self.exiting { + self.emit_window_event(window_id, WindowEvent::Destroyed); + } + let Some(window) = self.window.take() else { + return; + }; + if let Some(child) = &window.child { + self.remove_scheme_entries(child); + child.host.close_browser(1); + } + if close_native { + window.close(); + } + self.exit_if_done(event_loop); + } + + fn request_window_close(&mut self, window_id: WindowId, event_loop: &dyn ActiveEventLoop) { + if self.exiting { + return self.close_window(window_id, event_loop, true); + } + let Some(window) = self.window.as_ref().filter(|window| window.id == window_id) else { + return; + }; + let label = window.label.clone(); + let listeners = window.listeners.clone(); + let (tx, rx) = mpsc::channel(); + for listener in listeners.lock().unwrap().values() { + listener(&WindowEvent::CloseRequested { + signal_tx: tx.clone(), + }); + } + self.run_callback(RunEvent::WindowEvent { + label, + event: WindowEvent::CloseRequested { signal_tx: tx }, + }); + if !matches!(rx.try_recv(), Ok(true)) { + self.close_window(window_id, event_loop, true); + } + } + + fn remove_scheme_entries(&self, child: &AppWebview) { + let mut registry = self.scheme_registry.lock().unwrap(); + for scheme in child.uri_scheme_protocols.keys() { + registry.remove(&(child.browser_id, scheme.clone())); + } + } + + fn emit_window_event(&mut self, window_id: WindowId, event: WindowEvent) { + let Some(window) = self.window.as_ref().filter(|window| window.id == window_id) else { + return; + }; + let label = window.label.clone(); + let listeners = window.listeners.clone(); + self.run_callback(RunEvent::WindowEvent { + label, + event: event.clone(), + }); + for listener in listeners.lock().unwrap().values() { + listener(&event); + } + } + + fn request_exit(&mut self, code: Option) -> bool { + if self.exiting { + return false; + } + let (tx, rx) = mpsc::channel(); + self.run_callback(RunEvent::ExitRequested { code, tx }); + if matches!(rx.try_recv(), Ok(ExitRequestedEventAction::Prevent)) { + false + } else { + self.exiting = true; + true + } + } + + fn exit_if_done(&mut self, event_loop: &dyn ActiveEventLoop) { + if self.browser_live { + return; + } + if self.exiting || (self.window.is_none() && self.request_exit(None)) { + self.run_callback(RunEvent::Exit); + event_loop.exit(); + } + } + + fn handle_event_loop_message( + &mut self, + event_loop: &dyn ActiveEventLoop, + message: EventLoopMessage, + ) { + match message { + EventLoopMessage::SetTheme(_) => {} + EventLoopMessage::SetDeviceEventFilter(filter) => { + event_loop.listen_device_events(match filter { + DeviceEventFilter::Always => winit::event_loop::DeviceEvents::Never, + DeviceEventFilter::Unfocused => winit::event_loop::DeviceEvents::WhenFocused, + DeviceEventFilter::Never => winit::event_loop::DeviceEvents::Always, + }); + } + EventLoopMessage::PrimaryMonitor(tx) => _ = tx.send(None), + EventLoopMessage::MonitorFromPoint(tx, ..) => _ = tx.send(None), + EventLoopMessage::AvailableMonitors(tx) => _ = tx.send(Vec::new()), + EventLoopMessage::CursorPosition(tx) => _ = tx.send(Err(Error::FailedToGetCursorPosition)), + EventLoopMessage::DisplayHandle(tx) => { + _ = tx.send( + event_loop + .display_handle() + .map(|handle| SendRawDisplayHandle(handle.as_raw())), + ) + } + } + } + + fn service_cef(&self, event_loop: &dyn ActiveEventLoop) { + let context = gtk::glib::MainContext::default(); + while context.pending() { + context.iteration(false); + } + cef::do_message_loop_work(); + event_loop.set_control_flow(ControlFlow::WaitUntil(Instant::now() + CEF_WORK_INTERVAL)); + } +} + +impl ApplicationHandler for App { + fn new_events(&mut self, event_loop: &dyn ActiveEventLoop, cause: StartCause) { + let _guard = self.install_dispatch(event_loop); + match cause { + StartCause::Init => { + self.run_callback(RunEvent::Ready); + self.context.cef_pump.do_work(); + } + StartCause::Poll => self.run_callback(RunEvent::Resumed), + _ => {} + } + } + + fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) { + let _guard = self.install_dispatch(event_loop); + self.drain_messages(event_loop); + } + + fn proxy_wake_up(&mut self, event_loop: &dyn ActiveEventLoop) { + let _guard = self.install_dispatch(event_loop); + self.drain_messages(event_loop); + } + + fn window_event( + &mut self, + _event_loop: &dyn ActiveEventLoop, + _window_id: winit::window::WindowId, + _event: winit::event::WindowEvent, + ) { + } + + fn about_to_wait(&mut self, event_loop: &dyn ActiveEventLoop) { + let _guard = self.install_dispatch(event_loop); + self.service_cef(event_loop); + self.run_callback(RunEvent::MainEventsCleared); + } +} diff --git a/src/wayland/webview.rs b/src/wayland/webview.rs new file mode 100644 index 0000000..8262f76 --- /dev/null +++ b/src/wayland/webview.rs @@ -0,0 +1,309 @@ +//! Creates and configures the cef webview, which renders the content +//! Implements tauri operations on the cef webview + +use std::{ + collections::HashMap, + sync::{Arc, Mutex, atomic::Ordering, mpsc}, +}; + +use cef::*; +use tauri_runtime::{ + Error, UserEvent, + dpi::{PhysicalPosition, Rect}, + webview::{PendingWebview, WebviewAttributes}, + window::WindowId, +}; +use tauri_utils::Theme; + +use crate::{ + cef_impl::{client as browser_client, request_context, request_handler}, + compat::UriSchemeProtocolHandler, + runtime::{CefRuntime, RuntimeContext}, + webview::{ + AppWebview, INITIAL_LOAD_URL, PendingInitialLoads, Webview, WebviewMessage, + add_dev_tools_observer, initialization_scripts, + load_initial_url_after_registering_initialization_scripts, + }, +}; + +use super::window::{NativeWindow, WaylandWindow, WindowConfig}; + +fn browser_settings(attrs: &WebviewAttributes) -> BrowserSettings { + BrowserSettings { + javascript: State::from(if attrs.javascript_disabled { + sys::cef_state_t::STATE_DISABLED + } else { + sys::cef_state_t::STATE_ENABLED + }), + javascript_access_clipboard: State::from(if attrs.clipboard { + sys::cef_state_t::STATE_ENABLED + } else { + sys::cef_state_t::STATE_DISABLED + }), + ..Default::default() + } +} + +pub(super) fn build( + context: &RuntimeContext, + scheme_registry: &request_handler::SchemeRegistry, + window_id: WindowId, + webview_id: u32, + config: WindowConfig, + theme: Option, + mut pending: PendingWebview>, +) -> Option<(AppWebview, NativeWindow)> { + pending.webview_attributes.drag_drop_handler_enabled = false; + let scripts = initialization_scripts(&mut pending.webview_attributes); + let uri_scheme_protocols: Arc>>> = Arc::new( + pending + .uri_scheme_protocols + .into_iter() + .map(|(scheme, handler)| (scheme, Arc::new(handler))) + .collect(), + ); + let handlers = browser_client::TauriCefBrowserClientHandlers { + ipc_handler: pending.ipc_handler.map(Arc::from), + on_page_load_handler: pending.on_page_load_handler.take().map(Arc::from), + document_title_changed_handler: pending.document_title_changed_handler.take().map(Arc::from), + navigation_handler: pending.navigation_handler.map(Arc::from), + address_changed_handler: None, + new_window_handler: Some(Arc::new(|_, _| { + tauri_runtime::webview::NewWindowResponse::Deny + })), + download_handler: pending.download_handler.take(), + web_content_process_terminate_handler: None, + }; + let mut client = browser_client::TauriCefBrowserClient::new( + context.clone(), + window_id, + webview_id, + pending.label.clone(), + Some(pending.url.as_str().to_string()), + (cfg!(debug_assertions) || cfg!(feature = "devtools")) + && pending.webview_attributes.devtools.unwrap_or(true), + browser_client::DragDropEventTarget::Window, + false, + Arc::new(Mutex::new(browser_client::DragDropState::default())), + handlers, + context.proxy.clone(), + context.sender.clone(), + ); + let settings = browser_settings(&pending.webview_attributes); + let custom_protocol_scheme = if pending.webview_attributes.use_https_scheme { + "https" + } else { + "http" + } + .to_string(); + let custom_scheme_domains = uri_scheme_protocols + .keys() + .map(|scheme| format!("{scheme}.localhost")) + .collect::>(); + let real_initial_url = pending.url.as_str().to_string(); + let label = pending.label.clone(); + let (browser_tx, browser_rx) = mpsc::channel(); + let (init_done, on_initialized) = request_context::deferred_init_continuation({ + let scheme_registry = scheme_registry.clone(); + let uri_scheme_protocols = uri_scheme_protocols.clone(); + let scripts = scripts.clone(); + let custom_protocol_scheme = custom_protocol_scheme.clone(); + let custom_scheme_domains = custom_scheme_domains.clone(); + move |mut request_context| { + request_context::apply_theme_scheme(request_context.as_ref(), theme); + let callback_label = label.clone(); + let callback_protocols = uri_scheme_protocols.clone(); + let callback_scripts = scripts.clone(); + let callback_registry = scheme_registry.clone(); + let callback_scheme = custom_protocol_scheme.clone(); + let callback_domains = custom_scheme_domains.clone(); + let callback_url = real_initial_url.clone(); + if super::window::create( + &mut client, + &CefString::from(INITIAL_LOAD_URL), + &settings, + request_context.as_mut(), + config, + Box::new(move |browser, native| { + let Some(host) = browser.host() else { + log::error!("CEF browser for webview {callback_label:?} has no host"); + return; + }; + let browser_id = browser.identifier(); + { + let mut registry = callback_registry.lock().unwrap(); + for (scheme, handler) in callback_protocols.iter() { + registry.insert( + (browser_id, scheme.clone()), + ( + callback_label.clone(), + handler.clone(), + callback_scripts.clone(), + ), + ); + } + } + let protocol_handlers = Arc::new(Mutex::new(Vec::new())); + let pending_loads: PendingInitialLoads = Arc::new(Mutex::new(HashMap::new())); + let registration = Arc::new(Mutex::new(add_dev_tools_observer( + &browser, + protocol_handlers.clone(), + pending_loads.clone(), + ))); + load_initial_url_after_registering_initialization_scripts( + &browser, + &callback_scripts, + &callback_scheme, + &callback_domains, + &callback_url, + &pending_loads, + ); + let _ = browser_tx.send(( + AppWebview { + webview_id, + label: callback_label, + browser, + browser_id, + host, + uri_scheme_protocols: callback_protocols, + devtools_protocol_handlers: protocol_handlers, + devtools_observer_registration: registration, + listeners: Default::default(), + bounds_rate: None, + }, + native, + )); + }), + ) + .is_none() + { + log::error!("failed to create CEF Views window for webview {label:?}"); + } + } + }); + let request_context = request_context::request_context_from_webview_attributes( + &context.cache_path, + &pending.webview_attributes, + uri_scheme_protocols.keys(), + &custom_protocol_scheme, + scheme_registry.clone(), + on_initialized, + ); + if request_context.is_none() { + init_done.store(true, Ordering::SeqCst); + } + request_context::wait_for_deferred_init(&init_done); + wait_for_result(&browser_rx) +} + +fn wait_for_result(receiver: &mpsc::Receiver) -> Option { + if cef::currently_on(sys::cef_thread_id_t::TID_UI.into()) == 0 { + return receiver.recv().ok(); + } + let _allow = request_context::AllowNestableTasks::enter(); + loop { + match receiver.try_recv() { + Ok(value) => return Some(value), + Err(mpsc::TryRecvError::Disconnected) => return None, + Err(mpsc::TryRecvError::Empty) => cef::do_message_loop_work(), + } + } +} + +fn browser_view(child: &AppWebview) -> Option { + browser_view_get_for_browser(Some(&mut child.browser.clone())) +} + +pub(super) fn take_focus(child: &AppWebview) { + if let Some(view) = browser_view(child) { + view.request_focus(); + } +} + +pub(super) fn handle_message( + window_id: WindowId, + window: &mut WaylandWindow, + message: WebviewMessage, +) { + let size = window.initial_surface_size; + let Some(child) = window.child.as_mut() else { + return; + }; + match message { + WebviewMessage::AddEventListener(..) => {} + WebviewMessage::EvaluateScript(script) => { + if let Some(frame) = child.browser.main_frame() { + frame.execute_java_script(Some(&script.as_str().into()), Some(&"".into()), 0); + } + } + WebviewMessage::EvaluateScriptWithCallback(..) => { + log::error!("eval_with_callback is unimplemented for native Wayland; dropping callback"); + } + WebviewMessage::Navigate(url) => { + if let Some(frame) = child.browser.main_frame() { + frame.load_url(Some(&url.as_str().into())); + } + } + WebviewMessage::Reload => child.browser.reload(), + WebviewMessage::GoBack => child.browser.go_back(), + WebviewMessage::CanGoBack(tx) => _ = tx.send(Ok(child.browser.can_go_back() == 1)), + WebviewMessage::GoForward => child.browser.go_forward(), + WebviewMessage::CanGoForward(tx) => _ = tx.send(Ok(child.browser.can_go_forward() == 1)), + WebviewMessage::Print => child.host.print(), + WebviewMessage::Close => child.host.close_browser(1), + WebviewMessage::Show => { + if let Some(view) = browser_view(child) { + view.set_visible(1); + } + } + WebviewMessage::Hide => { + if let Some(view) = browser_view(child) { + view.set_visible(0); + } + } + WebviewMessage::SetPosition(_) | WebviewMessage::SetSize(_) | WebviewMessage::SetBounds(_) => {} + WebviewMessage::SetFocus => { + child.host.set_focus(1); + take_focus(child); + } + WebviewMessage::Reparent(target, tx) => { + _ = tx.send(if target == window_id { + Ok(()) + } else { + Err(Error::WindowNotFound) + }); + } + WebviewMessage::SetAutoResize(_) | WebviewMessage::ClearAllBrowsingData => {} + WebviewMessage::SetZoom(factor) => { + child.host.set_zoom_level(if factor > 0.0 { + factor.ln() / 1.2_f64.ln() + } else { + 0.0 + }); + } + WebviewMessage::SetBackgroundColor(_) => {} + WebviewMessage::Url(tx) => _ = tx.send(Ok(child.url().unwrap_or_default())), + WebviewMessage::Bounds(tx) => { + _ = tx.send(Ok(Rect { + position: PhysicalPosition::new(0, 0).into(), + size: size.into(), + })) + } + WebviewMessage::Position(tx) => _ = tx.send(Ok(PhysicalPosition::new(0, 0))), + WebviewMessage::Size(tx) => _ = tx.send(Ok(size)), + WebviewMessage::WithWebview(callback) => callback(Webview::new(child.browser.clone())), + WebviewMessage::CookiesForUrl(_, tx) | WebviewMessage::Cookies(tx) => { + _ = tx.send(Ok(Vec::new())) + } + WebviewMessage::SetCookie(_) | WebviewMessage::DeleteCookie(_) => {} + #[cfg(any(debug_assertions, feature = "devtools"))] + WebviewMessage::OpenDevTools => child.host.show_dev_tools(None, None, None, None), + #[cfg(any(debug_assertions, feature = "devtools"))] + WebviewMessage::CloseDevTools => child.host.close_dev_tools(), + #[cfg(any(debug_assertions, feature = "devtools"))] + WebviewMessage::IsDevToolsOpen(tx) => _ = tx.send(child.host.has_dev_tools() == 1), + WebviewMessage::SendDevToolsMessage(_, tx) | WebviewMessage::OnDevToolsProtocol(_, tx) => { + _ = tx.send(Err(Error::FailedToSendMessage)) + } + } +} diff --git a/src/wayland/window.rs b/src/wayland/window.rs new file mode 100644 index 0000000..0ab30bc --- /dev/null +++ b/src/wayland/window.rs @@ -0,0 +1,512 @@ +//! This wires up the window created by cef to tauri +//! For context: In the X11 linux version, winit's top level window is wired to tauri + +use std::{ + collections::HashMap, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, + }, +}; + +use cef::{rc::Rc, *}; +use tauri_runtime::{ + Error, WindowEventId, + dpi::{PhysicalPosition, PhysicalSize, Size as TauriSize}, + window::{WindowEvent, WindowId}, +}; +use winit::event_loop::ActiveEventLoop; + +use crate::{ + webview::AppWebview, + window::{AppWindowAttrs, WindowMessage, winit_theme_to_tauri_theme}, +}; + +type BrowserCreated = Box; +type Emit = Arc; + +#[derive(Debug)] +pub(super) enum Event { + CloseRequested, + Destroyed, + Focused(bool), +} + +#[derive(Clone)] +pub(super) struct WindowConfig { + title: String, + bounds: Rect, + show_state: ShowState, + visible: bool, + frameless: bool, + initial_scale_factor: f64, + app_id: String, + emit: Emit, + resizable: bool, + maximizable: bool, + minimizable: bool, + closable: bool, + min_size: Option, +} + +impl WindowConfig { + pub(super) fn new( + attrs: &AppWindowAttrs, + size: PhysicalSize, + scale_factor: f64, + emit: impl Fn(Event) + Send + Sync + 'static, + ) -> Self { + let buttons = attrs.inner.enabled_buttons; + let show_state = if attrs.inner.fullscreen.is_some() { + ShowState::FULLSCREEN + } else if attrs.inner.maximized { + ShowState::MAXIMIZED + } else { + ShowState::NORMAL + }; + + Self { + title: attrs.inner.title.clone(), + bounds: Rect { + x: 0, + y: 0, + width: cef_dimension(size.width, scale_factor), + height: cef_dimension(size.height, scale_factor), + }, + show_state, + visible: attrs.inner.visible, + frameless: !attrs.inner.decorations, + initial_scale_factor: scale_factor, + app_id: crate::config::config().identifier.clone(), + emit: Arc::new(emit), + resizable: attrs.inner.resizable, + maximizable: buttons.contains(winit::window::WindowButtons::MAXIMIZE), + minimizable: buttons.contains(winit::window::WindowButtons::MINIMIZE), + closable: buttons.contains(winit::window::WindowButtons::CLOSE), + min_size: attrs.inner.min_surface_size, + } + } +} + +pub(crate) struct NativeWindow { + window: Window, + browser_view: BrowserView, + allow_close: Arc, +} + +impl NativeWindow { + pub(super) fn force_close(&self) { + self.allow_close.store(true, Ordering::Release); + self.window.close(); + } +} + +type WindowEventListener = Box; + +pub(super) struct WaylandWindow { + pub(super) id: WindowId, + pub(super) label: String, + pub(super) attrs: AppWindowAttrs, + pub(super) child: Option, + pub(super) listeners: Arc>>, + pub(super) native: Option, + pub(super) initial_surface_size: PhysicalSize, + pub(super) initial_scale_factor: f64, + pub(super) reported_focus: bool, +} + +impl WaylandWindow { + pub(super) fn close(&self) { + if let Some(native) = &self.native { + native.force_close(); + } + } +} + +pub(crate) fn handle_window_message( + appwindow: &mut WaylandWindow, + event_loop: &dyn ActiveEventLoop, + message: WindowMessage, +) { + let native = appwindow.native.as_ref(); + let buttons = appwindow.attrs.inner.enabled_buttons; + + match message { + WindowMessage::AddEventListener(id, listener) => { + appwindow.listeners.lock().unwrap().insert(id, listener); + } + WindowMessage::Close | WindowMessage::Destroy => { + unreachable!("handled before borrowing") + } + WindowMessage::ScaleFactor(tx) => _ = tx.send(Ok(appwindow.initial_scale_factor)), + WindowMessage::InnerPosition(tx) | WindowMessage::OuterPosition(tx) => { + _ = tx.send(Ok(PhysicalPosition::new(0, 0))) + } + WindowMessage::InnerSize(tx) | WindowMessage::OuterSize(tx) => { + _ = tx.send(Ok(appwindow.initial_surface_size)) + } + WindowMessage::IsFullscreen(tx) => { + _ = tx.send(Ok( + native + .map(|native| native.window.is_fullscreen() != 0) + .unwrap_or(appwindow.attrs.inner.fullscreen.is_some()), + )) + } + WindowMessage::IsMinimized(tx) => { + _ = tx.send(Ok( + native.is_some_and(|native| native.window.is_minimized() != 0), + )) + } + WindowMessage::IsMaximized(tx) => { + _ = tx.send(Ok( + native + .map(|native| native.window.is_maximized() != 0) + .unwrap_or(appwindow.attrs.inner.maximized), + )) + } + WindowMessage::IsFocused(tx) => { + _ = tx.send(Ok( + native.is_some_and(|native| native.window.is_active() != 0), + )) + } + WindowMessage::IsDecorated(tx) => _ = tx.send(Ok(appwindow.attrs.inner.decorations)), + WindowMessage::IsResizable(tx) => _ = tx.send(Ok(appwindow.attrs.inner.resizable)), + WindowMessage::IsMaximizable(tx) => { + _ = tx.send(Ok(buttons.contains(winit::window::WindowButtons::MAXIMIZE))) + } + WindowMessage::IsMinimizable(tx) => { + _ = tx.send(Ok(buttons.contains(winit::window::WindowButtons::MINIMIZE))) + } + WindowMessage::IsClosable(tx) => { + _ = tx.send(Ok(buttons.contains(winit::window::WindowButtons::CLOSE))) + } + WindowMessage::IsVisible(tx) => { + _ = tx.send(Ok( + native + .map(|native| native.window.is_visible() != 0) + .unwrap_or(appwindow.attrs.inner.visible), + )) + } + WindowMessage::IsEnabled(tx) => _ = tx.send(Ok(true)), + WindowMessage::IsAlwaysOnTop(tx) => _ = tx.send(Ok(false)), + WindowMessage::Title(tx) => _ = tx.send(Ok(appwindow.attrs.inner.title.clone())), + // Monitor queries are unimplemented for native Wayland: no consumer calls + // them, and there's no Wayland protocol for the work_area they'd need. + WindowMessage::CurrentMonitor(tx) + | WindowMessage::PrimaryMonitor(tx) + | WindowMessage::MonitorFromPoint(tx, ..) => _ = tx.send(Ok(None)), + WindowMessage::AvailableMonitors(tx) => _ = tx.send(Ok(Vec::new())), + WindowMessage::RawWindowHandle(tx) => _ = tx.send(Err(Error::FailedToSendMessage)), + WindowMessage::Theme(tx) => { + let theme = appwindow + .attrs + .inner + .preferred_theme + .or_else(|| event_loop.system_theme()) + .map(winit_theme_to_tauri_theme) + .unwrap_or(tauri_utils::Theme::Light); + _ = tx.send(Ok(theme)); + } + WindowMessage::SetTitle(title) => { + appwindow.attrs.inner.title = title.clone(); + if let Some(native) = native { + native + .window + .set_title(Some(&CefString::from(title.as_str()))); + } + } + WindowMessage::Maximize => { + appwindow.attrs.inner.maximized = true; + if let Some(native) = native { + native.window.maximize(); + } + } + WindowMessage::Unmaximize => { + appwindow.attrs.inner.maximized = false; + if let Some(native) = native { + native.window.restore(); + } + } + WindowMessage::Minimize => { + if let Some(native) = native { + native.window.minimize(); + } + } + WindowMessage::Unminimize => { + if let Some(native) = native { + native.window.restore(); + } + } + WindowMessage::Show => { + appwindow.attrs.inner.visible = true; + if let Some(native) = native { + native.window.show(); + } + } + WindowMessage::Hide => { + appwindow.attrs.inner.visible = false; + if let Some(native) = native { + native.window.hide(); + } + } + WindowMessage::SetFullscreen(fullscreen) => { + appwindow.attrs.inner.fullscreen = + fullscreen.then_some(winit::monitor::Fullscreen::Borderless(None)); + if let Some(native) = native { + native.window.set_fullscreen(i32::from(fullscreen)); + } + } + WindowMessage::SetFocus => { + if let Some(native) = native { + native.window.activate(); + native.browser_view.request_focus(); + } + } + WindowMessage::Center + | WindowMessage::RequestUserAttention(_) + | WindowMessage::SetEnabled(_) + | WindowMessage::SetResizable(_) + | WindowMessage::SetMaximizable(_) + | WindowMessage::SetMinimizable(_) + | WindowMessage::SetClosable(_) + | WindowMessage::SetDecorations(_) + | WindowMessage::SetAlwaysOnBottom(_) + | WindowMessage::SetAlwaysOnTop(_) + | WindowMessage::SetVisibleOnAllWorkspaces(_) + | WindowMessage::SetContentProtected(_) + | WindowMessage::SetMinSize(_) + | WindowMessage::SetMaxSize(_) + | WindowMessage::SetSizeConstraints(_) + | WindowMessage::SetFocusable(_) + | WindowMessage::SetIcon(_) + | WindowMessage::SetSkipTaskbar(_) + | WindowMessage::SetShadow(_) + | WindowMessage::SetCursorGrab(_) + | WindowMessage::SetCursorVisible(_) + | WindowMessage::SetCursorIcon(_) + | WindowMessage::SetCursorPosition(_) + | WindowMessage::SetIgnoreCursorEvents(_) + | WindowMessage::SetBadgeCount(..) + | WindowMessage::SetBadgeLabel(_) + | WindowMessage::SetOverlayIcon(_) + | WindowMessage::SetTitleBarStyle(_) + | WindowMessage::SetTrafficLightPosition(_) + | WindowMessage::SetProgressBar(_) + | WindowMessage::SetTheme(_) => {} + // ponytail: no draggable-region support on the CEF Views backend, so a frameless + // window cannot be moved or edge-resized from the page. Only reachable when + // `decorations(false)` is set at build time. Upgrade path: implement + // `DragHandler::on_draggable_regions_changed` -> `Window::set_draggable_regions` + // and inject the `-webkit-app-region` stylesheet (see `src/native_wayland/drag.rs` + // in commit ddd270e). + // ponytail: geometry is frozen at creation. Getters answer from + // `initial_surface_size` / `PhysicalPosition(0, 0)` and the setters do nothing, so + // `tauri-plugin-window-state` will persist the creation size at 0,0. Upgrade path + // for size: `native.window.size()` / `set_bounds()` (already in scope, see + // `IsFullscreen` above). Position is a genuine Wayland protocol limit -- there are + // no global window coordinates -- so it stays stubbed. + WindowMessage::SetSize(_) | WindowMessage::SetPosition(_) => { + log::warn!( + "set_size/set_position are unsupported on native Wayland; window geometry is \ + fixed at the creation size and any persisted window state will be inaccurate" + ); + } + WindowMessage::StartDragging | WindowMessage::StartResizeDragging(_) => { + log::warn!( + "window drag is unsupported on native Wayland (decorations={}); \ + the CEF Views backend does not implement draggable regions", + appwindow.attrs.inner.decorations + ); + } + WindowMessage::SetBackgroundColor(_) => {} + } +} + +fn cef_dimension(value: u32, scale: f64) -> i32 { + (value as f64 / scale).round().max(1.0) as i32 +} + +fn cef_size_from_tauri(size: TauriSize, scale: f64) -> Size { + let size = size.to_physical::(scale); + Size { + width: cef_dimension(size.width, scale), + height: cef_dimension(size.height, scale), + } +} + +wrap_browser_view_delegate! { + struct NativeBrowserViewDelegate { + on_created: Arc>>, + allow_close: Arc, + } + + impl ViewDelegate {} + + impl BrowserViewDelegate { + fn browser_runtime_style(&self) -> RuntimeStyle { + RuntimeStyle::CHROME + } + + fn on_browser_created( + &self, + browser_view: Option<&mut BrowserView>, + browser: Option<&mut Browser>, + ) { + let (Some(browser_view), Some(browser)) = (browser_view, browser) else { + return; + }; + if let Some(on_created) = self.on_created.lock().unwrap().take() { + let Some(window) = browser_view.window() else { + log::error!("native Wayland browser view has no CEF window"); + return; + }; + on_created( + browser.clone(), + NativeWindow { + window, + browser_view: browser_view.clone(), + allow_close: self.allow_close.clone(), + }, + ); + } + } + + fn on_popup_browser_view_created( + &self, + _browser_view: Option<&mut BrowserView>, + popup_browser_view: Option<&mut BrowserView>, + _is_devtools: i32, + ) -> i32 { + // Sable has one top-level window. A popup policy can redirect navigation; + // an allowed native popup is closed instead of creating a second window. + if let Some(browser) = popup_browser_view.and_then(|view| view.browser()) + && let Some(host) = browser.host() + { + host.close_browser(1); + } + 1 + } + } +} + +wrap_window_delegate! { + struct NativeWindowDelegate { + browser_view: BrowserView, + config: WindowConfig, + allow_close: Arc, + } + + impl ViewDelegate { + fn preferred_size(&self, _view: Option<&mut View>) -> Size { + Size { + width: self.config.bounds.width, + height: self.config.bounds.height, + } + } + + fn minimum_size(&self, _view: Option<&mut View>) -> Size { + self + .config + .min_size + .map(|size| cef_size_from_tauri(size, self.config.initial_scale_factor)) + .unwrap_or_default() + } + } + + impl PanelDelegate {} + + impl WindowDelegate { + fn on_window_created(&self, window: Option<&mut Window>) { + let Some(window) = window else { return }; + window.set_to_fill_layout(); + let mut view = View::from(&self.browser_view); + window.add_child_view(Some(&mut view)); + window.set_title(Some(&CefString::from(self.config.title.as_str()))); + if self.config.visible { + window.show(); + } + } + + fn on_window_destroyed(&self, _window: Option<&mut Window>) { + (self.config.emit)(Event::Destroyed); + } + + fn on_window_activation_changed(&self, _window: Option<&mut Window>, active: i32) { + (self.config.emit)(Event::Focused(active != 0)); + } + + fn initial_bounds(&self, _window: Option<&mut Window>) -> Rect { + self.config.bounds.clone() + } + + fn initial_show_state(&self, _window: Option<&mut Window>) -> ShowState { + self.config.show_state + } + + fn is_frameless(&self, _window: Option<&mut Window>) -> i32 { + i32::from(self.config.frameless) + } + + fn can_resize(&self, _window: Option<&mut Window>) -> i32 { + i32::from(self.config.resizable) + } + + fn can_maximize(&self, _window: Option<&mut Window>) -> i32 { + i32::from(self.config.maximizable) + } + + fn can_minimize(&self, _window: Option<&mut Window>) -> i32 { + i32::from(self.config.minimizable) + } + + fn can_close(&self, _window: Option<&mut Window>) -> i32 { + if self.allow_close.load(Ordering::Acquire) { + 1 + } else if !self.config.closable { + 0 + } else { + (self.config.emit)(Event::CloseRequested); + 0 + } + } + + fn window_runtime_style(&self) -> RuntimeStyle { + RuntimeStyle::CHROME + } + + fn linux_window_properties( + &self, + _window: Option<&mut Window>, + properties: Option<&mut LinuxWindowProperties>, + ) -> i32 { + let Some(properties) = properties else { return 0 }; + properties.wayland_app_id = CefString::from(self.config.app_id.as_str()); + 1 + } + } +} + +pub(crate) fn create( + client: &mut Client, + url: &CefString, + settings: &BrowserSettings, + request_context: Option<&mut RequestContext>, + config: WindowConfig, + on_created: BrowserCreated, +) -> Option<()> { + let allow_close = Arc::new(AtomicBool::new(false)); + let mut browser_delegate = + NativeBrowserViewDelegate::new(Arc::new(Mutex::new(Some(on_created))), allow_close.clone()); + let browser_view = browser_view_create( + Some(client), + Some(url), + Some(settings), + None, + request_context, + Some(&mut browser_delegate), + )?; + let mut window_delegate = + NativeWindowDelegate::new(browser_view.clone(), config, allow_close.clone()); + window_create_top_level(Some(&mut window_delegate))?; + Some(()) +}