From 7028e466c3ca36a1963cc18a26f3d14432314778 Mon Sep 17 00:00:00 2001 From: lunar-seal Date: Tue, 25 Aug 2026 11:27:57 +0200 Subject: [PATCH 1/8] feat(linux): add native Wayland CEF Views path Co-Authored-By: GPT-5 Codex --- README.md | 17 +- src/cef_impl/request_context.rs | 19 ++ src/config.rs | 17 ++ src/lib.rs | 4 +- src/native_wayland.rs | 493 ++++++++++++++++++++++++++++++++ src/platform/linux/utils.rs | 8 +- src/platform/linux/webview.rs | 50 +++- src/runtime.rs | 88 +++++- src/webview.rs | 173 +++++++---- src/window.rs | 42 ++- 10 files changed, 846 insertions(+), 65 deletions(-) create mode 100644 src/native_wayland.rs diff --git a/README.md b/README.md index 372d453..1b6214c 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,21 @@ fn main() { Because published tauri only defaults its generic types (`AppHandle`, `WebviewWindow`, …) to wry, apps alias them once (`type AppHandle = tauri::AppHandle;`) and build tauri with `default-features = false`. +On Linux, native Wayland can be selected before startup: + +```rust +tauri_runtime_cef::configure(tauri_runtime_cef::CefConfig { + linux_windowing: tauri_runtime_cef::LinuxWindowing::Wayland, + ..Default::default() +}); +``` + +This is a parallel CEF Views path: CEF owns the single visible top-level and its +browser view. X11 remains the default and compiled fallback, but the runtime does +not open an X display connection in Wayland mode. Native Wayland currently supports +one window with one full-window webview; Linux raw window handles and runtime +decoration/constraint changes are unavailable in this mode. + ## Additions Capabilities this crate adds on top of the imported runtime: @@ -94,7 +109,7 @@ How building against published tauri changes the mechanics, relative to the feat ## Known ceilings -- Verified on Linux (X11). The macOS/Windows paths compile-ported blind — they need a real pass. +- Verified on Linux (X11 and native Wayland). The macOS/Windows paths compile-ported blind — they need a real pass. - macOS `.app` bundling needs the CEF framework + helper-app layout that feat/cef's tauri-cli produces; the published CLI doesn't do this. Bundle scripting lives with the consuming app for now. - Deep-link relaunch URLs are dropped on Linux/Windows (published `tauri-runtime` has no `RunEvent::Opened` there). diff --git a/src/cef_impl/request_context.rs b/src/cef_impl/request_context.rs index af705ff..3caa2f9 100644 --- a/src/cef_impl/request_context.rs +++ b/src/cef_impl/request_context.rs @@ -8,6 +8,7 @@ use std::{ sync::{ Arc, Mutex, atomic::{AtomicBool, Ordering}, + mpsc::{Receiver, TryRecvError}, }, time::Duration, }; @@ -171,6 +172,24 @@ pub(crate) fn wait_for_deferred_init(flag: &Arc) { } } +/// Wait for an asynchronously-created CEF object while continuing to service +/// the CEF UI thread. Views creates its browser only after the BrowserView is +/// attached to a Window, unlike `browser_host_create_browser_sync`. +pub(crate) fn wait_for_deferred_result(receiver: &Receiver) -> Option { + if cef::currently_on(cef::sys::cef_thread_id_t::TID_UI.into()) == 0 { + return receiver.recv().ok(); + } + + let _allow = AllowNestableTasks::enter(); + loop { + match receiver.try_recv() { + Ok(value) => return Some(value), + Err(TryRecvError::Disconnected) => return None, + Err(TryRecvError::Empty) => cef::do_message_loop_work(), + } + } +} + /// RAII guard that scopes `CefSetNestableTasksAllowed(true)` for the current /// CEF UI-thread call. /// 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..c1db4d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,8 @@ mod cef_impl; mod compat; mod config; mod external_message_pump; +#[cfg(target_os = "linux")] +mod native_wayland; mod platform; mod policy; mod runtime; @@ -18,7 +20,7 @@ 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/native_wayland.rs b/src/native_wayland.rs new file mode 100644 index 0000000..64846e2 --- /dev/null +++ b/src/native_wayland.rs @@ -0,0 +1,493 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Native Wayland top-level window backed by CEF Views. +//! +//! CEF cannot embed a native browser child into a foreign Wayland window. The +//! supported native path is the one used by `cefsimple`: CEF owns the +//! `CefWindow`, with a `CefBrowserView` filling it. + +use std::sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, +}; + +use cef::{rc::Rc, *}; +use tauri_runtime::{ + Error, UserEvent, + dpi::{PhysicalPosition, PhysicalSize, Size as TauriSize}, + window::WindowId, +}; +use winit::window::WindowLevel; + +use crate::{ + runtime::{Message, RuntimeContext}, + window::{AppWindow, AppWindowAttrs, WindowMessage}, +}; + +type BrowserCreated = Box; +type Emit = Arc; + +#[derive(Debug)] +pub(crate) enum Event { + CloseRequested, + Destroyed, + Focused(bool), + Resized(PhysicalSize), + ScaleFactorChanged { + scale_factor: f64, + new_inner_size: PhysicalSize, + }, +} + +#[derive(Clone, Copy)] +struct Geometry { + scale_factor: f64, + inner_size: PhysicalSize, +} + +#[derive(Clone)] +pub(crate) struct WindowConfig { + title: String, + bounds: Rect, + show_state: ShowState, + visible: bool, + frameless: bool, + resizable: bool, + maximizable: bool, + minimizable: bool, + closable: bool, + app_id: String, + emit: Emit, +} + +impl WindowConfig { + pub(crate) fn new( + context: &RuntimeContext, + window_id: WindowId, + attrs: &AppWindowAttrs, + size: PhysicalSize, + scale_factor: f64, + ) -> Self { + let sender = context.sender.clone(); + let proxy = context.proxy.clone(); + let emit = Arc::new(move |event| { + if sender + .send(Message::NativeWaylandWindow(window_id, event)) + .is_ok() + { + proxy.wake_up(); + } + }); + 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: (size.width as f64 / scale_factor).round().max(1.0) as i32, + height: (size.height as f64 / scale_factor).round().max(1.0) as i32, + }, + show_state, + visible: attrs.inner.visible, + frameless: !attrs.inner.decorations, + 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), + app_id: crate::config::config().identifier.clone(), + emit, + } + } +} + +#[derive(Clone)] +pub(crate) struct NativeWindow { + pub(crate) window: Window, + pub(crate) browser_view: BrowserView, + allow_close: Arc, +} + +impl NativeWindow { + pub(crate) fn force_close(&self) { + self.allow_close.store(true, Ordering::Release); + self.window.close(); + } + + pub(crate) fn scale_factor(&self) -> f64 { + scale_factor(&self.window) + } + + pub(crate) fn physical_bounds(&self) -> (PhysicalPosition, PhysicalSize) { + physical_bounds(self.window.bounds_in_screen()) + } + + pub(crate) fn physical_inner_size(&self) -> PhysicalSize { + physical_bounds(self.window.client_area_bounds_in_screen()).1 + } +} + +pub(crate) fn handle_window_message( + appwindow: &mut AppWindow, + message: WindowMessage, +) -> Option { + let native = appwindow + .children + .first() + .and_then(|child| child.native_wayland.clone()); + let Some(native) = native else { + return Some(message); + }; + let window = &native.window; + let scale = native.scale_factor(); + let (outer_position, outer_size) = native.physical_bounds(); + let buttons = appwindow.attrs.inner.enabled_buttons; + + match message { + WindowMessage::ScaleFactor(tx) => _ = tx.send(Ok(scale)), + WindowMessage::InnerPosition(tx) | WindowMessage::OuterPosition(tx) => { + _ = tx.send(Ok(outer_position)) + } + WindowMessage::InnerSize(tx) => _ = tx.send(Ok(native.physical_inner_size())), + WindowMessage::OuterSize(tx) => _ = tx.send(Ok(outer_size)), + WindowMessage::IsFullscreen(tx) => _ = tx.send(Ok(window.is_fullscreen() != 0)), + WindowMessage::IsMinimized(tx) => _ = tx.send(Ok(window.is_minimized() != 0)), + WindowMessage::IsMaximized(tx) => _ = tx.send(Ok(window.is_maximized() != 0)), + WindowMessage::IsFocused(tx) => _ = tx.send(Ok(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(window.is_visible() != 0)), + WindowMessage::IsEnabled(tx) => _ = tx.send(Ok(window.is_enabled() != 0)), + WindowMessage::IsAlwaysOnTop(tx) => _ = tx.send(Ok(window.is_always_on_top() != 0)), + WindowMessage::Title(tx) => { + let title = window.title(); + _ = tx.send(Ok(CefString::from(&title).to_string())) + } + WindowMessage::RawWindowHandle(tx) => _ = tx.send(Err(Error::FailedToSendMessage)), + WindowMessage::Center => window.center_window(Some(&cef_size(outer_size, scale))), + WindowMessage::RequestUserAttention(_) => {} + WindowMessage::SetEnabled(enabled) => window.set_enabled(i32::from(enabled)), + WindowMessage::SetResizable(resizable) => appwindow.attrs.inner.resizable = resizable, + WindowMessage::SetMaximizable(enabled) => appwindow + .attrs + .inner + .enabled_buttons + .set(winit::window::WindowButtons::MAXIMIZE, enabled), + WindowMessage::SetMinimizable(enabled) => appwindow + .attrs + .inner + .enabled_buttons + .set(winit::window::WindowButtons::MINIMIZE, enabled), + WindowMessage::SetClosable(enabled) => appwindow + .attrs + .inner + .enabled_buttons + .set(winit::window::WindowButtons::CLOSE, enabled), + WindowMessage::SetTitle(title) => { + appwindow.attrs.inner.title = title.clone(); + window.set_title(Some(&CefString::from(title.as_str()))); + } + WindowMessage::Maximize => window.maximize(), + WindowMessage::Unmaximize | WindowMessage::Unminimize => window.restore(), + WindowMessage::Minimize => window.minimize(), + WindowMessage::Show => window.show(), + WindowMessage::Hide => window.hide(), + WindowMessage::SetDecorations(decorations) => appwindow.attrs.inner.decorations = decorations, + WindowMessage::SetAlwaysOnBottom(_) => {} + WindowMessage::SetAlwaysOnTop(on_top) => { + appwindow.attrs.inner.window_level = if on_top { + WindowLevel::AlwaysOnTop + } else { + WindowLevel::Normal + }; + window.set_always_on_top(i32::from(on_top)); + } + WindowMessage::SetVisibleOnAllWorkspaces(_) | WindowMessage::SetContentProtected(_) => {} + WindowMessage::SetSize(size) => window.set_size(Some(&cef_size_from_tauri(size, scale))), + WindowMessage::SetMinSize(size) => { + appwindow.attrs.inner.min_surface_size = size; + } + WindowMessage::SetMaxSize(size) => { + appwindow.attrs.inner.max_surface_size = size; + } + WindowMessage::SetSizeConstraints(constraints) => { + appwindow.attrs.inner.min_surface_size = + crate::window::paired_size_constraint(constraints.min_width, constraints.min_height); + appwindow.attrs.inner.max_surface_size = + crate::window::paired_size_constraint(constraints.max_width, constraints.max_height); + } + WindowMessage::SetPosition(_position) => {} + WindowMessage::SetFullscreen(fullscreen) => window.set_fullscreen(i32::from(fullscreen)), + WindowMessage::SetFocus => { + window.activate(); + native.browser_view.request_focus(); + } + WindowMessage::SetFocusable(focusable) => { + native.browser_view.set_focusable(i32::from(focusable)); + } + 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::StartDragging + | WindowMessage::StartResizeDragging(_) + | WindowMessage::SetProgressBar(_) => {} + WindowMessage::SetBackgroundColor(color) => { + appwindow.attrs.background_color = color; + if let Some(child) = appwindow.children.first() { + child.set_background_color(color); + } + } + message => return Some(message), + } + None +} + +fn cef_size(size: PhysicalSize, scale: f64) -> Size { + Size { + width: (size.width as f64 / scale).round().max(1.0) as i32, + height: (size.height as f64 / scale).round().max(1.0) as i32, + } +} + +fn cef_size_from_tauri(size: TauriSize, scale: f64) -> Size { + cef_size(size.to_physical::(scale), 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; + }; + // Chrome-style Views creates its internal WebView after applying defaults. + // Reapply the color now so CEF also uses it behind clipped resize frames. + browser_view.set_background_color(browser_view.background_color()); + 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, + geometry: Arc>>, + } + + impl ViewDelegate { + fn preferred_size(&self, _view: Option<&mut View>) -> Size { + Size { + width: self.config.bounds.width, + height: self.config.bounds.height, + } + } + } + + 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 on_window_bounds_changed(&self, window: Option<&mut Window>, _bounds: Option<&Rect>) { + let Some(window) = window else { return }; + let geometry = Geometry { + scale_factor: scale_factor(window), + inner_size: physical_bounds(window.client_area_bounds_in_screen()).1, + }; + let previous = self.geometry.lock().unwrap().replace(geometry); + + if previous.is_some_and(|previous| previous.scale_factor != geometry.scale_factor) { + (self.config.emit)(Event::ScaleFactorChanged { + scale_factor: geometry.scale_factor, + new_inner_size: geometry.inner_size, + }); + } + if previous.is_none_or(|previous| previous.inner_size != geometry.inner_size) { + (self.config.emit)(Event::Resized(geometry.inner_size)); + } + } + + 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(), + Arc::new(Mutex::new(None)), + ); + window_create_top_level(Some(&mut window_delegate))?; + Some(()) +} + +fn physical_bounds(bounds: Rect) -> (PhysicalPosition, PhysicalSize) { + let bounds = display_convert_screen_rect_to_pixels(Some(&bounds)); + ( + PhysicalPosition::new(bounds.x, bounds.y), + PhysicalSize::new(bounds.width.max(0) as u32, bounds.height.max(0) as u32), + ) +} + +fn scale_factor(window: &Window) -> f64 { + window + .display() + .map(|display| display.device_scale_factor() as f64) + .unwrap_or(1.0) +} diff --git a/src/platform/linux/utils.rs b/src/platform/linux/utils.rs index 74e77ca..3c69cd1 100644 --- a/src/platform/linux/utils.rs +++ b/src/platform/linux/utils.rs @@ -16,7 +16,13 @@ const CLIENT_MESSAGE: i32 = 33; const SUBSTRUCTURE_REDIRECT_MASK: c_long = 1 << 20; const SUBSTRUCTURE_NOTIFY_MASK: c_long = 1 << 19; -static XLIB: LazyLock> = LazyLock::new(|| xlib::Xlib::open().ok()); +static XLIB: LazyLock> = LazyLock::new(|| { + #[cfg(target_os = "linux")] + if crate::config::native_wayland() { + return None; + } + xlib::Xlib::open().ok() +}); struct Display(*mut xlib::Display); diff --git a/src/platform/linux/webview.rs b/src/platform/linux/webview.rs index f63f6de..565443b 100644 --- a/src/platform/linux/webview.rs +++ b/src/platform/linux/webview.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use cef::ImplBrowserHost; +use cef::{ImplBrowserHost, ImplView}; use std::os::raw::c_ulong; use tauri_runtime::dpi::{PhysicalPosition, PhysicalSize, Rect}; use tauri_utils::config::Color; @@ -20,12 +20,35 @@ impl AppWebview { } pub(crate) fn set_background_color(&self, color: Option) { + if let Some(native) = &self.native_wayland { + let (r, g, b, a) = color.unwrap_or_default().into(); + native.browser_view.set_background_color( + ((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | b as u32, + ); + return; + } let _ = (self, color); // Native child-window background is not equivalent to Chromium's rendered // background. Creation still applies BrowserSettings. } pub(crate) fn bounds(&self) -> Option { + if let Some(native) = &self.native_wayland { + let bounds = native.browser_view.bounds(); + let scale = native.scale_factor(); + return Some(Rect { + position: PhysicalPosition::new( + (bounds.x as f64 * scale).round() as i32, + (bounds.y as f64 * scale).round() as i32, + ) + .into(), + size: PhysicalSize::new( + (bounds.width.max(0) as f64 * scale).round() as u32, + (bounds.height.max(0) as f64 * scale).round() as u32, + ) + .into(), + }); + } let xid = self.xid(); with_cef_display(None, |xlib, display| unsafe { @@ -66,6 +89,10 @@ impl AppWebview { /// with neither focus nor pointer as inactive. Alloy does this in /// `CefWindowX11::Focus`; Chrome-style child windows have no equivalent. pub(crate) fn take_input_focus(&self) { + if let Some(native) = &self.native_wayland { + native.browser_view.request_focus(); + return; + } let xid = self.xid(); with_cef_display((), |xlib, display| unsafe { @@ -82,6 +109,9 @@ impl AppWebview { } pub(crate) fn reparent(&self, parent: &AppWindow) { + if self.native_wayland.is_some() { + return; + } let xid = self.xid(); let parent_xid = parent.xid(); @@ -92,6 +122,10 @@ impl AppWebview { } pub(crate) fn apply_visible(&self, visible: bool) { + if let Some(native) = &self.native_wayland { + native.browser_view.set_visible(i32::from(visible)); + return; + } let xid = self.xid(); with_cef_display((), |xlib, display| unsafe { @@ -128,6 +162,10 @@ impl AppWebview { } pub(crate) fn destroy_native(&self) { + if let Some(native) = &self.native_wayland { + native.force_close(); + return; + } let xid = self.xid(); with_cef_display((), |xlib, display| unsafe { (xlib.XDestroyWindow)(display, xid); @@ -136,6 +174,16 @@ impl AppWebview { } pub(crate) fn apply_physical_bounds(&self, _scale: f64, x: i32, y: i32, width: i32, height: i32) { + if let Some(native) = &self.native_wayland { + let scale = native.scale_factor(); + native.browser_view.set_bounds(Some(&cef::Rect { + x: (x as f64 / scale).round() as i32, + y: (y as f64 / scale).round() as i32, + width: (width.max(1) as f64 / scale).round() as i32, + height: (height.max(1) as f64 / scale).round() as i32, + })); + return; + } let xid = self.xid(); with_cef_display((), |xlib, display| unsafe { diff --git a/src/runtime.rs b/src/runtime.rs index 59343bf..6dc2ece 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( @@ -258,6 +260,8 @@ pub(crate) type AfterWindowCreationCallback = Box Fn(RawWindow<'a>) pub(crate) enum Message { EventLoop(EventLoopMessage), BrowserClosed(WindowId, u32), + #[cfg(target_os = "linux")] + NativeWaylandWindow(WindowId, crate::native_wayland::Event), Opened(Vec), #[cfg(target_os = "macos")] Reopen { @@ -512,6 +516,10 @@ impl WinitCefApp { self.state.live_browsers = self.state.live_browsers.saturating_sub(1); self.exit_if_done(event_loop); } + #[cfg(target_os = "linux")] + Message::NativeWaylandWindow(window_id, event) => { + self.handle_native_wayland_event(window_id, event, event_loop) + } Message::CreateWindow { window_id, webview_id, @@ -694,6 +702,11 @@ impl WinitCefApp { /// winit already considers the top-level unfocused once the browser child holds /// the focus, so it drops the `FocusOut` for a real loss. The loop still wakes. fn sync_delegated_focus(&mut self) { + #[cfg(target_os = "linux")] + if crate::config::native_wayland() { + return; + } + #[cfg(any( target_os = "linux", target_os = "dragonfly", @@ -724,6 +737,51 @@ impl WinitCefApp { } } + #[cfg(target_os = "linux")] + fn handle_native_wayland_event( + &mut self, + window_id: WindowId, + event: crate::native_wayland::Event, + event_loop: &dyn ActiveEventLoop, + ) { + match event { + crate::native_wayland::Event::CloseRequested => { + self.request_window_close(window_id, event_loop) + } + crate::native_wayland::Event::Destroyed => { + self.close_window(window_id, event_loop); + } + crate::native_wayland::Event::Focused(focused) => { + let Some(appwindow) = self.state.windows.get_mut(&window_id) else { + return; + }; + if appwindow.reported_focus != focused { + appwindow.reported_focus = focused; + for child in &appwindow.children { + child.host.set_focus(i32::from(focused)); + } + if focused && let Some(child) = appwindow.children.first() { + child.take_input_focus(); + } + self.emit_window_event(window_id, WindowEvent::Focused(focused)); + } + } + crate::native_wayland::Event::Resized(size) => { + self.emit_window_event(window_id, WindowEvent::Resized(size)); + } + crate::native_wayland::Event::ScaleFactorChanged { + scale_factor, + new_inner_size, + } => self.emit_window_event( + window_id, + WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size, + }, + ), + } + } + fn emit_window_event(&mut self, window_id: WindowId, event: WindowEvent) { let Some(appwindow) = self.state.windows.get(&window_id) else { return; @@ -813,7 +871,7 @@ impl WinitCefApp { // shutdown drain is still enforced by live_browsers. for child in &appwindow.children { self.remove_scheme_handler_entries(child); - child.host.close_browser(1); + child.close(); } self.exit_if_done(event_loop); } @@ -862,7 +920,7 @@ impl WinitCefApp { for appwindow in self.state.windows.values() { for child in &appwindow.children { self.remove_scheme_handler_entries(child); - child.host.close_browser(1); + child.close(); } } self.state.windows.clear(); @@ -1486,9 +1544,19 @@ impl CefRuntime { )); } - // Force X11 usage on Linux + #[cfg(target_os = "linux")] + match cef_config.linux_windowing { + crate::LinuxWindowing::X11 => { + command_line_args.push(("ozone-platform".to_string(), Some("x11".to_string()))); + event_loop_builder.with_x11(); + } + crate::LinuxWindowing::Wayland => { + command_line_args.push(("ozone-platform".to_string(), Some("wayland".to_string()))); + event_loop_builder.with_wayland(); + } + } + #[cfg(any( - target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", @@ -1599,8 +1667,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 +1745,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) } diff --git a/src/webview.rs b/src/webview.rs index f3cd17c..9287ddd 100644 --- a/src/webview.rs +++ b/src/webview.rs @@ -198,6 +198,8 @@ pub(crate) struct AppWebview { pub(crate) devtools_observer_registration: Arc>>, pub(crate) listeners: WebviewEventListeners, pub(crate) bounds_rate: Option, + #[cfg(target_os = "linux")] + pub(crate) native_wayland: Option, } impl AppWebview { @@ -231,6 +233,11 @@ impl AppWebview { self.apply_visible(visible); } + pub(crate) fn close(&self) { + self.host.close_browser(1); + self.destroy_native(); + } + pub fn url(&self) -> Option { self .browser @@ -280,6 +287,21 @@ impl WinitCefApp { drag_drop_event_target: browser_client::DragDropEventTarget, pending: PendingWebview>, ) -> Result<()> { + #[cfg(target_os = "linux")] + if crate::config::native_wayland() && !appwindow.children.is_empty() { + return Err(Error::CreateWebview( + "native Wayland supports one webview in its single top-level window" + .to_string() + .into(), + )); + } + #[cfg(target_os = "linux")] + let parent = if crate::config::native_wayland() { + Default::default() + } else { + appwindow.raw_cef_handle() + }; + #[cfg(not(target_os = "linux"))] let parent = appwindow.raw_cef_handle(); let parent_size = appwindow.window.surface_size(); let scale = appwindow.window.scale_factor(); @@ -295,6 +317,16 @@ impl WinitCefApp { scale, theme, drag_drop_event_target, + #[cfg(target_os = "linux")] + crate::config::native_wayland().then(|| { + crate::native_wayland::WindowConfig::new( + context, + appwindow.id, + &appwindow.attrs, + parent_size, + scale, + ) + }), pending, ) else { return Err(Error::CreateWebview( @@ -325,6 +357,7 @@ impl WinitCefApp { scale: f64, theme: Option, drag_drop_event_target: browser_client::DragDropEventTarget, + #[cfg(target_os = "linux")] native_wayland: Option, mut pending: PendingWebview>, ) -> Option { let bounds_rate = compute_child_bounds_rate( @@ -438,6 +471,78 @@ impl WinitCefApp { // Create with an inert document so the BrowserHost exists before the real // navigation; the real URL is loaded once the document-start script is set. let initial_url = CefString::from(INITIAL_LOAD_URL); + let finish_browser = move |browser: Browser, #[cfg(target_os = "linux")] native_wayland| { + let Some(host) = browser.host() else { + log::error!("CEF browser for webview {label:?} has no host"); + return; + }; + let browser_id = browser.identifier(); + + { + let mut registry = scheme_registry.lock().unwrap(); + for (scheme, handler) in uri_scheme_protocols.iter() { + registry.insert( + (browser_id, scheme.clone()), + ( + label.clone(), + handler.clone(), + initialization_scripts.clone(), + ), + ); + } + } + + let devtools_protocol_handlers = Arc::new(Mutex::new(Vec::new())); + let pending_initial_loads: PendingInitialLoads = Arc::new(Mutex::new(HashMap::new())); + let devtools_observer_registration = Arc::new(Mutex::new(add_dev_tools_observer( + &browser, + devtools_protocol_handlers.clone(), + pending_initial_loads.clone(), + ))); + load_initial_url_after_registering_initialization_scripts( + &browser, + &initialization_scripts, + &custom_protocol_scheme, + &custom_scheme_domain_names, + &real_initial_url, + &pending_initial_loads, + ); + + browser_tx + .send(AppWebview { + webview_id, + label, + browser, + browser_id, + host, + uri_scheme_protocols, + devtools_protocol_handlers, + devtools_observer_registration, + listeners: Default::default(), + bounds_rate, + #[cfg(target_os = "linux")] + native_wayland, + }) + .expect("failed to send initialized CEF browser"); + }; + + #[cfg(target_os = "linux")] + if let Some(native_wayland) = native_wayland { + if crate::native_wayland::create( + &mut client, + &initial_url, + &settings, + request_context.as_mut(), + native_wayland, + Box::new(move |browser, window| finish_browser(browser, Some(window))), + ) + .is_none() + { + log::error!("failed to create native Wayland CEF window"); + } + return; + } + let Some(browser) = cef::browser_host_create_browser_sync( Some(&window_info), Some(&mut client), @@ -446,59 +551,14 @@ impl WinitCefApp { None, request_context.as_mut(), ) else { - log::error!("failed to create CEF browser for webview {label:?}"); - return; - }; - let Some(host) = browser.host() else { - log::error!("CEF browser for webview {label:?} has no host"); + log::error!("failed to create CEF browser"); return; }; - let browser_id = browser.identifier(); - - { - let mut registry = scheme_registry.lock().unwrap(); - for (scheme, handler) in uri_scheme_protocols.iter() { - registry.insert( - (browser_id, scheme.clone()), - ( - label.clone(), - handler.clone(), - initialization_scripts.clone(), - ), - ); - } - } - - let devtools_protocol_handlers = Arc::new(Mutex::new(Vec::new())); - let pending_initial_loads: PendingInitialLoads = Arc::new(Mutex::new(HashMap::new())); - let devtools_observer_registration = Arc::new(Mutex::new(add_dev_tools_observer( - &browser, - devtools_protocol_handlers.clone(), - pending_initial_loads.clone(), - ))); - load_initial_url_after_registering_initialization_scripts( - &browser, - &initialization_scripts, - &custom_protocol_scheme, - &custom_scheme_domain_names, - &real_initial_url, - &pending_initial_loads, + finish_browser( + browser, + #[cfg(target_os = "linux")] + None, ); - - browser_tx - .send(AppWebview { - webview_id, - label, - browser, - browser_id, - host, - uri_scheme_protocols, - devtools_protocol_handlers, - devtools_observer_registration, - listeners: Default::default(), - bounds_rate, - }) - .expect("failed to send initialized CEF browser"); } }); let request_context = request_context::request_context_from_webview_attributes( @@ -517,7 +577,7 @@ impl WinitCefApp { // `None` here means browser creation failed (or the request context never // initialized); the continuation logs the reason. Soft-fail instead of // taking down the whole process. - browser_rx.recv().ok() + request_context::wait_for_deferred_result(&browser_rx) } pub(crate) fn handle_webview_message( @@ -603,12 +663,11 @@ impl WinitCefApp { // callback can race parent-window bookkeeping. Window/app teardown already // uses force_close=true; standalone child close needs the same semantics. WebviewMessage::Close => { - child.host.close_browser(1); // Windowed CEF browsers are not destroyed by CloseBrowser alone: the // native child hierarchy must also be torn down before OnBeforeClose // runs. Leaving it attached leaks the renderer; letting CEF forward a // close to its top-level parent can close the whole Tauri window. - child.destroy_native(); + child.close(); } WebviewMessage::SetBounds(bounds) => { let parent_size = appwindow.window.surface_size(); @@ -1228,6 +1287,14 @@ impl WebviewDispatch for CefWebviewDispatcher { /// from the current window size; children with fixed bounds keep whatever bounds /// they were last given. pub(crate) fn layout_app_window(appwindow: &AppWindow) { + #[cfg(target_os = "linux")] + if appwindow + .children + .first() + .is_some_and(|child| child.native_wayland.is_some()) + { + return; + } let parent_size = appwindow.window.surface_size(); let win_w = parent_size.width as f32; let win_h = parent_size.height as f32; diff --git a/src/window.rs b/src/window.rs index fa2d25f..c8cb6b0 100644 --- a/src/window.rs +++ b/src/window.rs @@ -422,6 +422,13 @@ impl WinitCefApp { pending: Box>>, _after_window_creation: Option, ) -> Result<()> { + #[cfg(target_os = "linux")] + let native_wayland = crate::config::native_wayland(); + #[cfg(target_os = "linux")] + if native_wayland && !self.state.windows.is_empty() { + return Err(Error::CreateWindow); + } + let mut attrs = pending.window_builder.attrs.clone(); if attrs.inner.preferred_theme.is_none() { attrs.inner.preferred_theme = @@ -429,8 +436,15 @@ impl WinitCefApp { } prepare_window_attributes(event_loop, &mut attrs); + let mut control_attrs = attrs.inner.clone(); + #[cfg(target_os = "linux")] + if native_wayland { + // CEF Views owns the visible top-level; winit remains an invisible + // event-loop and monitor provider for Tauri's runtime contract. + control_attrs.visible = false; + } let window = event_loop - .create_window(attrs.inner.clone()) + .create_window(control_attrs) .map_err(|_| Error::CreateWindow)?; let winit_id = window.id(); @@ -457,8 +471,13 @@ impl WinitCefApp { } } + #[cfg(target_os = "linux")] + if !native_wayland { + appwindow.set_visible_on_all_workspaces(appwindow.attrs.visible_on_all_workspaces); + appwindow.set_skip_taskbar(appwindow.attrs.skip_taskbar); + } + #[cfg(any( - target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", @@ -474,7 +493,12 @@ impl WinitCefApp { appwindow.draw_background_surface(); } - #[cfg(not(windows))] + #[cfg(target_os = "linux")] + if appwindow.attrs.background_color.is_some() && !native_wayland { + appwindow.set_background_color(appwindow.attrs.background_color); + } + + #[cfg(all(not(windows), not(target_os = "linux")))] if appwindow.attrs.background_color.is_some() { appwindow.set_background_color(appwindow.attrs.background_color); } @@ -503,6 +527,14 @@ impl WinitCefApp { )?; } + #[cfg(target_os = "linux")] + if !native_wayland { + self + .state + .winid_id_to_window_id_map + .insert(winit_id, window_id); + } + #[cfg(not(target_os = "linux"))] self .state .winid_id_to_window_id_map @@ -534,6 +566,10 @@ impl WinitCefApp { let Some(appwindow) = self.state.windows.get_mut(&window_id) else { return; }; + #[cfg(target_os = "linux")] + let Some(message) = crate::native_wayland::handle_window_message(appwindow, message) else { + return; + }; let window = &appwindow.window; match message { From d6325895f2f8dc250e61b21bd1fb20c7f8f42756 Mon Sep 17 00:00:00 2001 From: lunar-seal Date: Tue, 25 Aug 2026 11:28:11 +0200 Subject: [PATCH 2/8] chore: add Rust development flake Co-Authored-By: GPT-5 Codex --- flake.lock | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ flake.nix | 53 ++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix 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..42ff4fc --- /dev/null +++ b/flake.nix @@ -0,0 +1,53 @@ +{ + description = "Rust Rover environment"; + + 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 + ]; + + shellHook = '' + # cef-dll-sys preserves read-only archive modes, then overwrites + # those files when its build script changes. + if [ -d target/debug ]; then + find target/debug -maxdepth 2 -type f ! -perm -u=w -exec chmod u+w {} + + fi + + mkdir -p ~/.rust-rover/toolchain + + ln -sfn ${rustToolchain}/lib ~/.rust-rover/toolchain + ln -sfn ${rustToolchain}/bin ~/.rust-rover/toolchain + + export RUST_SRC_PATH="$HOME/.rust-rover/toolchain/lib/rustlib/src/rust/library" + ''; + }; + } + ); +} From dbec855af707953b6e9ed5ca41ed5af12df2248d Mon Sep 17 00:00:00 2001 From: lunar-seal Date: Tue, 25 Aug 2026 18:13:37 +0200 Subject: [PATCH 3/8] feat(linux): forward window state and drag regions to native Wayland Resizable/maximizable/minimizable/closable, min/max size, and always-on-top now reach the CEF window delegate instead of being dropped, and frameless native Wayland windows get Tauri's data-tauri-drag-region CSS behavior via CEF's draggable-regions API. Co-Authored-By: GPT-5 Codex --- README.md | 8 +- src/cef_impl/client/drag.rs | 14 +++ src/cef_impl/client/mod.rs | 14 ++- src/native_wayland.rs | 235 ++++++++++++++++++++++++++++++------ src/webview.rs | 17 ++- 5 files changed, 246 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 1b6214c..f762711 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,13 @@ This is a parallel CEF Views path: CEF owns the single visible top-level and its browser view. X11 remains the default and compiled fallback, but the runtime does not open an X display connection in Wayland mode. Native Wayland currently supports one window with one full-window webview; Linux raw window handles and runtime -decoration/constraint changes are unavailable in this mode. +decoration changes are unavailable in this mode. Creation-time frameless windows +support Tauri drag regions; size constraints and resizable/maximizable/minimizable/ +closable state are forwarded to the CEF window delegate. + +CEF's Chrome password manager and this crate's permission policy use the same +request-context and browser-client paths on X11 and Wayland; non-incognito +profiles use the same persistent cache path. ## Additions diff --git a/src/cef_impl/client/drag.rs b/src/cef_impl/client/drag.rs index fd493a2..ed98a61 100644 --- a/src/cef_impl/client/drag.rs +++ b/src/cef_impl/client/drag.rs @@ -18,6 +18,8 @@ use url::Url; use crate::runtime::{Message, RuntimeContext}; +pub(crate) type DraggableRegionsChanged = Arc)>; + const DRAG_DROP_BRIDGE_PATH: &str = "/__tauri_cef_drag_drop__"; const DRAG_DROP_INIT_SCRIPT: &str = r#" @@ -141,6 +143,7 @@ fn collect_drag_data_paths(drag_data: &mut DragData) -> Vec { wrap_drag_handler! { pub struct TauriCefDragHandler { drag_drop_state: Arc>, + draggable_regions_changed: Option, } impl DragHandler { @@ -161,6 +164,17 @@ wrap_drag_handler! { // report over/drop/leave with accurate viewport positions. 0 } + + fn on_draggable_regions_changed( + &self, + _browser: Option<&mut Browser>, + _frame: Option<&mut Frame>, + regions: Option<&[DraggableRegion]>, + ) { + if let Some(handler) = &self.draggable_regions_changed { + handler(regions); + } + } } } diff --git a/src/cef_impl/client/mod.rs b/src/cef_impl/client/mod.rs index b1993f2..9b2efe5 100644 --- a/src/cef_impl/client/mod.rs +++ b/src/cef_impl/client/mod.rs @@ -28,8 +28,8 @@ use display::TauriCefDisplayHandler; use download::TauriCefDownloadHandler; use drag::TauriCefDragHandler; pub(crate) use drag::{ - DragDropEventTarget, DragDropScriptEvent, DragDropState, WebDragDropResourceRequestHandler, - drag_drop_initialization_script, event_from_script_event, + DragDropEventTarget, DragDropScriptEvent, DragDropState, DraggableRegionsChanged, + WebDragDropResourceRequestHandler, drag_drop_initialization_script, event_from_script_event, }; use keyboard::TauriCefKeyboardHandler; use life_span::TauriCefChildLifeSpanHandler; @@ -75,6 +75,7 @@ wrap_client! { drag_drop_event_target: DragDropEventTarget, drag_drop_handler_enabled: bool, drag_drop_state: Arc>, + draggable_regions_changed: Option, pub(crate) handlers: TauriCefBrowserClientHandlers, proxy: WinitEventLoopProxy, sender: Sender>, @@ -82,9 +83,12 @@ wrap_client! { impl Client { fn drag_handler(&self) -> Option { - self - .drag_drop_handler_enabled - .then(|| TauriCefDragHandler::new(self.drag_drop_state.clone())) + (self.drag_drop_handler_enabled || self.draggable_regions_changed.is_some()).then(|| { + TauriCefDragHandler::new( + self.drag_drop_state.clone(), + self.draggable_regions_changed.clone(), + ) + }) } fn request_handler(&self) -> Option { diff --git a/src/native_wayland.rs b/src/native_wayland.rs index 64846e2..07abfc2 100644 --- a/src/native_wayland.rs +++ b/src/native_wayland.rs @@ -17,6 +17,7 @@ use cef::{rc::Rc, *}; use tauri_runtime::{ Error, UserEvent, dpi::{PhysicalPosition, PhysicalSize, Size as TauriSize}, + webview::InitializationScript, window::WindowId, }; use winit::window::WindowLevel; @@ -29,6 +30,76 @@ use crate::{ type BrowserCreated = Box; type Emit = Arc; +const DRAG_REGION_SCRIPT: &str = r#" +(() => { + const style = document.createElement("style"); + style.textContent = ` + [data-tauri-drag-region]:not([data-tauri-drag-region="false"]) { + -webkit-app-region: drag; + } + [data-tauri-drag-region]:not([data-tauri-drag-region="deep"]) * { + -webkit-app-region: no-drag; + } + [data-tauri-drag-region="false"], + [data-tauri-drag-region="deep"] :is(a, button, input, select, textarea, label, summary, + [contenteditable]:not([contenteditable="false"]), [tabindex]:not([tabindex="-1"]), + [role="button"], [role="link"], [role="menuitem"], [role="tab"], [role="checkbox"], + [role="radio"], [role="switch"], [role="option"]):not([data-tauri-drag-region]) { + -webkit-app-region: no-drag; + } + `; + (document.head || document.documentElement).append(style); +})(); +"#; + +pub(crate) fn drag_region_initialization_script() -> InitializationScript { + InitializationScript { + script: DRAG_REGION_SCRIPT.to_string(), + for_main_frame_only: false, + } +} + +#[derive(Default)] +struct DraggableRegionsState { + window: Option, + regions: Vec, +} + +#[derive(Clone, Default)] +struct DraggableRegions(Arc>); + +impl DraggableRegions { + fn attach(&self, window: Window) { + let regions = { + let mut state = self.0.lock().unwrap(); + state.window = Some(window.clone()); + state.regions.clone() + }; + window.set_draggable_regions((!regions.is_empty()).then_some(regions.as_slice())); + } + + fn set(&self, regions: Option<&[DraggableRegion]>) { + let (window, regions) = { + let mut state = self.0.lock().unwrap(); + state.regions = regions.unwrap_or_default().to_vec(); + (state.window.clone(), state.regions.clone()) + }; + if let Some(window) = window { + window.set_draggable_regions((!regions.is_empty()).then_some(regions.as_slice())); + } + } +} + +#[derive(Clone)] +struct WindowState { + resizable: bool, + maximizable: bool, + minimizable: bool, + closable: bool, + min_size: Option, + max_size: Option, +} + #[derive(Debug)] pub(crate) enum Event { CloseRequested, @@ -54,12 +125,12 @@ pub(crate) struct WindowConfig { show_state: ShowState, visible: bool, frameless: bool, - resizable: bool, - maximizable: bool, - minimizable: bool, - closable: bool, + always_on_top: bool, + initial_scale_factor: f64, app_id: String, emit: Emit, + state: Arc>, + draggable_regions: DraggableRegions, } impl WindowConfig { @@ -100,14 +171,32 @@ impl WindowConfig { show_state, visible: attrs.inner.visible, frameless: !attrs.inner.decorations, - 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), + always_on_top: attrs.inner.window_level == WindowLevel::AlwaysOnTop, + initial_scale_factor: scale_factor, app_id: crate::config::config().identifier.clone(), emit, + state: Arc::new(Mutex::new(WindowState { + 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, + max_size: attrs.inner.max_surface_size, + })), + draggable_regions: DraggableRegions::default(), } } + + pub(crate) fn draggable_regions_changed( + &self, + ) -> crate::cef_impl::client::DraggableRegionsChanged { + let draggable_regions = self.draggable_regions.clone(); + Arc::new(move |regions| draggable_regions.set(regions)) + } + + pub(crate) fn is_frameless(&self) -> bool { + self.frameless + } } #[derive(Clone)] @@ -115,6 +204,7 @@ pub(crate) struct NativeWindow { pub(crate) window: Window, pub(crate) browser_view: BrowserView, allow_close: Arc, + state: Arc>, } impl NativeWindow { @@ -185,22 +275,34 @@ pub(crate) fn handle_window_message( WindowMessage::Center => window.center_window(Some(&cef_size(outer_size, scale))), WindowMessage::RequestUserAttention(_) => {} WindowMessage::SetEnabled(enabled) => window.set_enabled(i32::from(enabled)), - WindowMessage::SetResizable(resizable) => appwindow.attrs.inner.resizable = resizable, - WindowMessage::SetMaximizable(enabled) => appwindow - .attrs - .inner - .enabled_buttons - .set(winit::window::WindowButtons::MAXIMIZE, enabled), - WindowMessage::SetMinimizable(enabled) => appwindow - .attrs - .inner - .enabled_buttons - .set(winit::window::WindowButtons::MINIMIZE, enabled), - WindowMessage::SetClosable(enabled) => appwindow - .attrs - .inner - .enabled_buttons - .set(winit::window::WindowButtons::CLOSE, enabled), + WindowMessage::SetResizable(resizable) => { + appwindow.attrs.inner.resizable = resizable; + native.state.lock().unwrap().resizable = resizable; + } + WindowMessage::SetMaximizable(enabled) => { + appwindow + .attrs + .inner + .enabled_buttons + .set(winit::window::WindowButtons::MAXIMIZE, enabled); + native.state.lock().unwrap().maximizable = enabled; + } + WindowMessage::SetMinimizable(enabled) => { + appwindow + .attrs + .inner + .enabled_buttons + .set(winit::window::WindowButtons::MINIMIZE, enabled); + native.state.lock().unwrap().minimizable = enabled; + } + WindowMessage::SetClosable(enabled) => { + appwindow + .attrs + .inner + .enabled_buttons + .set(winit::window::WindowButtons::CLOSE, enabled); + native.state.lock().unwrap().closable = enabled; + } WindowMessage::SetTitle(title) => { appwindow.attrs.inner.title = title.clone(); window.set_title(Some(&CefString::from(title.as_str()))); @@ -210,7 +312,7 @@ pub(crate) fn handle_window_message( WindowMessage::Minimize => window.minimize(), WindowMessage::Show => window.show(), WindowMessage::Hide => window.hide(), - WindowMessage::SetDecorations(decorations) => appwindow.attrs.inner.decorations = decorations, + WindowMessage::SetDecorations(_) => {} WindowMessage::SetAlwaysOnBottom(_) => {} WindowMessage::SetAlwaysOnTop(on_top) => { appwindow.attrs.inner.window_level = if on_top { @@ -223,16 +325,23 @@ pub(crate) fn handle_window_message( WindowMessage::SetVisibleOnAllWorkspaces(_) | WindowMessage::SetContentProtected(_) => {} WindowMessage::SetSize(size) => window.set_size(Some(&cef_size_from_tauri(size, scale))), WindowMessage::SetMinSize(size) => { - appwindow.attrs.inner.min_surface_size = size; + appwindow.attrs.inner.min_surface_size = size.clone(); + native.state.lock().unwrap().min_size = size; } WindowMessage::SetMaxSize(size) => { - appwindow.attrs.inner.max_surface_size = size; + appwindow.attrs.inner.max_surface_size = size.clone(); + native.state.lock().unwrap().max_size = size; } WindowMessage::SetSizeConstraints(constraints) => { - appwindow.attrs.inner.min_surface_size = + let min_size = crate::window::paired_size_constraint(constraints.min_width, constraints.min_height); - appwindow.attrs.inner.max_surface_size = + let max_size = crate::window::paired_size_constraint(constraints.max_width, constraints.max_height); + appwindow.attrs.inner.min_surface_size = min_size.clone(); + appwindow.attrs.inner.max_surface_size = max_size.clone(); + let mut state = native.state.lock().unwrap(); + state.min_size = min_size; + state.max_size = max_size; } WindowMessage::SetPosition(_position) => {} WindowMessage::SetFullscreen(fullscreen) => window.set_fullscreen(i32::from(fullscreen)), @@ -285,6 +394,8 @@ wrap_browser_view_delegate! { struct NativeBrowserViewDelegate { on_created: Arc>>, allow_close: Arc, + state: Arc>, + draggable_regions: DraggableRegions, } impl ViewDelegate {} @@ -310,12 +421,14 @@ wrap_browser_view_delegate! { log::error!("native Wayland browser view has no CEF window"); return; }; + self.draggable_regions.attach(window.clone()); on_created( browser.clone(), NativeWindow { window, browser_view: browser_view.clone(), allow_close: self.allow_close.clone(), + state: self.state.clone(), }, ); } @@ -354,6 +467,30 @@ wrap_window_delegate! { height: self.config.bounds.height, } } + + fn minimum_size(&self, view: Option<&mut View>) -> Size { + self + .config + .state + .lock() + .unwrap() + .min_size + .clone() + .map(|size| cef_size_from_tauri(size, view_scale_factor(view, self.config.initial_scale_factor))) + .unwrap_or_default() + } + + fn maximum_size(&self, view: Option<&mut View>) -> Size { + self + .config + .state + .lock() + .unwrap() + .max_size + .clone() + .map(|size| cef_size_from_tauri(size, view_scale_factor(view, self.config.initial_scale_factor))) + .unwrap_or_default() + } } impl PanelDelegate {} @@ -365,6 +502,7 @@ wrap_window_delegate! { 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()))); + window.set_always_on_top(i32::from(self.config.always_on_top)); if self.config.visible { window.show(); } @@ -410,21 +548,21 @@ wrap_window_delegate! { } fn can_resize(&self, _window: Option<&mut Window>) -> i32 { - i32::from(self.config.resizable) + i32::from(self.config.state.lock().unwrap().resizable) } fn can_maximize(&self, _window: Option<&mut Window>) -> i32 { - i32::from(self.config.maximizable) + i32::from(self.config.state.lock().unwrap().maximizable) } fn can_minimize(&self, _window: Option<&mut Window>) -> i32 { - i32::from(self.config.minimizable) + i32::from(self.config.state.lock().unwrap().minimizable) } fn can_close(&self, _window: Option<&mut Window>) -> i32 { if self.allow_close.load(Ordering::Acquire) { 1 - } else if !self.config.closable { + } else if !self.config.state.lock().unwrap().closable { 0 } else { (self.config.emit)(Event::CloseRequested); @@ -457,8 +595,12 @@ pub(crate) fn create( 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 mut browser_delegate = NativeBrowserViewDelegate::new( + Arc::new(Mutex::new(Some(on_created))), + allow_close.clone(), + config.state.clone(), + config.draggable_regions.clone(), + ); let browser_view = browser_view_create( Some(client), Some(url), @@ -491,3 +633,26 @@ fn scale_factor(window: &Window) -> f64 { .map(|display| display.device_scale_factor() as f64) .unwrap_or(1.0) } + +fn view_scale_factor(view: Option<&mut View>, fallback: f64) -> f64 { + view + .and_then(|view| view.window()) + .as_ref() + .map(scale_factor) + .unwrap_or(fallback) +} + +#[cfg(test)] +mod tests { + use super::*; + use tauri_runtime::dpi::LogicalSize; + + #[test] + fn cef_sizes_stay_in_device_independent_pixels() { + let physical = cef_size(PhysicalSize::new(300, 180), 1.5); + assert_eq!((physical.width, physical.height), (200, 120)); + + let logical = cef_size_from_tauri(TauriSize::Logical(LogicalSize::new(200.0, 120.0)), 1.5); + assert_eq!((logical.width, logical.height), (200, 120)); + } +} diff --git a/src/webview.rs b/src/webview.rs index 9287ddd..1716e8c 100644 --- a/src/webview.rs +++ b/src/webview.rs @@ -366,7 +366,16 @@ impl WinitCefApp { parent_size, scale, ); - let initialization_scripts = initialization_scripts(&mut pending.webview_attributes); + let mut initialization_scripts = initialization_scripts(&mut pending.webview_attributes); + #[cfg(target_os = "linux")] + if native_wayland + .as_ref() + .is_some_and(crate::native_wayland::WindowConfig::is_frameless) + { + Arc::make_mut(&mut initialization_scripts).push(CefInitScript::new( + crate::native_wayland::drag_region_initialization_script(), + )); + } let uri_scheme_protocols: Arc> = Arc::new( pending .uri_scheme_protocols @@ -412,6 +421,12 @@ impl WinitCefApp { drag_drop_event_target, drag_drop_handler_enabled, drag_drop_state, + #[cfg(target_os = "linux")] + native_wayland + .as_ref() + .map(crate::native_wayland::WindowConfig::draggable_regions_changed), + #[cfg(not(target_os = "linux"))] + None, handlers, context.proxy.clone(), context.sender.clone(), From 1fe88b321925621c6462d72515d634865f3767eb Mon Sep 17 00:00:00 2001 From: lunar-seal Date: Tue, 25 Aug 2026 18:22:00 +0200 Subject: [PATCH 4/8] feat(linux): gate native Wayland behind a native-wayland feature The CEF Views/Ozone-Wayland windowing path is a parallel, less-hardened alternative to the default X11 embedding path this crate has years of focus/embedding fixes behind: single window, single webview, no raw window handles, no runtime decoration changes. Sable, the only current consumer, already treats the entire CEF runtime as opt-in via its own `cef` Cargo feature (mirroring `wry`); this crate itself already gates other non-default surfaces the same way (`devtools`, `sandbox`, `macos-private-api`). Gate LinuxWindowing::Wayland and the native_wayland module the same way so a consumer has to explicitly opt in before this experimental path is even reachable, instead of it being silently compiled into every Linux build. Also drops a few `.clone()` calls on Copy types in native_wayland.rs that clippy flags once the native-wayland feature actually gets built. Co-Authored-By: GPT-5 Codex --- Cargo.toml | 5 +++++ README.md | 4 +++- src/config.rs | 13 +++++++++++-- src/lib.rs | 2 +- src/native_wayland.rs | 10 ++++------ src/platform/linux/webview.rs | 11 ++++++++++- src/runtime.rs | 7 ++++--- src/webview.rs | 33 ++++++++++++++++++++++----------- src/window.rs | 2 +- 9 files changed, 61 insertions(+), 26 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1fccfbf..65939da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,6 +97,11 @@ x11-dl = "2.21" default = ["sandbox"] devtools = [] macos-private-api = ["tauri-runtime/macos-private-api"] +# Linux CEF Views/Ozone-Wayland windowing path (`LinuxWindowing::Wayland`). +# Off by default: unlike the X11 embedding path, it owns a single top-level +# window with a single full-window webview, has no raw window handles, and +# can't change decorations at runtime. Opt in once an app needs it. +native-wayland = [] sandbox = ["cef/sandbox"] [dev-dependencies] diff --git a/README.md b/README.md index f762711..5a49254 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,9 @@ fn main() { Because published tauri only defaults its generic types (`AppHandle`, `WebviewWindow`, …) to wry, apps alias them once (`type AppHandle = tauri::AppHandle;`) and build tauri with `default-features = false`. -On Linux, native Wayland can be selected before startup: +On Linux, native Wayland can be selected before startup — this is opt-in via the +`native-wayland` crate feature (`tauri-runtime-cef = { ..., features = ["native-wayland"] }`), +which also gates `LinuxWindowing::Wayland` itself: ```rust tauri_runtime_cef::configure(tauri_runtime_cef::CefConfig { diff --git a/src/config.rs b/src/config.rs index dac743c..124d860 100644 --- a/src/config.rs +++ b/src/config.rs @@ -28,7 +28,9 @@ use std::sync::OnceLock; pub enum LinuxWindowing { #[default] X11, - /// A CEF Views-owned top-level window using Ozone/Wayland. + /// A CEF Views-owned top-level window using Ozone/Wayland. Requires the + /// `native-wayland` crate feature. + #[cfg(feature = "native-wayland")] Wayland, } @@ -91,5 +93,12 @@ pub(crate) fn config() -> &'static CefConfig { #[cfg(target_os = "linux")] pub(crate) fn native_wayland() -> bool { - config().linux_windowing == LinuxWindowing::Wayland + #[cfg(feature = "native-wayland")] + { + config().linux_windowing == LinuxWindowing::Wayland + } + #[cfg(not(feature = "native-wayland"))] + { + false + } } diff --git a/src/lib.rs b/src/lib.rs index c1db4d3..a7c7605 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ mod cef_impl; mod compat; mod config; mod external_message_pump; -#[cfg(target_os = "linux")] +#[cfg(all(target_os = "linux", feature = "native-wayland"))] mod native_wayland; mod platform; mod policy; diff --git a/src/native_wayland.rs b/src/native_wayland.rs index 07abfc2..879a2a8 100644 --- a/src/native_wayland.rs +++ b/src/native_wayland.rs @@ -325,11 +325,11 @@ pub(crate) fn handle_window_message( WindowMessage::SetVisibleOnAllWorkspaces(_) | WindowMessage::SetContentProtected(_) => {} WindowMessage::SetSize(size) => window.set_size(Some(&cef_size_from_tauri(size, scale))), WindowMessage::SetMinSize(size) => { - appwindow.attrs.inner.min_surface_size = size.clone(); + appwindow.attrs.inner.min_surface_size = size; native.state.lock().unwrap().min_size = size; } WindowMessage::SetMaxSize(size) => { - appwindow.attrs.inner.max_surface_size = size.clone(); + appwindow.attrs.inner.max_surface_size = size; native.state.lock().unwrap().max_size = size; } WindowMessage::SetSizeConstraints(constraints) => { @@ -337,8 +337,8 @@ pub(crate) fn handle_window_message( crate::window::paired_size_constraint(constraints.min_width, constraints.min_height); let max_size = crate::window::paired_size_constraint(constraints.max_width, constraints.max_height); - appwindow.attrs.inner.min_surface_size = min_size.clone(); - appwindow.attrs.inner.max_surface_size = max_size.clone(); + appwindow.attrs.inner.min_surface_size = min_size; + appwindow.attrs.inner.max_surface_size = max_size; let mut state = native.state.lock().unwrap(); state.min_size = min_size; state.max_size = max_size; @@ -475,7 +475,6 @@ wrap_window_delegate! { .lock() .unwrap() .min_size - .clone() .map(|size| cef_size_from_tauri(size, view_scale_factor(view, self.config.initial_scale_factor))) .unwrap_or_default() } @@ -487,7 +486,6 @@ wrap_window_delegate! { .lock() .unwrap() .max_size - .clone() .map(|size| cef_size_from_tauri(size, view_scale_factor(view, self.config.initial_scale_factor))) .unwrap_or_default() } diff --git a/src/platform/linux/webview.rs b/src/platform/linux/webview.rs index 565443b..821320a 100644 --- a/src/platform/linux/webview.rs +++ b/src/platform/linux/webview.rs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use cef::{ImplBrowserHost, ImplView}; +use cef::ImplBrowserHost; +#[cfg(feature = "native-wayland")] +use cef::ImplView; use std::os::raw::c_ulong; use tauri_runtime::dpi::{PhysicalPosition, PhysicalSize, Rect}; use tauri_utils::config::Color; @@ -20,6 +22,7 @@ impl AppWebview { } pub(crate) fn set_background_color(&self, color: Option) { + #[cfg(feature = "native-wayland")] if let Some(native) = &self.native_wayland { let (r, g, b, a) = color.unwrap_or_default().into(); native.browser_view.set_background_color( @@ -33,6 +36,7 @@ impl AppWebview { } pub(crate) fn bounds(&self) -> Option { + #[cfg(feature = "native-wayland")] if let Some(native) = &self.native_wayland { let bounds = native.browser_view.bounds(); let scale = native.scale_factor(); @@ -89,6 +93,7 @@ impl AppWebview { /// with neither focus nor pointer as inactive. Alloy does this in /// `CefWindowX11::Focus`; Chrome-style child windows have no equivalent. pub(crate) fn take_input_focus(&self) { + #[cfg(feature = "native-wayland")] if let Some(native) = &self.native_wayland { native.browser_view.request_focus(); return; @@ -109,6 +114,7 @@ impl AppWebview { } pub(crate) fn reparent(&self, parent: &AppWindow) { + #[cfg(feature = "native-wayland")] if self.native_wayland.is_some() { return; } @@ -122,6 +128,7 @@ impl AppWebview { } pub(crate) fn apply_visible(&self, visible: bool) { + #[cfg(feature = "native-wayland")] if let Some(native) = &self.native_wayland { native.browser_view.set_visible(i32::from(visible)); return; @@ -162,6 +169,7 @@ impl AppWebview { } pub(crate) fn destroy_native(&self) { + #[cfg(feature = "native-wayland")] if let Some(native) = &self.native_wayland { native.force_close(); return; @@ -174,6 +182,7 @@ impl AppWebview { } pub(crate) fn apply_physical_bounds(&self, _scale: f64, x: i32, y: i32, width: i32, height: i32) { + #[cfg(feature = "native-wayland")] if let Some(native) = &self.native_wayland { let scale = native.scale_factor(); native.browser_view.set_bounds(Some(&cef::Rect { diff --git a/src/runtime.rs b/src/runtime.rs index 6dc2ece..f82dc3f 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -260,7 +260,7 @@ pub(crate) type AfterWindowCreationCallback = Box Fn(RawWindow<'a>) pub(crate) enum Message { EventLoop(EventLoopMessage), BrowserClosed(WindowId, u32), - #[cfg(target_os = "linux")] + #[cfg(all(target_os = "linux", feature = "native-wayland"))] NativeWaylandWindow(WindowId, crate::native_wayland::Event), Opened(Vec), #[cfg(target_os = "macos")] @@ -516,7 +516,7 @@ impl WinitCefApp { self.state.live_browsers = self.state.live_browsers.saturating_sub(1); self.exit_if_done(event_loop); } - #[cfg(target_os = "linux")] + #[cfg(all(target_os = "linux", feature = "native-wayland"))] Message::NativeWaylandWindow(window_id, event) => { self.handle_native_wayland_event(window_id, event, event_loop) } @@ -737,7 +737,7 @@ impl WinitCefApp { } } - #[cfg(target_os = "linux")] + #[cfg(all(target_os = "linux", feature = "native-wayland"))] fn handle_native_wayland_event( &mut self, window_id: WindowId, @@ -1550,6 +1550,7 @@ impl CefRuntime { command_line_args.push(("ozone-platform".to_string(), Some("x11".to_string()))); event_loop_builder.with_x11(); } + #[cfg(feature = "native-wayland")] crate::LinuxWindowing::Wayland => { command_line_args.push(("ozone-platform".to_string(), Some("wayland".to_string()))); event_loop_builder.with_wayland(); diff --git a/src/webview.rs b/src/webview.rs index 1716e8c..48ff4af 100644 --- a/src/webview.rs +++ b/src/webview.rs @@ -198,7 +198,7 @@ pub(crate) struct AppWebview { pub(crate) devtools_observer_registration: Arc>>, pub(crate) listeners: WebviewEventListeners, pub(crate) bounds_rate: Option, - #[cfg(target_os = "linux")] + #[cfg(all(target_os = "linux", feature = "native-wayland"))] pub(crate) native_wayland: Option, } @@ -317,7 +317,7 @@ impl WinitCefApp { scale, theme, drag_drop_event_target, - #[cfg(target_os = "linux")] + #[cfg(all(target_os = "linux", feature = "native-wayland"))] crate::config::native_wayland().then(|| { crate::native_wayland::WindowConfig::new( context, @@ -357,7 +357,9 @@ impl WinitCefApp { scale: f64, theme: Option, drag_drop_event_target: browser_client::DragDropEventTarget, - #[cfg(target_os = "linux")] native_wayland: Option, + #[cfg(all(target_os = "linux", feature = "native-wayland"))] native_wayland: Option< + crate::native_wayland::WindowConfig, + >, mut pending: PendingWebview>, ) -> Option { let bounds_rate = compute_child_bounds_rate( @@ -366,8 +368,12 @@ impl WinitCefApp { parent_size, scale, ); + #[cfg_attr( + not(all(target_os = "linux", feature = "native-wayland")), + allow(unused_mut) + )] let mut initialization_scripts = initialization_scripts(&mut pending.webview_attributes); - #[cfg(target_os = "linux")] + #[cfg(all(target_os = "linux", feature = "native-wayland"))] if native_wayland .as_ref() .is_some_and(crate::native_wayland::WindowConfig::is_frameless) @@ -421,11 +427,11 @@ impl WinitCefApp { drag_drop_event_target, drag_drop_handler_enabled, drag_drop_state, - #[cfg(target_os = "linux")] + #[cfg(all(target_os = "linux", feature = "native-wayland"))] native_wayland .as_ref() .map(crate::native_wayland::WindowConfig::draggable_regions_changed), - #[cfg(not(target_os = "linux"))] + #[cfg(not(all(target_os = "linux", feature = "native-wayland")))] None, handlers, context.proxy.clone(), @@ -486,7 +492,12 @@ impl WinitCefApp { // Create with an inert document so the BrowserHost exists before the real // navigation; the real URL is loaded once the document-start script is set. let initial_url = CefString::from(INITIAL_LOAD_URL); - let finish_browser = move |browser: Browser, #[cfg(target_os = "linux")] native_wayland| { + let finish_browser = move |browser: Browser, + #[cfg(all( + target_os = "linux", + feature = "native-wayland" + ))] + native_wayland| { let Some(host) = browser.host() else { log::error!("CEF browser for webview {label:?} has no host"); return; @@ -535,13 +546,13 @@ impl WinitCefApp { devtools_observer_registration, listeners: Default::default(), bounds_rate, - #[cfg(target_os = "linux")] + #[cfg(all(target_os = "linux", feature = "native-wayland"))] native_wayland, }) .expect("failed to send initialized CEF browser"); }; - #[cfg(target_os = "linux")] + #[cfg(all(target_os = "linux", feature = "native-wayland"))] if let Some(native_wayland) = native_wayland { if crate::native_wayland::create( &mut client, @@ -571,7 +582,7 @@ impl WinitCefApp { }; finish_browser( browser, - #[cfg(target_os = "linux")] + #[cfg(all(target_os = "linux", feature = "native-wayland"))] None, ); } @@ -1302,7 +1313,7 @@ impl WebviewDispatch for CefWebviewDispatcher { /// from the current window size; children with fixed bounds keep whatever bounds /// they were last given. pub(crate) fn layout_app_window(appwindow: &AppWindow) { - #[cfg(target_os = "linux")] + #[cfg(all(target_os = "linux", feature = "native-wayland"))] if appwindow .children .first() diff --git a/src/window.rs b/src/window.rs index c8cb6b0..ed081c8 100644 --- a/src/window.rs +++ b/src/window.rs @@ -566,7 +566,7 @@ impl WinitCefApp { let Some(appwindow) = self.state.windows.get_mut(&window_id) else { return; }; - #[cfg(target_os = "linux")] + #[cfg(all(target_os = "linux", feature = "native-wayland"))] let Some(message) = crate::native_wayland::handle_window_message(appwindow, message) else { return; }; From ddd270ef76c4aa9d601de435fe4b3ea0d471a600 Mon Sep 17 00:00:00 2001 From: lunar-seal Date: Thu, 27 Aug 2026 09:57:31 +0200 Subject: [PATCH 5/8] fix cors issues, refactor wayland implementation --- src/cef_impl/client/drag.rs | 33 +++++ src/native_wayland/drag.rs | 88 ++++++++++++ .../mod.rs} | 127 ++++-------------- src/native_wayland/webview.rs | 65 +++++++++ src/platform/linux/webview.rs | 49 ++----- src/runtime.rs | 41 +++++- src/webview.rs | 48 ++++--- src/window.rs | 4 + 8 files changed, 296 insertions(+), 159 deletions(-) create mode 100644 src/native_wayland/drag.rs rename src/{native_wayland.rs => native_wayland/mod.rs} (82%) create mode 100644 src/native_wayland/webview.rs diff --git a/src/cef_impl/client/drag.rs b/src/cef_impl/client/drag.rs index ed98a61..a898e42 100644 --- a/src/cef_impl/client/drag.rs +++ b/src/cef_impl/client/drag.rs @@ -220,6 +220,39 @@ pub(crate) fn event_from_script_event( } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn file_drop_follows_tauri_event_sequence() { + let state = Arc::new(Mutex::new(DragDropState { + paths: Some(vec![PathBuf::from("/tmp/file")]), + native_entered: true, + entered: false, + })); + let event = |kind: &str| DragDropScriptEvent { + kind: kind.into(), + x: 4.0, + y: 8.0, + }; + + assert!(matches!( + event_from_script_event(&state, event("enter")), + Some(DragDropEvent::Enter { .. }) + )); + assert!(matches!( + event_from_script_event(&state, event("over")), + Some(DragDropEvent::Over { .. }) + )); + assert!(matches!( + event_from_script_event(&state, event("drop")), + Some(DragDropEvent::Drop { .. }) + )); + assert!(!state.lock().unwrap().native_entered); + } +} + wrap_resource_request_handler! { pub(crate) struct WebDragDropResourceRequestHandler { context: RuntimeContext, diff --git a/src/native_wayland/drag.rs b/src/native_wayland/drag.rs new file mode 100644 index 0000000..1a7a84a --- /dev/null +++ b/src/native_wayland/drag.rs @@ -0,0 +1,88 @@ +use std::sync::{Arc, Mutex}; + +use cef::{DraggableRegion, ImplWindow, Window}; +use tauri_runtime::webview::InitializationScript; + +use crate::cef_impl::client::DraggableRegionsChanged; + +const DRAG_REGION_SCRIPT: &str = r#" +(() => { + addEventListener("DOMContentLoaded", () => { + const style = document.createElement("style"); + const nonce = document.querySelector("style[nonce], script[nonce]")?.nonce; + if (nonce) style.nonce = nonce; + style.textContent = ` + [data-tauri-drag-region]:not([data-tauri-drag-region="false"]) { + -webkit-app-region: drag; + } + [data-tauri-drag-region]:not([data-tauri-drag-region="deep"]) * { + -webkit-app-region: no-drag; + } + [data-tauri-drag-region="false"], + [data-tauri-drag-region="deep"] :is(a, button, input, select, textarea, label, summary, + [contenteditable]:not([contenteditable="false"]), [tabindex]:not([tabindex="-1"]), + [role="button"], [role="link"], [role="menuitem"], [role="tab"], [role="checkbox"], + [role="radio"], [role="switch"], [role="option"]):not([data-tauri-drag-region]) { + -webkit-app-region: no-drag; + } + `; + (document.head || document.documentElement).append(style); + }, { once: true }); +})(); +"#; + +pub(crate) fn drag_region_initialization_script() -> InitializationScript { + InitializationScript { + script: DRAG_REGION_SCRIPT.to_string(), + for_main_frame_only: true, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn drag_region_style_waits_for_and_reuses_the_csp_nonce() { + let script = drag_region_initialization_script(); + assert!(script.for_main_frame_only); + assert!(script.script.contains("DOMContentLoaded")); + assert!(script.script.contains("style.nonce = nonce")); + } +} + +#[derive(Default)] +struct State { + window: Option, + regions: Vec, +} + +#[derive(Clone, Default)] +pub(super) struct DraggableRegions(Arc>); + +impl DraggableRegions { + pub(super) fn attach(&self, window: Window) { + let regions = { + let mut state = self.0.lock().unwrap(); + state.window = Some(window.clone()); + state.regions.clone() + }; + window.set_draggable_regions((!regions.is_empty()).then_some(regions.as_slice())); + } + + fn set(&self, regions: Option<&[DraggableRegion]>) { + let (window, regions) = { + let mut state = self.0.lock().unwrap(); + state.regions = regions.unwrap_or_default().to_vec(); + (state.window.clone(), state.regions.clone()) + }; + if let Some(window) = window { + window.set_draggable_regions((!regions.is_empty()).then_some(regions.as_slice())); + } + } + + pub(super) fn changed_handler(&self) -> DraggableRegionsChanged { + let regions = self.clone(); + Arc::new(move |changed| regions.set(changed)) + } +} diff --git a/src/native_wayland.rs b/src/native_wayland/mod.rs similarity index 82% rename from src/native_wayland.rs rename to src/native_wayland/mod.rs index 879a2a8..f65040f 100644 --- a/src/native_wayland.rs +++ b/src/native_wayland/mod.rs @@ -1,12 +1,12 @@ -// Copyright 2019-2024 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -//! Native Wayland top-level window backed by CEF Views. +//! Use the CEF Views Framework to create a native Wayland window. +//! This is not embeddable into a native window owned by us. //! -//! CEF cannot embed a native browser child into a foreign Wayland window. The -//! supported native path is the one used by `cefsimple`: CEF owns the -//! `CefWindow`, with a `CefBrowserView` filling it. +//! Reference: https://github.com/chromiumembedded/cef/tree/master/tests/cefsimple + +mod drag; +mod webview; + +pub(crate) use drag::drag_region_initialization_script; use std::sync::{ Arc, Mutex, @@ -17,7 +17,6 @@ use cef::{rc::Rc, *}; use tauri_runtime::{ Error, UserEvent, dpi::{PhysicalPosition, PhysicalSize, Size as TauriSize}, - webview::InitializationScript, window::WindowId, }; use winit::window::WindowLevel; @@ -27,69 +26,11 @@ use crate::{ window::{AppWindow, AppWindowAttrs, WindowMessage}, }; +use drag::DraggableRegions; + type BrowserCreated = Box; type Emit = Arc; -const DRAG_REGION_SCRIPT: &str = r#" -(() => { - const style = document.createElement("style"); - style.textContent = ` - [data-tauri-drag-region]:not([data-tauri-drag-region="false"]) { - -webkit-app-region: drag; - } - [data-tauri-drag-region]:not([data-tauri-drag-region="deep"]) * { - -webkit-app-region: no-drag; - } - [data-tauri-drag-region="false"], - [data-tauri-drag-region="deep"] :is(a, button, input, select, textarea, label, summary, - [contenteditable]:not([contenteditable="false"]), [tabindex]:not([tabindex="-1"]), - [role="button"], [role="link"], [role="menuitem"], [role="tab"], [role="checkbox"], - [role="radio"], [role="switch"], [role="option"]):not([data-tauri-drag-region]) { - -webkit-app-region: no-drag; - } - `; - (document.head || document.documentElement).append(style); -})(); -"#; - -pub(crate) fn drag_region_initialization_script() -> InitializationScript { - InitializationScript { - script: DRAG_REGION_SCRIPT.to_string(), - for_main_frame_only: false, - } -} - -#[derive(Default)] -struct DraggableRegionsState { - window: Option, - regions: Vec, -} - -#[derive(Clone, Default)] -struct DraggableRegions(Arc>); - -impl DraggableRegions { - fn attach(&self, window: Window) { - let regions = { - let mut state = self.0.lock().unwrap(); - state.window = Some(window.clone()); - state.regions.clone() - }; - window.set_draggable_regions((!regions.is_empty()).then_some(regions.as_slice())); - } - - fn set(&self, regions: Option<&[DraggableRegion]>) { - let (window, regions) = { - let mut state = self.0.lock().unwrap(); - state.regions = regions.unwrap_or_default().to_vec(); - (state.window.clone(), state.regions.clone()) - }; - if let Some(window) = window { - window.set_draggable_regions((!regions.is_empty()).then_some(regions.as_slice())); - } - } -} - #[derive(Clone)] struct WindowState { resizable: bool, @@ -190,8 +131,7 @@ impl WindowConfig { pub(crate) fn draggable_regions_changed( &self, ) -> crate::cef_impl::client::DraggableRegionsChanged { - let draggable_regions = self.draggable_regions.clone(); - Arc::new(move |regions| draggable_regions.set(regions)) + self.draggable_regions.changed_handler() } pub(crate) fn is_frameless(&self) -> bool { @@ -199,42 +139,45 @@ impl WindowConfig { } } -#[derive(Clone)] pub(crate) struct NativeWindow { - pub(crate) window: Window, - pub(crate) browser_view: BrowserView, + window: Window, + browser_view: BrowserView, allow_close: Arc, state: Arc>, } impl NativeWindow { - pub(crate) fn force_close(&self) { + fn force_close(&self) { self.allow_close.store(true, Ordering::Release); self.window.close(); } - pub(crate) fn scale_factor(&self) -> f64 { + fn scale_factor(&self) -> f64 { scale_factor(&self.window) } - pub(crate) fn physical_bounds(&self) -> (PhysicalPosition, PhysicalSize) { + fn physical_bounds(&self) -> (PhysicalPosition, PhysicalSize) { physical_bounds(self.window.bounds_in_screen()) } - pub(crate) fn physical_inner_size(&self) -> PhysicalSize { + fn physical_inner_size(&self) -> PhysicalSize { physical_bounds(self.window.client_area_bounds_in_screen()).1 } } +impl AppWindow { + pub(crate) fn close_native_wayland(&self) { + if let Some(native) = &self.native_wayland { + native.force_close(); + } + } +} + pub(crate) fn handle_window_message( appwindow: &mut AppWindow, message: WindowMessage, ) -> Option { - let native = appwindow - .children - .first() - .and_then(|child| child.native_wayland.clone()); - let Some(native) = native else { + let Some(native) = appwindow.native_wayland.as_ref() else { return Some(message); }; let window = &native.window; @@ -413,9 +356,6 @@ wrap_browser_view_delegate! { let (Some(browser_view), Some(browser)) = (browser_view, browser) else { return; }; - // Chrome-style Views creates its internal WebView after applying defaults. - // Reapply the color now so CEF also uses it behind clipped resize frames. - browser_view.set_background_color(browser_view.background_color()); 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"); @@ -639,18 +579,3 @@ fn view_scale_factor(view: Option<&mut View>, fallback: f64) -> f64 { .map(scale_factor) .unwrap_or(fallback) } - -#[cfg(test)] -mod tests { - use super::*; - use tauri_runtime::dpi::LogicalSize; - - #[test] - fn cef_sizes_stay_in_device_independent_pixels() { - let physical = cef_size(PhysicalSize::new(300, 180), 1.5); - assert_eq!((physical.width, physical.height), (200, 120)); - - let logical = cef_size_from_tauri(TauriSize::Logical(LogicalSize::new(200.0, 120.0)), 1.5); - assert_eq!((logical.width, logical.height), (200, 120)); - } -} diff --git a/src/native_wayland/webview.rs b/src/native_wayland/webview.rs new file mode 100644 index 0000000..4ebe68d --- /dev/null +++ b/src/native_wayland/webview.rs @@ -0,0 +1,65 @@ +use cef::{BrowserView, ImplView, browser_view_get_for_browser}; +use tauri_runtime::dpi::{PhysicalPosition, PhysicalSize, Rect}; +use tauri_utils::config::Color; + +use crate::webview::AppWebview; + +use super::scale_factor; + +impl AppWebview { + fn native_wayland_browser_view(&self) -> Option { + browser_view_get_for_browser(Some(&mut self.browser.clone())) + } + + pub(crate) fn native_wayland_set_background_color(&self, color: Option) { + let Some(view) = self.native_wayland_browser_view() else { + return; + }; + let (r, g, b, a) = color.unwrap_or_default().into(); + view + .set_background_color(((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | b as u32); + } + + pub(crate) fn native_wayland_bounds(&self) -> Option { + let view = self.native_wayland_browser_view()?; + let scale = view.window().as_ref().map(scale_factor).unwrap_or(1.0); + let bounds = view.bounds(); + Some(Rect { + position: PhysicalPosition::new( + (bounds.x as f64 * scale).round() as i32, + (bounds.y as f64 * scale).round() as i32, + ) + .into(), + size: PhysicalSize::new( + (bounds.width.max(0) as f64 * scale).round() as u32, + (bounds.height.max(0) as f64 * scale).round() as u32, + ) + .into(), + }) + } + + pub(crate) fn native_wayland_take_input_focus(&self) { + if let Some(view) = self.native_wayland_browser_view() { + view.request_focus(); + } + } + + pub(crate) fn native_wayland_set_visible(&self, visible: bool) { + if let Some(view) = self.native_wayland_browser_view() { + view.set_visible(i32::from(visible)); + } + } + + pub(crate) fn native_wayland_set_bounds(&self, x: i32, y: i32, width: i32, height: i32) { + let Some(view) = self.native_wayland_browser_view() else { + return; + }; + let scale = view.window().as_ref().map(scale_factor).unwrap_or(1.0); + view.set_bounds(Some(&cef::Rect { + x: (x as f64 / scale).round() as i32, + y: (y as f64 / scale).round() as i32, + width: (width.max(1) as f64 / scale).round() as i32, + height: (height.max(1) as f64 / scale).round() as i32, + })); + } +} diff --git a/src/platform/linux/webview.rs b/src/platform/linux/webview.rs index 821320a..a3b4fae 100644 --- a/src/platform/linux/webview.rs +++ b/src/platform/linux/webview.rs @@ -3,8 +3,6 @@ // SPDX-License-Identifier: MIT use cef::ImplBrowserHost; -#[cfg(feature = "native-wayland")] -use cef::ImplView; use std::os::raw::c_ulong; use tauri_runtime::dpi::{PhysicalPosition, PhysicalSize, Rect}; use tauri_utils::config::Color; @@ -23,11 +21,8 @@ impl AppWebview { pub(crate) fn set_background_color(&self, color: Option) { #[cfg(feature = "native-wayland")] - if let Some(native) = &self.native_wayland { - let (r, g, b, a) = color.unwrap_or_default().into(); - native.browser_view.set_background_color( - ((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | b as u32, - ); + if crate::config::native_wayland() { + self.native_wayland_set_background_color(color); return; } let _ = (self, color); @@ -37,21 +32,8 @@ impl AppWebview { pub(crate) fn bounds(&self) -> Option { #[cfg(feature = "native-wayland")] - if let Some(native) = &self.native_wayland { - let bounds = native.browser_view.bounds(); - let scale = native.scale_factor(); - return Some(Rect { - position: PhysicalPosition::new( - (bounds.x as f64 * scale).round() as i32, - (bounds.y as f64 * scale).round() as i32, - ) - .into(), - size: PhysicalSize::new( - (bounds.width.max(0) as f64 * scale).round() as u32, - (bounds.height.max(0) as f64 * scale).round() as u32, - ) - .into(), - }); + if crate::config::native_wayland() { + return self.native_wayland_bounds(); } let xid = self.xid(); @@ -94,8 +76,8 @@ impl AppWebview { /// `CefWindowX11::Focus`; Chrome-style child windows have no equivalent. pub(crate) fn take_input_focus(&self) { #[cfg(feature = "native-wayland")] - if let Some(native) = &self.native_wayland { - native.browser_view.request_focus(); + if crate::config::native_wayland() { + self.native_wayland_take_input_focus(); return; } let xid = self.xid(); @@ -115,7 +97,7 @@ impl AppWebview { pub(crate) fn reparent(&self, parent: &AppWindow) { #[cfg(feature = "native-wayland")] - if self.native_wayland.is_some() { + if crate::config::native_wayland() { return; } let xid = self.xid(); @@ -129,8 +111,8 @@ impl AppWebview { pub(crate) fn apply_visible(&self, visible: bool) { #[cfg(feature = "native-wayland")] - if let Some(native) = &self.native_wayland { - native.browser_view.set_visible(i32::from(visible)); + if crate::config::native_wayland() { + self.native_wayland_set_visible(visible); return; } let xid = self.xid(); @@ -170,8 +152,7 @@ impl AppWebview { pub(crate) fn destroy_native(&self) { #[cfg(feature = "native-wayland")] - if let Some(native) = &self.native_wayland { - native.force_close(); + if crate::config::native_wayland() { return; } let xid = self.xid(); @@ -183,14 +164,8 @@ impl AppWebview { pub(crate) fn apply_physical_bounds(&self, _scale: f64, x: i32, y: i32, width: i32, height: i32) { #[cfg(feature = "native-wayland")] - if let Some(native) = &self.native_wayland { - let scale = native.scale_factor(); - native.browser_view.set_bounds(Some(&cef::Rect { - x: (x as f64 / scale).round() as i32, - y: (y as f64 / scale).round() as i32, - width: (width.max(1) as f64 / scale).round() as i32, - height: (height.max(1) as f64 / scale).round() as i32, - })); + if crate::config::native_wayland() { + self.native_wayland_set_bounds(x, y, width, height); return; } let xid = self.xid(); diff --git a/src/runtime.rs b/src/runtime.rs index f82dc3f..57d403b 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -407,6 +407,8 @@ pub(crate) struct WinitCefApp { target_os = "openbsd" ))] last_focus_probe: Option, + #[cfg(target_os = "linux")] + last_slow_cef_tick_log: Option, } /// Stands in for the scheduling callbacks `external_message_pump` would provide. @@ -419,6 +421,12 @@ pub(crate) struct WinitCefApp { ))] const CEF_WORK_INTERVAL: std::time::Duration = std::time::Duration::from_millis(4); +#[cfg(target_os = "linux")] +const SLOW_CEF_TICK: std::time::Duration = std::time::Duration::from_millis(8); + +#[cfg(target_os = "linux")] +const SLOW_CEF_TICK_LOG_INTERVAL: std::time::Duration = std::time::Duration::from_secs(1); + /// Probing costs blocking X round-trips and focus is not that time-sensitive. #[cfg(any( target_os = "linux", @@ -457,6 +465,8 @@ impl WinitCefApp { target_os = "openbsd" ))] last_focus_probe: None, + #[cfg(target_os = "linux")] + last_slow_cef_tick_log: None, } } @@ -873,6 +883,8 @@ impl WinitCefApp { self.remove_scheme_handler_entries(child); child.close(); } + #[cfg(all(target_os = "linux", feature = "native-wayland"))] + appwindow.close_native_wayland(); self.exit_if_done(event_loop); } @@ -922,6 +934,8 @@ impl WinitCefApp { self.remove_scheme_handler_entries(child); child.close(); } + #[cfg(all(target_os = "linux", feature = "native-wayland"))] + appwindow.close_native_wayland(); } self.state.windows.clear(); self.state.winid_id_to_window_id_map.clear(); @@ -963,13 +977,38 @@ impl WinitCefApp { target_os = "netbsd", target_os = "openbsd" ))] - fn service_glib(&self, event_loop: &dyn ActiveEventLoop) { + fn service_glib(&mut self, event_loop: &dyn ActiveEventLoop) { + let tick_started = std::time::Instant::now(); let context = gtk::glib::MainContext::default(); + let mut glib_iterations = 0; while context.pending() { context.iteration(false); + glib_iterations += 1; } + let glib_elapsed = tick_started.elapsed(); + let cef_started = std::time::Instant::now(); cef::do_message_loop_work(); + let cef_elapsed = cef_started.elapsed(); + + #[cfg(target_os = "linux")] + { + let tick_elapsed = tick_started.elapsed(); + let now = std::time::Instant::now(); + if crate::config::native_wayland() + && tick_elapsed >= SLOW_CEF_TICK + && self + .last_slow_cef_tick_log + .is_none_or(|last| now.duration_since(last) >= SLOW_CEF_TICK_LOG_INTERVAL) + { + self.last_slow_cef_tick_log = Some(now); + log::warn!( + "slow native Wayland CEF tick: total={tick_elapsed:?}, glib={glib_elapsed:?} \ + ({glib_iterations} iterations), cef={cef_elapsed:?}" + ); + } + } + event_loop.set_control_flow(winit::event_loop::ControlFlow::WaitUntil( std::time::Instant::now() + CEF_WORK_INTERVAL, )); diff --git a/src/webview.rs b/src/webview.rs index 48ff4af..2ccb09b 100644 --- a/src/webview.rs +++ b/src/webview.rs @@ -198,8 +198,12 @@ pub(crate) struct AppWebview { pub(crate) devtools_observer_registration: Arc>>, pub(crate) listeners: WebviewEventListeners, pub(crate) bounds_rate: Option, +} + +struct BrowserChild { + webview: AppWebview, #[cfg(all(target_os = "linux", feature = "native-wayland"))] - pub(crate) native_wayland: Option, + native_wayland: Option, } impl AppWebview { @@ -335,7 +339,11 @@ impl WinitCefApp { }; *live_browsers += 1; - appwindow.children.push(child); + #[cfg(all(target_os = "linux", feature = "native-wayland"))] + { + appwindow.native_wayland = child.native_wayland; + } + appwindow.children.push(child.webview); layout_app_window(appwindow); // No winit focus event is coming for a window that is already focused. if appwindow.reported_focus @@ -347,7 +355,7 @@ impl WinitCefApp { Ok(()) } - pub(crate) fn build_browser_child( + fn build_browser_child( context: &RuntimeContext, scheme_registry: &request_handler::SchemeRegistry, window_id: WindowId, @@ -361,7 +369,7 @@ impl WinitCefApp { crate::native_wayland::WindowConfig, >, mut pending: PendingWebview>, - ) -> Option { + ) -> Option { let bounds_rate = compute_child_bounds_rate( pending.webview_attributes.bounds.as_ref(), pending.webview_attributes.auto_resize, @@ -535,17 +543,19 @@ impl WinitCefApp { ); browser_tx - .send(AppWebview { - webview_id, - label, - browser, - browser_id, - host, - uri_scheme_protocols, - devtools_protocol_handlers, - devtools_observer_registration, - listeners: Default::default(), - bounds_rate, + .send(BrowserChild { + webview: AppWebview { + webview_id, + label, + browser, + browser_id, + host, + uri_scheme_protocols, + devtools_protocol_handlers, + devtools_observer_registration, + listeners: Default::default(), + bounds_rate, + }, #[cfg(all(target_os = "linux", feature = "native-wayland"))] native_wayland, }) @@ -694,6 +704,8 @@ impl WinitCefApp { // runs. Leaving it attached leaks the renderer; letting CEF forward a // close to its top-level parent can close the whole Tauri window. child.close(); + #[cfg(all(target_os = "linux", feature = "native-wayland"))] + appwindow.close_native_wayland(); } WebviewMessage::SetBounds(bounds) => { let parent_size = appwindow.window.surface_size(); @@ -1314,11 +1326,7 @@ impl WebviewDispatch for CefWebviewDispatcher { /// they were last given. pub(crate) fn layout_app_window(appwindow: &AppWindow) { #[cfg(all(target_os = "linux", feature = "native-wayland"))] - if appwindow - .children - .first() - .is_some_and(|child| child.native_wayland.is_some()) - { + if appwindow.native_wayland.is_some() { return; } let parent_size = appwindow.window.surface_size(); diff --git a/src/window.rs b/src/window.rs index ed081c8..0a0568c 100644 --- a/src/window.rs +++ b/src/window.rs @@ -328,6 +328,8 @@ pub(crate) struct AppWindow { pub(crate) window: Box, pub(crate) attrs: AppWindowAttrs, pub(crate) children: Vec, + #[cfg(all(target_os = "linux", feature = "native-wayland"))] + pub(crate) native_wayland: Option, pub(crate) listeners: WindowEventListeners, /// Last focus state reported to Tauri. See `WinitCefApp::sync_window_focus`. pub(crate) reported_focus: bool, @@ -456,6 +458,8 @@ impl WinitCefApp { window, attrs, children: Vec::new(), + #[cfg(all(target_os = "linux", feature = "native-wayland"))] + native_wayland: None, listeners: Default::default(), reported_focus: false, #[cfg(target_os = "macos")] From aad7f999a9bcd9af10444b63af224dce35f5e786 Mon Sep 17 00:00:00 2001 From: lunar-seal Date: Thu, 27 Aug 2026 17:19:26 +0200 Subject: [PATCH 6/8] refactor more - remove winit/x11 references - sanity check implemented functionality - limit impact on X11 backend to more or less zero --- Cargo.toml | 5 - flake.nix | 17 - src/cef_impl/client/drag.rs | 47 --- src/cef_impl/client/mod.rs | 14 +- src/cef_impl/request_context.rs | 23 +- src/config.rs | 13 +- src/lib.rs | 4 +- src/native_wayland/drag.rs | 88 ----- src/native_wayland/mod.rs | 581 -------------------------------- src/native_wayland/webview.rs | 65 ---- src/platform/linux/utils.rs | 13 +- src/platform/linux/webview.rs | 32 -- src/runtime.rs | 186 ++++------ src/wayland/mod.rs | 475 ++++++++++++++++++++++++++ src/wayland/webview.rs | 309 +++++++++++++++++ src/wayland/window.rs | 513 ++++++++++++++++++++++++++++ src/webview.rs | 215 ++++-------- src/window.rs | 46 +-- 18 files changed, 1433 insertions(+), 1213 deletions(-) delete mode 100644 src/native_wayland/drag.rs delete mode 100644 src/native_wayland/mod.rs delete mode 100644 src/native_wayland/webview.rs create mode 100644 src/wayland/mod.rs create mode 100644 src/wayland/webview.rs create mode 100644 src/wayland/window.rs diff --git a/Cargo.toml b/Cargo.toml index 65939da..1fccfbf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,11 +97,6 @@ x11-dl = "2.21" default = ["sandbox"] devtools = [] macos-private-api = ["tauri-runtime/macos-private-api"] -# Linux CEF Views/Ozone-Wayland windowing path (`LinuxWindowing::Wayland`). -# Off by default: unlike the X11 embedding path, it owns a single top-level -# window with a single full-window webview, has no raw window handles, and -# can't change decorations at runtime. Opt in once an app needs it. -native-wayland = [] sandbox = ["cef/sandbox"] [dev-dependencies] diff --git a/flake.nix b/flake.nix index 42ff4fc..ebc7dcd 100644 --- a/flake.nix +++ b/flake.nix @@ -1,6 +1,4 @@ { - description = "Rust Rover environment"; - inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; rust-overlay.url = "github:oxalica/rust-overlay"; @@ -32,21 +30,6 @@ rust-analyzer ]; - - shellHook = '' - # cef-dll-sys preserves read-only archive modes, then overwrites - # those files when its build script changes. - if [ -d target/debug ]; then - find target/debug -maxdepth 2 -type f ! -perm -u=w -exec chmod u+w {} + - fi - - mkdir -p ~/.rust-rover/toolchain - - ln -sfn ${rustToolchain}/lib ~/.rust-rover/toolchain - ln -sfn ${rustToolchain}/bin ~/.rust-rover/toolchain - - export RUST_SRC_PATH="$HOME/.rust-rover/toolchain/lib/rustlib/src/rust/library" - ''; }; } ); diff --git a/src/cef_impl/client/drag.rs b/src/cef_impl/client/drag.rs index a898e42..fd493a2 100644 --- a/src/cef_impl/client/drag.rs +++ b/src/cef_impl/client/drag.rs @@ -18,8 +18,6 @@ use url::Url; use crate::runtime::{Message, RuntimeContext}; -pub(crate) type DraggableRegionsChanged = Arc)>; - const DRAG_DROP_BRIDGE_PATH: &str = "/__tauri_cef_drag_drop__"; const DRAG_DROP_INIT_SCRIPT: &str = r#" @@ -143,7 +141,6 @@ fn collect_drag_data_paths(drag_data: &mut DragData) -> Vec { wrap_drag_handler! { pub struct TauriCefDragHandler { drag_drop_state: Arc>, - draggable_regions_changed: Option, } impl DragHandler { @@ -164,17 +161,6 @@ wrap_drag_handler! { // report over/drop/leave with accurate viewport positions. 0 } - - fn on_draggable_regions_changed( - &self, - _browser: Option<&mut Browser>, - _frame: Option<&mut Frame>, - regions: Option<&[DraggableRegion]>, - ) { - if let Some(handler) = &self.draggable_regions_changed { - handler(regions); - } - } } } @@ -220,39 +206,6 @@ pub(crate) fn event_from_script_event( } } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn file_drop_follows_tauri_event_sequence() { - let state = Arc::new(Mutex::new(DragDropState { - paths: Some(vec![PathBuf::from("/tmp/file")]), - native_entered: true, - entered: false, - })); - let event = |kind: &str| DragDropScriptEvent { - kind: kind.into(), - x: 4.0, - y: 8.0, - }; - - assert!(matches!( - event_from_script_event(&state, event("enter")), - Some(DragDropEvent::Enter { .. }) - )); - assert!(matches!( - event_from_script_event(&state, event("over")), - Some(DragDropEvent::Over { .. }) - )); - assert!(matches!( - event_from_script_event(&state, event("drop")), - Some(DragDropEvent::Drop { .. }) - )); - assert!(!state.lock().unwrap().native_entered); - } -} - wrap_resource_request_handler! { pub(crate) struct WebDragDropResourceRequestHandler { context: RuntimeContext, diff --git a/src/cef_impl/client/mod.rs b/src/cef_impl/client/mod.rs index 9b2efe5..b1993f2 100644 --- a/src/cef_impl/client/mod.rs +++ b/src/cef_impl/client/mod.rs @@ -28,8 +28,8 @@ use display::TauriCefDisplayHandler; use download::TauriCefDownloadHandler; use drag::TauriCefDragHandler; pub(crate) use drag::{ - DragDropEventTarget, DragDropScriptEvent, DragDropState, DraggableRegionsChanged, - WebDragDropResourceRequestHandler, drag_drop_initialization_script, event_from_script_event, + DragDropEventTarget, DragDropScriptEvent, DragDropState, WebDragDropResourceRequestHandler, + drag_drop_initialization_script, event_from_script_event, }; use keyboard::TauriCefKeyboardHandler; use life_span::TauriCefChildLifeSpanHandler; @@ -75,7 +75,6 @@ wrap_client! { drag_drop_event_target: DragDropEventTarget, drag_drop_handler_enabled: bool, drag_drop_state: Arc>, - draggable_regions_changed: Option, pub(crate) handlers: TauriCefBrowserClientHandlers, proxy: WinitEventLoopProxy, sender: Sender>, @@ -83,12 +82,9 @@ wrap_client! { impl Client { fn drag_handler(&self) -> Option { - (self.drag_drop_handler_enabled || self.draggable_regions_changed.is_some()).then(|| { - TauriCefDragHandler::new( - self.drag_drop_state.clone(), - self.draggable_regions_changed.clone(), - ) - }) + self + .drag_drop_handler_enabled + .then(|| TauriCefDragHandler::new(self.drag_drop_state.clone())) } fn request_handler(&self) -> Option { diff --git a/src/cef_impl/request_context.rs b/src/cef_impl/request_context.rs index 3caa2f9..608ddf6 100644 --- a/src/cef_impl/request_context.rs +++ b/src/cef_impl/request_context.rs @@ -8,7 +8,6 @@ use std::{ sync::{ Arc, Mutex, atomic::{AtomicBool, Ordering}, - mpsc::{Receiver, TryRecvError}, }, time::Duration, }; @@ -172,24 +171,6 @@ pub(crate) fn wait_for_deferred_init(flag: &Arc) { } } -/// Wait for an asynchronously-created CEF object while continuing to service -/// the CEF UI thread. Views creates its browser only after the BrowserView is -/// attached to a Window, unlike `browser_host_create_browser_sync`. -pub(crate) fn wait_for_deferred_result(receiver: &Receiver) -> Option { - if cef::currently_on(cef::sys::cef_thread_id_t::TID_UI.into()) == 0 { - return receiver.recv().ok(); - } - - let _allow = AllowNestableTasks::enter(); - loop { - match receiver.try_recv() { - Ok(value) => return Some(value), - Err(TryRecvError::Disconnected) => return None, - Err(TryRecvError::Empty) => cef::do_message_loop_work(), - } - } -} - /// RAII guard that scopes `CefSetNestableTasksAllowed(true)` for the current /// CEF UI-thread call. /// @@ -199,10 +180,10 @@ pub(crate) fn wait_for_deferred_result(receiver: &Receiver) -> Option { /// [`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 124d860..dac743c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -28,9 +28,7 @@ use std::sync::OnceLock; pub enum LinuxWindowing { #[default] X11, - /// A CEF Views-owned top-level window using Ozone/Wayland. Requires the - /// `native-wayland` crate feature. - #[cfg(feature = "native-wayland")] + /// A CEF Views-owned top-level window using Ozone/Wayland. Wayland, } @@ -93,12 +91,5 @@ pub(crate) fn config() -> &'static CefConfig { #[cfg(target_os = "linux")] pub(crate) fn native_wayland() -> bool { - #[cfg(feature = "native-wayland")] - { - config().linux_windowing == LinuxWindowing::Wayland - } - #[cfg(not(feature = "native-wayland"))] - { - false - } + config().linux_windowing == LinuxWindowing::Wayland } diff --git a/src/lib.rs b/src/lib.rs index a7c7605..2619a31 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,12 +9,12 @@ mod cef_impl; mod compat; mod config; mod external_message_pump; -#[cfg(all(target_os = "linux", feature = "native-wayland"))] -mod native_wayland; mod platform; mod policy; mod runtime; mod streaming; +#[cfg(target_os = "linux")] +mod wayland; mod webview; mod window; mod window_builder; diff --git a/src/native_wayland/drag.rs b/src/native_wayland/drag.rs deleted file mode 100644 index 1a7a84a..0000000 --- a/src/native_wayland/drag.rs +++ /dev/null @@ -1,88 +0,0 @@ -use std::sync::{Arc, Mutex}; - -use cef::{DraggableRegion, ImplWindow, Window}; -use tauri_runtime::webview::InitializationScript; - -use crate::cef_impl::client::DraggableRegionsChanged; - -const DRAG_REGION_SCRIPT: &str = r#" -(() => { - addEventListener("DOMContentLoaded", () => { - const style = document.createElement("style"); - const nonce = document.querySelector("style[nonce], script[nonce]")?.nonce; - if (nonce) style.nonce = nonce; - style.textContent = ` - [data-tauri-drag-region]:not([data-tauri-drag-region="false"]) { - -webkit-app-region: drag; - } - [data-tauri-drag-region]:not([data-tauri-drag-region="deep"]) * { - -webkit-app-region: no-drag; - } - [data-tauri-drag-region="false"], - [data-tauri-drag-region="deep"] :is(a, button, input, select, textarea, label, summary, - [contenteditable]:not([contenteditable="false"]), [tabindex]:not([tabindex="-1"]), - [role="button"], [role="link"], [role="menuitem"], [role="tab"], [role="checkbox"], - [role="radio"], [role="switch"], [role="option"]):not([data-tauri-drag-region]) { - -webkit-app-region: no-drag; - } - `; - (document.head || document.documentElement).append(style); - }, { once: true }); -})(); -"#; - -pub(crate) fn drag_region_initialization_script() -> InitializationScript { - InitializationScript { - script: DRAG_REGION_SCRIPT.to_string(), - for_main_frame_only: true, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn drag_region_style_waits_for_and_reuses_the_csp_nonce() { - let script = drag_region_initialization_script(); - assert!(script.for_main_frame_only); - assert!(script.script.contains("DOMContentLoaded")); - assert!(script.script.contains("style.nonce = nonce")); - } -} - -#[derive(Default)] -struct State { - window: Option, - regions: Vec, -} - -#[derive(Clone, Default)] -pub(super) struct DraggableRegions(Arc>); - -impl DraggableRegions { - pub(super) fn attach(&self, window: Window) { - let regions = { - let mut state = self.0.lock().unwrap(); - state.window = Some(window.clone()); - state.regions.clone() - }; - window.set_draggable_regions((!regions.is_empty()).then_some(regions.as_slice())); - } - - fn set(&self, regions: Option<&[DraggableRegion]>) { - let (window, regions) = { - let mut state = self.0.lock().unwrap(); - state.regions = regions.unwrap_or_default().to_vec(); - (state.window.clone(), state.regions.clone()) - }; - if let Some(window) = window { - window.set_draggable_regions((!regions.is_empty()).then_some(regions.as_slice())); - } - } - - pub(super) fn changed_handler(&self) -> DraggableRegionsChanged { - let regions = self.clone(); - Arc::new(move |changed| regions.set(changed)) - } -} diff --git a/src/native_wayland/mod.rs b/src/native_wayland/mod.rs deleted file mode 100644 index f65040f..0000000 --- a/src/native_wayland/mod.rs +++ /dev/null @@ -1,581 +0,0 @@ -//! Use the CEF Views Framework to create a native Wayland window. -//! This is not embeddable into a native window owned by us. -//! -//! Reference: https://github.com/chromiumembedded/cef/tree/master/tests/cefsimple - -mod drag; -mod webview; - -pub(crate) use drag::drag_region_initialization_script; - -use std::sync::{ - Arc, Mutex, - atomic::{AtomicBool, Ordering}, -}; - -use cef::{rc::Rc, *}; -use tauri_runtime::{ - Error, UserEvent, - dpi::{PhysicalPosition, PhysicalSize, Size as TauriSize}, - window::WindowId, -}; -use winit::window::WindowLevel; - -use crate::{ - runtime::{Message, RuntimeContext}, - window::{AppWindow, AppWindowAttrs, WindowMessage}, -}; - -use drag::DraggableRegions; - -type BrowserCreated = Box; -type Emit = Arc; - -#[derive(Clone)] -struct WindowState { - resizable: bool, - maximizable: bool, - minimizable: bool, - closable: bool, - min_size: Option, - max_size: Option, -} - -#[derive(Debug)] -pub(crate) enum Event { - CloseRequested, - Destroyed, - Focused(bool), - Resized(PhysicalSize), - ScaleFactorChanged { - scale_factor: f64, - new_inner_size: PhysicalSize, - }, -} - -#[derive(Clone, Copy)] -struct Geometry { - scale_factor: f64, - inner_size: PhysicalSize, -} - -#[derive(Clone)] -pub(crate) struct WindowConfig { - title: String, - bounds: Rect, - show_state: ShowState, - visible: bool, - frameless: bool, - always_on_top: bool, - initial_scale_factor: f64, - app_id: String, - emit: Emit, - state: Arc>, - draggable_regions: DraggableRegions, -} - -impl WindowConfig { - pub(crate) fn new( - context: &RuntimeContext, - window_id: WindowId, - attrs: &AppWindowAttrs, - size: PhysicalSize, - scale_factor: f64, - ) -> Self { - let sender = context.sender.clone(); - let proxy = context.proxy.clone(); - let emit = Arc::new(move |event| { - if sender - .send(Message::NativeWaylandWindow(window_id, event)) - .is_ok() - { - proxy.wake_up(); - } - }); - 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: (size.width as f64 / scale_factor).round().max(1.0) as i32, - height: (size.height as f64 / scale_factor).round().max(1.0) as i32, - }, - show_state, - visible: attrs.inner.visible, - frameless: !attrs.inner.decorations, - always_on_top: attrs.inner.window_level == WindowLevel::AlwaysOnTop, - initial_scale_factor: scale_factor, - app_id: crate::config::config().identifier.clone(), - emit, - state: Arc::new(Mutex::new(WindowState { - 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, - max_size: attrs.inner.max_surface_size, - })), - draggable_regions: DraggableRegions::default(), - } - } - - pub(crate) fn draggable_regions_changed( - &self, - ) -> crate::cef_impl::client::DraggableRegionsChanged { - self.draggable_regions.changed_handler() - } - - pub(crate) fn is_frameless(&self) -> bool { - self.frameless - } -} - -pub(crate) struct NativeWindow { - window: Window, - browser_view: BrowserView, - allow_close: Arc, - state: Arc>, -} - -impl NativeWindow { - fn force_close(&self) { - self.allow_close.store(true, Ordering::Release); - self.window.close(); - } - - fn scale_factor(&self) -> f64 { - scale_factor(&self.window) - } - - fn physical_bounds(&self) -> (PhysicalPosition, PhysicalSize) { - physical_bounds(self.window.bounds_in_screen()) - } - - fn physical_inner_size(&self) -> PhysicalSize { - physical_bounds(self.window.client_area_bounds_in_screen()).1 - } -} - -impl AppWindow { - pub(crate) fn close_native_wayland(&self) { - if let Some(native) = &self.native_wayland { - native.force_close(); - } - } -} - -pub(crate) fn handle_window_message( - appwindow: &mut AppWindow, - message: WindowMessage, -) -> Option { - let Some(native) = appwindow.native_wayland.as_ref() else { - return Some(message); - }; - let window = &native.window; - let scale = native.scale_factor(); - let (outer_position, outer_size) = native.physical_bounds(); - let buttons = appwindow.attrs.inner.enabled_buttons; - - match message { - WindowMessage::ScaleFactor(tx) => _ = tx.send(Ok(scale)), - WindowMessage::InnerPosition(tx) | WindowMessage::OuterPosition(tx) => { - _ = tx.send(Ok(outer_position)) - } - WindowMessage::InnerSize(tx) => _ = tx.send(Ok(native.physical_inner_size())), - WindowMessage::OuterSize(tx) => _ = tx.send(Ok(outer_size)), - WindowMessage::IsFullscreen(tx) => _ = tx.send(Ok(window.is_fullscreen() != 0)), - WindowMessage::IsMinimized(tx) => _ = tx.send(Ok(window.is_minimized() != 0)), - WindowMessage::IsMaximized(tx) => _ = tx.send(Ok(window.is_maximized() != 0)), - WindowMessage::IsFocused(tx) => _ = tx.send(Ok(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(window.is_visible() != 0)), - WindowMessage::IsEnabled(tx) => _ = tx.send(Ok(window.is_enabled() != 0)), - WindowMessage::IsAlwaysOnTop(tx) => _ = tx.send(Ok(window.is_always_on_top() != 0)), - WindowMessage::Title(tx) => { - let title = window.title(); - _ = tx.send(Ok(CefString::from(&title).to_string())) - } - WindowMessage::RawWindowHandle(tx) => _ = tx.send(Err(Error::FailedToSendMessage)), - WindowMessage::Center => window.center_window(Some(&cef_size(outer_size, scale))), - WindowMessage::RequestUserAttention(_) => {} - WindowMessage::SetEnabled(enabled) => window.set_enabled(i32::from(enabled)), - WindowMessage::SetResizable(resizable) => { - appwindow.attrs.inner.resizable = resizable; - native.state.lock().unwrap().resizable = resizable; - } - WindowMessage::SetMaximizable(enabled) => { - appwindow - .attrs - .inner - .enabled_buttons - .set(winit::window::WindowButtons::MAXIMIZE, enabled); - native.state.lock().unwrap().maximizable = enabled; - } - WindowMessage::SetMinimizable(enabled) => { - appwindow - .attrs - .inner - .enabled_buttons - .set(winit::window::WindowButtons::MINIMIZE, enabled); - native.state.lock().unwrap().minimizable = enabled; - } - WindowMessage::SetClosable(enabled) => { - appwindow - .attrs - .inner - .enabled_buttons - .set(winit::window::WindowButtons::CLOSE, enabled); - native.state.lock().unwrap().closable = enabled; - } - WindowMessage::SetTitle(title) => { - appwindow.attrs.inner.title = title.clone(); - window.set_title(Some(&CefString::from(title.as_str()))); - } - WindowMessage::Maximize => window.maximize(), - WindowMessage::Unmaximize | WindowMessage::Unminimize => window.restore(), - WindowMessage::Minimize => window.minimize(), - WindowMessage::Show => window.show(), - WindowMessage::Hide => window.hide(), - WindowMessage::SetDecorations(_) => {} - WindowMessage::SetAlwaysOnBottom(_) => {} - WindowMessage::SetAlwaysOnTop(on_top) => { - appwindow.attrs.inner.window_level = if on_top { - WindowLevel::AlwaysOnTop - } else { - WindowLevel::Normal - }; - window.set_always_on_top(i32::from(on_top)); - } - WindowMessage::SetVisibleOnAllWorkspaces(_) | WindowMessage::SetContentProtected(_) => {} - WindowMessage::SetSize(size) => window.set_size(Some(&cef_size_from_tauri(size, scale))), - WindowMessage::SetMinSize(size) => { - appwindow.attrs.inner.min_surface_size = size; - native.state.lock().unwrap().min_size = size; - } - WindowMessage::SetMaxSize(size) => { - appwindow.attrs.inner.max_surface_size = size; - native.state.lock().unwrap().max_size = size; - } - WindowMessage::SetSizeConstraints(constraints) => { - let min_size = - crate::window::paired_size_constraint(constraints.min_width, constraints.min_height); - let max_size = - crate::window::paired_size_constraint(constraints.max_width, constraints.max_height); - appwindow.attrs.inner.min_surface_size = min_size; - appwindow.attrs.inner.max_surface_size = max_size; - let mut state = native.state.lock().unwrap(); - state.min_size = min_size; - state.max_size = max_size; - } - WindowMessage::SetPosition(_position) => {} - WindowMessage::SetFullscreen(fullscreen) => window.set_fullscreen(i32::from(fullscreen)), - WindowMessage::SetFocus => { - window.activate(); - native.browser_view.request_focus(); - } - WindowMessage::SetFocusable(focusable) => { - native.browser_view.set_focusable(i32::from(focusable)); - } - 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::StartDragging - | WindowMessage::StartResizeDragging(_) - | WindowMessage::SetProgressBar(_) => {} - WindowMessage::SetBackgroundColor(color) => { - appwindow.attrs.background_color = color; - if let Some(child) = appwindow.children.first() { - child.set_background_color(color); - } - } - message => return Some(message), - } - None -} - -fn cef_size(size: PhysicalSize, scale: f64) -> Size { - Size { - width: (size.width as f64 / scale).round().max(1.0) as i32, - height: (size.height as f64 / scale).round().max(1.0) as i32, - } -} - -fn cef_size_from_tauri(size: TauriSize, scale: f64) -> Size { - cef_size(size.to_physical::(scale), scale) -} - -wrap_browser_view_delegate! { - struct NativeBrowserViewDelegate { - on_created: Arc>>, - allow_close: Arc, - state: Arc>, - draggable_regions: DraggableRegions, - } - - 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; - }; - self.draggable_regions.attach(window.clone()); - on_created( - browser.clone(), - NativeWindow { - window, - browser_view: browser_view.clone(), - allow_close: self.allow_close.clone(), - state: self.state.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, - geometry: 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 - .state - .lock() - .unwrap() - .min_size - .map(|size| cef_size_from_tauri(size, view_scale_factor(view, self.config.initial_scale_factor))) - .unwrap_or_default() - } - - fn maximum_size(&self, view: Option<&mut View>) -> Size { - self - .config - .state - .lock() - .unwrap() - .max_size - .map(|size| cef_size_from_tauri(size, view_scale_factor(view, 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()))); - window.set_always_on_top(i32::from(self.config.always_on_top)); - 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 on_window_bounds_changed(&self, window: Option<&mut Window>, _bounds: Option<&Rect>) { - let Some(window) = window else { return }; - let geometry = Geometry { - scale_factor: scale_factor(window), - inner_size: physical_bounds(window.client_area_bounds_in_screen()).1, - }; - let previous = self.geometry.lock().unwrap().replace(geometry); - - if previous.is_some_and(|previous| previous.scale_factor != geometry.scale_factor) { - (self.config.emit)(Event::ScaleFactorChanged { - scale_factor: geometry.scale_factor, - new_inner_size: geometry.inner_size, - }); - } - if previous.is_none_or(|previous| previous.inner_size != geometry.inner_size) { - (self.config.emit)(Event::Resized(geometry.inner_size)); - } - } - - 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.state.lock().unwrap().resizable) - } - - fn can_maximize(&self, _window: Option<&mut Window>) -> i32 { - i32::from(self.config.state.lock().unwrap().maximizable) - } - - fn can_minimize(&self, _window: Option<&mut Window>) -> i32 { - i32::from(self.config.state.lock().unwrap().minimizable) - } - - fn can_close(&self, _window: Option<&mut Window>) -> i32 { - if self.allow_close.load(Ordering::Acquire) { - 1 - } else if !self.config.state.lock().unwrap().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(), - config.state.clone(), - config.draggable_regions.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(), - Arc::new(Mutex::new(None)), - ); - window_create_top_level(Some(&mut window_delegate))?; - Some(()) -} - -fn physical_bounds(bounds: Rect) -> (PhysicalPosition, PhysicalSize) { - let bounds = display_convert_screen_rect_to_pixels(Some(&bounds)); - ( - PhysicalPosition::new(bounds.x, bounds.y), - PhysicalSize::new(bounds.width.max(0) as u32, bounds.height.max(0) as u32), - ) -} - -fn scale_factor(window: &Window) -> f64 { - window - .display() - .map(|display| display.device_scale_factor() as f64) - .unwrap_or(1.0) -} - -fn view_scale_factor(view: Option<&mut View>, fallback: f64) -> f64 { - view - .and_then(|view| view.window()) - .as_ref() - .map(scale_factor) - .unwrap_or(fallback) -} diff --git a/src/native_wayland/webview.rs b/src/native_wayland/webview.rs deleted file mode 100644 index 4ebe68d..0000000 --- a/src/native_wayland/webview.rs +++ /dev/null @@ -1,65 +0,0 @@ -use cef::{BrowserView, ImplView, browser_view_get_for_browser}; -use tauri_runtime::dpi::{PhysicalPosition, PhysicalSize, Rect}; -use tauri_utils::config::Color; - -use crate::webview::AppWebview; - -use super::scale_factor; - -impl AppWebview { - fn native_wayland_browser_view(&self) -> Option { - browser_view_get_for_browser(Some(&mut self.browser.clone())) - } - - pub(crate) fn native_wayland_set_background_color(&self, color: Option) { - let Some(view) = self.native_wayland_browser_view() else { - return; - }; - let (r, g, b, a) = color.unwrap_or_default().into(); - view - .set_background_color(((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | b as u32); - } - - pub(crate) fn native_wayland_bounds(&self) -> Option { - let view = self.native_wayland_browser_view()?; - let scale = view.window().as_ref().map(scale_factor).unwrap_or(1.0); - let bounds = view.bounds(); - Some(Rect { - position: PhysicalPosition::new( - (bounds.x as f64 * scale).round() as i32, - (bounds.y as f64 * scale).round() as i32, - ) - .into(), - size: PhysicalSize::new( - (bounds.width.max(0) as f64 * scale).round() as u32, - (bounds.height.max(0) as f64 * scale).round() as u32, - ) - .into(), - }) - } - - pub(crate) fn native_wayland_take_input_focus(&self) { - if let Some(view) = self.native_wayland_browser_view() { - view.request_focus(); - } - } - - pub(crate) fn native_wayland_set_visible(&self, visible: bool) { - if let Some(view) = self.native_wayland_browser_view() { - view.set_visible(i32::from(visible)); - } - } - - pub(crate) fn native_wayland_set_bounds(&self, x: i32, y: i32, width: i32, height: i32) { - let Some(view) = self.native_wayland_browser_view() else { - return; - }; - let scale = view.window().as_ref().map(scale_factor).unwrap_or(1.0); - view.set_bounds(Some(&cef::Rect { - x: (x as f64 / scale).round() as i32, - y: (y as f64 / scale).round() as i32, - width: (width.max(1) as f64 / scale).round() as i32, - height: (height.max(1) as f64 / scale).round() as i32, - })); - } -} diff --git a/src/platform/linux/utils.rs b/src/platform/linux/utils.rs index 3c69cd1..a991953 100644 --- a/src/platform/linux/utils.rs +++ b/src/platform/linux/utils.rs @@ -16,13 +16,7 @@ const CLIENT_MESSAGE: i32 = 33; const SUBSTRUCTURE_REDIRECT_MASK: c_long = 1 << 20; const SUBSTRUCTURE_NOTIFY_MASK: c_long = 1 << 19; -static XLIB: LazyLock> = LazyLock::new(|| { - #[cfg(target_os = "linux")] - if crate::config::native_wayland() { - return None; - } - xlib::Xlib::open().ok() -}); +static XLIB: LazyLock> = LazyLock::new(|| xlib::Xlib::open().ok()); struct Display(*mut xlib::Display); @@ -114,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/platform/linux/webview.rs b/src/platform/linux/webview.rs index a3b4fae..f63f6de 100644 --- a/src/platform/linux/webview.rs +++ b/src/platform/linux/webview.rs @@ -20,21 +20,12 @@ impl AppWebview { } pub(crate) fn set_background_color(&self, color: Option) { - #[cfg(feature = "native-wayland")] - if crate::config::native_wayland() { - self.native_wayland_set_background_color(color); - return; - } let _ = (self, color); // Native child-window background is not equivalent to Chromium's rendered // background. Creation still applies BrowserSettings. } pub(crate) fn bounds(&self) -> Option { - #[cfg(feature = "native-wayland")] - if crate::config::native_wayland() { - return self.native_wayland_bounds(); - } let xid = self.xid(); with_cef_display(None, |xlib, display| unsafe { @@ -75,11 +66,6 @@ impl AppWebview { /// with neither focus nor pointer as inactive. Alloy does this in /// `CefWindowX11::Focus`; Chrome-style child windows have no equivalent. pub(crate) fn take_input_focus(&self) { - #[cfg(feature = "native-wayland")] - if crate::config::native_wayland() { - self.native_wayland_take_input_focus(); - return; - } let xid = self.xid(); with_cef_display((), |xlib, display| unsafe { @@ -96,10 +82,6 @@ impl AppWebview { } pub(crate) fn reparent(&self, parent: &AppWindow) { - #[cfg(feature = "native-wayland")] - if crate::config::native_wayland() { - return; - } let xid = self.xid(); let parent_xid = parent.xid(); @@ -110,11 +92,6 @@ impl AppWebview { } pub(crate) fn apply_visible(&self, visible: bool) { - #[cfg(feature = "native-wayland")] - if crate::config::native_wayland() { - self.native_wayland_set_visible(visible); - return; - } let xid = self.xid(); with_cef_display((), |xlib, display| unsafe { @@ -151,10 +128,6 @@ impl AppWebview { } pub(crate) fn destroy_native(&self) { - #[cfg(feature = "native-wayland")] - if crate::config::native_wayland() { - return; - } let xid = self.xid(); with_cef_display((), |xlib, display| unsafe { (xlib.XDestroyWindow)(display, xid); @@ -163,11 +136,6 @@ impl AppWebview { } pub(crate) fn apply_physical_bounds(&self, _scale: f64, x: i32, y: i32, width: i32, height: i32) { - #[cfg(feature = "native-wayland")] - if crate::config::native_wayland() { - self.native_wayland_set_bounds(x, y, width, height); - return; - } let xid = self.xid(); with_cef_display((), |xlib, display| unsafe { diff --git a/src/runtime.rs b/src/runtime.rs index 57d403b..439e443 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -115,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, } @@ -156,7 +157,7 @@ impl Default for MainThreadDispatchSlot { } } -struct MainThreadDispatchGuard { +pub(crate) struct MainThreadDispatchGuard { context: RuntimeContext, dispatch: Box>, previous: *mut MainThreadDispatch, @@ -180,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(()) } @@ -198,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) { @@ -260,8 +278,6 @@ pub(crate) type AfterWindowCreationCallback = Box Fn(RawWindow<'a>) pub(crate) enum Message { EventLoop(EventLoopMessage), BrowserClosed(WindowId, u32), - #[cfg(all(target_os = "linux", feature = "native-wayland"))] - NativeWaylandWindow(WindowId, crate::native_wayland::Event), Opened(Vec), #[cfg(target_os = "macos")] Reopen { @@ -407,8 +423,6 @@ pub(crate) struct WinitCefApp { target_os = "openbsd" ))] last_focus_probe: Option, - #[cfg(target_os = "linux")] - last_slow_cef_tick_log: Option, } /// Stands in for the scheduling callbacks `external_message_pump` would provide. @@ -421,12 +435,6 @@ pub(crate) struct WinitCefApp { ))] const CEF_WORK_INTERVAL: std::time::Duration = std::time::Duration::from_millis(4); -#[cfg(target_os = "linux")] -const SLOW_CEF_TICK: std::time::Duration = std::time::Duration::from_millis(8); - -#[cfg(target_os = "linux")] -const SLOW_CEF_TICK_LOG_INTERVAL: std::time::Duration = std::time::Duration::from_secs(1); - /// Probing costs blocking X round-trips and focus is not that time-sensitive. #[cfg(any( target_os = "linux", @@ -465,8 +473,6 @@ impl WinitCefApp { target_os = "openbsd" ))] last_focus_probe: None, - #[cfg(target_os = "linux")] - last_slow_cef_tick_log: None, } } @@ -478,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) { @@ -526,10 +532,6 @@ impl WinitCefApp { self.state.live_browsers = self.state.live_browsers.saturating_sub(1); self.exit_if_done(event_loop); } - #[cfg(all(target_os = "linux", feature = "native-wayland"))] - Message::NativeWaylandWindow(window_id, event) => { - self.handle_native_wayland_event(window_id, event, event_loop) - } Message::CreateWindow { window_id, webview_id, @@ -712,11 +714,6 @@ impl WinitCefApp { /// winit already considers the top-level unfocused once the browser child holds /// the focus, so it drops the `FocusOut` for a real loss. The loop still wakes. fn sync_delegated_focus(&mut self) { - #[cfg(target_os = "linux")] - if crate::config::native_wayland() { - return; - } - #[cfg(any( target_os = "linux", target_os = "dragonfly", @@ -747,51 +744,6 @@ impl WinitCefApp { } } - #[cfg(all(target_os = "linux", feature = "native-wayland"))] - fn handle_native_wayland_event( - &mut self, - window_id: WindowId, - event: crate::native_wayland::Event, - event_loop: &dyn ActiveEventLoop, - ) { - match event { - crate::native_wayland::Event::CloseRequested => { - self.request_window_close(window_id, event_loop) - } - crate::native_wayland::Event::Destroyed => { - self.close_window(window_id, event_loop); - } - crate::native_wayland::Event::Focused(focused) => { - let Some(appwindow) = self.state.windows.get_mut(&window_id) else { - return; - }; - if appwindow.reported_focus != focused { - appwindow.reported_focus = focused; - for child in &appwindow.children { - child.host.set_focus(i32::from(focused)); - } - if focused && let Some(child) = appwindow.children.first() { - child.take_input_focus(); - } - self.emit_window_event(window_id, WindowEvent::Focused(focused)); - } - } - crate::native_wayland::Event::Resized(size) => { - self.emit_window_event(window_id, WindowEvent::Resized(size)); - } - crate::native_wayland::Event::ScaleFactorChanged { - scale_factor, - new_inner_size, - } => self.emit_window_event( - window_id, - WindowEvent::ScaleFactorChanged { - scale_factor, - new_inner_size, - }, - ), - } - } - fn emit_window_event(&mut self, window_id: WindowId, event: WindowEvent) { let Some(appwindow) = self.state.windows.get(&window_id) else { return; @@ -881,10 +833,8 @@ impl WinitCefApp { // shutdown drain is still enforced by live_browsers. for child in &appwindow.children { self.remove_scheme_handler_entries(child); - child.close(); + child.host.close_browser(1); } - #[cfg(all(target_os = "linux", feature = "native-wayland"))] - appwindow.close_native_wayland(); self.exit_if_done(event_loop); } @@ -932,10 +882,8 @@ impl WinitCefApp { for appwindow in self.state.windows.values() { for child in &appwindow.children { self.remove_scheme_handler_entries(child); - child.close(); + child.host.close_browser(1); } - #[cfg(all(target_os = "linux", feature = "native-wayland"))] - appwindow.close_native_wayland(); } self.state.windows.clear(); self.state.winid_id_to_window_id_map.clear(); @@ -977,38 +925,13 @@ impl WinitCefApp { target_os = "netbsd", target_os = "openbsd" ))] - fn service_glib(&mut self, event_loop: &dyn ActiveEventLoop) { - let tick_started = std::time::Instant::now(); + fn service_glib(&self, event_loop: &dyn ActiveEventLoop) { let context = gtk::glib::MainContext::default(); - let mut glib_iterations = 0; while context.pending() { context.iteration(false); - glib_iterations += 1; } - let glib_elapsed = tick_started.elapsed(); - let cef_started = std::time::Instant::now(); cef::do_message_loop_work(); - let cef_elapsed = cef_started.elapsed(); - - #[cfg(target_os = "linux")] - { - let tick_elapsed = tick_started.elapsed(); - let now = std::time::Instant::now(); - if crate::config::native_wayland() - && tick_elapsed >= SLOW_CEF_TICK - && self - .last_slow_cef_tick_log - .is_none_or(|last| now.duration_since(last) >= SLOW_CEF_TICK_LOG_INTERVAL) - { - self.last_slow_cef_tick_log = Some(now); - log::warn!( - "slow native Wayland CEF tick: total={tick_elapsed:?}, glib={glib_elapsed:?} \ - ({glib_iterations} iterations), cef={cef_elapsed:?}" - ); - } - } - event_loop.set_control_flow(winit::event_loop::ControlFlow::WaitUntil( std::time::Instant::now() + CEF_WORK_INTERVAL, )); @@ -1584,16 +1507,12 @@ impl CefRuntime { } #[cfg(target_os = "linux")] - match cef_config.linux_windowing { - crate::LinuxWindowing::X11 => { - command_line_args.push(("ozone-platform".to_string(), Some("x11".to_string()))); - event_loop_builder.with_x11(); - } - #[cfg(feature = "native-wayland")] - crate::LinuxWindowing::Wayland => { - command_line_args.push(("ozone-platform".to_string(), Some("wayland".to_string()))); - event_loop_builder.with_wayland(); - } + 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( @@ -1895,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..891b099 --- /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..88744bb --- /dev/null +++ b/src/wayland/window.rs @@ -0,0 +1,513 @@ +//! 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(()) +} diff --git a/src/webview.rs b/src/webview.rs index 2ccb09b..f3cd17c 100644 --- a/src/webview.rs +++ b/src/webview.rs @@ -200,12 +200,6 @@ pub(crate) struct AppWebview { pub(crate) bounds_rate: Option, } -struct BrowserChild { - webview: AppWebview, - #[cfg(all(target_os = "linux", feature = "native-wayland"))] - native_wayland: Option, -} - impl AppWebview { pub(crate) fn set_bounds(&mut self, parent_size: PhysicalSize, scale: f64, bounds: Rect) { let position = bounds.position.to_physical::(scale); @@ -237,11 +231,6 @@ impl AppWebview { self.apply_visible(visible); } - pub(crate) fn close(&self) { - self.host.close_browser(1); - self.destroy_native(); - } - pub fn url(&self) -> Option { self .browser @@ -291,21 +280,6 @@ impl WinitCefApp { drag_drop_event_target: browser_client::DragDropEventTarget, pending: PendingWebview>, ) -> Result<()> { - #[cfg(target_os = "linux")] - if crate::config::native_wayland() && !appwindow.children.is_empty() { - return Err(Error::CreateWebview( - "native Wayland supports one webview in its single top-level window" - .to_string() - .into(), - )); - } - #[cfg(target_os = "linux")] - let parent = if crate::config::native_wayland() { - Default::default() - } else { - appwindow.raw_cef_handle() - }; - #[cfg(not(target_os = "linux"))] let parent = appwindow.raw_cef_handle(); let parent_size = appwindow.window.surface_size(); let scale = appwindow.window.scale_factor(); @@ -321,16 +295,6 @@ impl WinitCefApp { scale, theme, drag_drop_event_target, - #[cfg(all(target_os = "linux", feature = "native-wayland"))] - crate::config::native_wayland().then(|| { - crate::native_wayland::WindowConfig::new( - context, - appwindow.id, - &appwindow.attrs, - parent_size, - scale, - ) - }), pending, ) else { return Err(Error::CreateWebview( @@ -339,11 +303,7 @@ impl WinitCefApp { }; *live_browsers += 1; - #[cfg(all(target_os = "linux", feature = "native-wayland"))] - { - appwindow.native_wayland = child.native_wayland; - } - appwindow.children.push(child.webview); + appwindow.children.push(child); layout_app_window(appwindow); // No winit focus event is coming for a window that is already focused. if appwindow.reported_focus @@ -355,7 +315,7 @@ impl WinitCefApp { Ok(()) } - fn build_browser_child( + pub(crate) fn build_browser_child( context: &RuntimeContext, scheme_registry: &request_handler::SchemeRegistry, window_id: WindowId, @@ -365,31 +325,15 @@ impl WinitCefApp { scale: f64, theme: Option, drag_drop_event_target: browser_client::DragDropEventTarget, - #[cfg(all(target_os = "linux", feature = "native-wayland"))] native_wayland: Option< - crate::native_wayland::WindowConfig, - >, mut pending: PendingWebview>, - ) -> Option { + ) -> Option { let bounds_rate = compute_child_bounds_rate( pending.webview_attributes.bounds.as_ref(), pending.webview_attributes.auto_resize, parent_size, scale, ); - #[cfg_attr( - not(all(target_os = "linux", feature = "native-wayland")), - allow(unused_mut) - )] - let mut initialization_scripts = initialization_scripts(&mut pending.webview_attributes); - #[cfg(all(target_os = "linux", feature = "native-wayland"))] - if native_wayland - .as_ref() - .is_some_and(crate::native_wayland::WindowConfig::is_frameless) - { - Arc::make_mut(&mut initialization_scripts).push(CefInitScript::new( - crate::native_wayland::drag_region_initialization_script(), - )); - } + let initialization_scripts = initialization_scripts(&mut pending.webview_attributes); let uri_scheme_protocols: Arc> = Arc::new( pending .uri_scheme_protocols @@ -435,12 +379,6 @@ impl WinitCefApp { drag_drop_event_target, drag_drop_handler_enabled, drag_drop_state, - #[cfg(all(target_os = "linux", feature = "native-wayland"))] - native_wayland - .as_ref() - .map(crate::native_wayland::WindowConfig::draggable_regions_changed), - #[cfg(not(all(target_os = "linux", feature = "native-wayland")))] - None, handlers, context.proxy.clone(), context.sender.clone(), @@ -500,85 +438,6 @@ impl WinitCefApp { // Create with an inert document so the BrowserHost exists before the real // navigation; the real URL is loaded once the document-start script is set. let initial_url = CefString::from(INITIAL_LOAD_URL); - let finish_browser = move |browser: Browser, - #[cfg(all( - target_os = "linux", - feature = "native-wayland" - ))] - native_wayland| { - let Some(host) = browser.host() else { - log::error!("CEF browser for webview {label:?} has no host"); - return; - }; - let browser_id = browser.identifier(); - - { - let mut registry = scheme_registry.lock().unwrap(); - for (scheme, handler) in uri_scheme_protocols.iter() { - registry.insert( - (browser_id, scheme.clone()), - ( - label.clone(), - handler.clone(), - initialization_scripts.clone(), - ), - ); - } - } - - let devtools_protocol_handlers = Arc::new(Mutex::new(Vec::new())); - let pending_initial_loads: PendingInitialLoads = Arc::new(Mutex::new(HashMap::new())); - let devtools_observer_registration = Arc::new(Mutex::new(add_dev_tools_observer( - &browser, - devtools_protocol_handlers.clone(), - pending_initial_loads.clone(), - ))); - load_initial_url_after_registering_initialization_scripts( - &browser, - &initialization_scripts, - &custom_protocol_scheme, - &custom_scheme_domain_names, - &real_initial_url, - &pending_initial_loads, - ); - - browser_tx - .send(BrowserChild { - webview: AppWebview { - webview_id, - label, - browser, - browser_id, - host, - uri_scheme_protocols, - devtools_protocol_handlers, - devtools_observer_registration, - listeners: Default::default(), - bounds_rate, - }, - #[cfg(all(target_os = "linux", feature = "native-wayland"))] - native_wayland, - }) - .expect("failed to send initialized CEF browser"); - }; - - #[cfg(all(target_os = "linux", feature = "native-wayland"))] - if let Some(native_wayland) = native_wayland { - if crate::native_wayland::create( - &mut client, - &initial_url, - &settings, - request_context.as_mut(), - native_wayland, - Box::new(move |browser, window| finish_browser(browser, Some(window))), - ) - .is_none() - { - log::error!("failed to create native Wayland CEF window"); - } - return; - } - let Some(browser) = cef::browser_host_create_browser_sync( Some(&window_info), Some(&mut client), @@ -587,14 +446,59 @@ impl WinitCefApp { None, request_context.as_mut(), ) else { - log::error!("failed to create CEF browser"); + log::error!("failed to create CEF browser for webview {label:?}"); return; }; - finish_browser( - browser, - #[cfg(all(target_os = "linux", feature = "native-wayland"))] - None, + let Some(host) = browser.host() else { + log::error!("CEF browser for webview {label:?} has no host"); + return; + }; + let browser_id = browser.identifier(); + + { + let mut registry = scheme_registry.lock().unwrap(); + for (scheme, handler) in uri_scheme_protocols.iter() { + registry.insert( + (browser_id, scheme.clone()), + ( + label.clone(), + handler.clone(), + initialization_scripts.clone(), + ), + ); + } + } + + let devtools_protocol_handlers = Arc::new(Mutex::new(Vec::new())); + let pending_initial_loads: PendingInitialLoads = Arc::new(Mutex::new(HashMap::new())); + let devtools_observer_registration = Arc::new(Mutex::new(add_dev_tools_observer( + &browser, + devtools_protocol_handlers.clone(), + pending_initial_loads.clone(), + ))); + load_initial_url_after_registering_initialization_scripts( + &browser, + &initialization_scripts, + &custom_protocol_scheme, + &custom_scheme_domain_names, + &real_initial_url, + &pending_initial_loads, ); + + browser_tx + .send(AppWebview { + webview_id, + label, + browser, + browser_id, + host, + uri_scheme_protocols, + devtools_protocol_handlers, + devtools_observer_registration, + listeners: Default::default(), + bounds_rate, + }) + .expect("failed to send initialized CEF browser"); } }); let request_context = request_context::request_context_from_webview_attributes( @@ -613,7 +517,7 @@ impl WinitCefApp { // `None` here means browser creation failed (or the request context never // initialized); the continuation logs the reason. Soft-fail instead of // taking down the whole process. - request_context::wait_for_deferred_result(&browser_rx) + browser_rx.recv().ok() } pub(crate) fn handle_webview_message( @@ -699,13 +603,12 @@ impl WinitCefApp { // callback can race parent-window bookkeeping. Window/app teardown already // uses force_close=true; standalone child close needs the same semantics. WebviewMessage::Close => { + child.host.close_browser(1); // Windowed CEF browsers are not destroyed by CloseBrowser alone: the // native child hierarchy must also be torn down before OnBeforeClose // runs. Leaving it attached leaks the renderer; letting CEF forward a // close to its top-level parent can close the whole Tauri window. - child.close(); - #[cfg(all(target_os = "linux", feature = "native-wayland"))] - appwindow.close_native_wayland(); + child.destroy_native(); } WebviewMessage::SetBounds(bounds) => { let parent_size = appwindow.window.surface_size(); @@ -1325,10 +1228,6 @@ impl WebviewDispatch for CefWebviewDispatcher { /// from the current window size; children with fixed bounds keep whatever bounds /// they were last given. pub(crate) fn layout_app_window(appwindow: &AppWindow) { - #[cfg(all(target_os = "linux", feature = "native-wayland"))] - if appwindow.native_wayland.is_some() { - return; - } let parent_size = appwindow.window.surface_size(); let win_w = parent_size.width as f32; let win_h = parent_size.height as f32; diff --git a/src/window.rs b/src/window.rs index 0a0568c..fa2d25f 100644 --- a/src/window.rs +++ b/src/window.rs @@ -328,8 +328,6 @@ pub(crate) struct AppWindow { pub(crate) window: Box, pub(crate) attrs: AppWindowAttrs, pub(crate) children: Vec, - #[cfg(all(target_os = "linux", feature = "native-wayland"))] - pub(crate) native_wayland: Option, pub(crate) listeners: WindowEventListeners, /// Last focus state reported to Tauri. See `WinitCefApp::sync_window_focus`. pub(crate) reported_focus: bool, @@ -424,13 +422,6 @@ impl WinitCefApp { pending: Box>>, _after_window_creation: Option, ) -> Result<()> { - #[cfg(target_os = "linux")] - let native_wayland = crate::config::native_wayland(); - #[cfg(target_os = "linux")] - if native_wayland && !self.state.windows.is_empty() { - return Err(Error::CreateWindow); - } - let mut attrs = pending.window_builder.attrs.clone(); if attrs.inner.preferred_theme.is_none() { attrs.inner.preferred_theme = @@ -438,15 +429,8 @@ impl WinitCefApp { } prepare_window_attributes(event_loop, &mut attrs); - let mut control_attrs = attrs.inner.clone(); - #[cfg(target_os = "linux")] - if native_wayland { - // CEF Views owns the visible top-level; winit remains an invisible - // event-loop and monitor provider for Tauri's runtime contract. - control_attrs.visible = false; - } let window = event_loop - .create_window(control_attrs) + .create_window(attrs.inner.clone()) .map_err(|_| Error::CreateWindow)?; let winit_id = window.id(); @@ -458,8 +442,6 @@ impl WinitCefApp { window, attrs, children: Vec::new(), - #[cfg(all(target_os = "linux", feature = "native-wayland"))] - native_wayland: None, listeners: Default::default(), reported_focus: false, #[cfg(target_os = "macos")] @@ -475,13 +457,8 @@ impl WinitCefApp { } } - #[cfg(target_os = "linux")] - if !native_wayland { - appwindow.set_visible_on_all_workspaces(appwindow.attrs.visible_on_all_workspaces); - appwindow.set_skip_taskbar(appwindow.attrs.skip_taskbar); - } - #[cfg(any( + target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", @@ -497,12 +474,7 @@ impl WinitCefApp { appwindow.draw_background_surface(); } - #[cfg(target_os = "linux")] - if appwindow.attrs.background_color.is_some() && !native_wayland { - appwindow.set_background_color(appwindow.attrs.background_color); - } - - #[cfg(all(not(windows), not(target_os = "linux")))] + #[cfg(not(windows))] if appwindow.attrs.background_color.is_some() { appwindow.set_background_color(appwindow.attrs.background_color); } @@ -531,14 +503,6 @@ impl WinitCefApp { )?; } - #[cfg(target_os = "linux")] - if !native_wayland { - self - .state - .winid_id_to_window_id_map - .insert(winit_id, window_id); - } - #[cfg(not(target_os = "linux"))] self .state .winid_id_to_window_id_map @@ -570,10 +534,6 @@ impl WinitCefApp { let Some(appwindow) = self.state.windows.get_mut(&window_id) else { return; }; - #[cfg(all(target_os = "linux", feature = "native-wayland"))] - let Some(message) = crate::native_wayland::handle_window_message(appwindow, message) else { - return; - }; let window = &appwindow.window; match message { From 199a2717c8d41aee1784aacf5096640545817ea2 Mon Sep 17 00:00:00 2001 From: lunar-seal Date: Thu, 27 Aug 2026 17:25:05 +0200 Subject: [PATCH 7/8] restore original README --- README.md | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/README.md b/README.md index 5a49254..372d453 100644 --- a/README.md +++ b/README.md @@ -32,29 +32,6 @@ fn main() { Because published tauri only defaults its generic types (`AppHandle`, `WebviewWindow`, …) to wry, apps alias them once (`type AppHandle = tauri::AppHandle;`) and build tauri with `default-features = false`. -On Linux, native Wayland can be selected before startup — this is opt-in via the -`native-wayland` crate feature (`tauri-runtime-cef = { ..., features = ["native-wayland"] }`), -which also gates `LinuxWindowing::Wayland` itself: - -```rust -tauri_runtime_cef::configure(tauri_runtime_cef::CefConfig { - linux_windowing: tauri_runtime_cef::LinuxWindowing::Wayland, - ..Default::default() -}); -``` - -This is a parallel CEF Views path: CEF owns the single visible top-level and its -browser view. X11 remains the default and compiled fallback, but the runtime does -not open an X display connection in Wayland mode. Native Wayland currently supports -one window with one full-window webview; Linux raw window handles and runtime -decoration changes are unavailable in this mode. Creation-time frameless windows -support Tauri drag regions; size constraints and resizable/maximizable/minimizable/ -closable state are forwarded to the CEF window delegate. - -CEF's Chrome password manager and this crate's permission policy use the same -request-context and browser-client paths on X11 and Wayland; non-incognito -profiles use the same persistent cache path. - ## Additions Capabilities this crate adds on top of the imported runtime: @@ -117,7 +94,7 @@ How building against published tauri changes the mechanics, relative to the feat ## Known ceilings -- Verified on Linux (X11 and native Wayland). The macOS/Windows paths compile-ported blind — they need a real pass. +- Verified on Linux (X11). The macOS/Windows paths compile-ported blind — they need a real pass. - macOS `.app` bundling needs the CEF framework + helper-app layout that feat/cef's tauri-cli produces; the published CLI doesn't do this. Bundle scripting lives with the consuming app for now. - Deep-link relaunch URLs are dropped on Linux/Windows (published `tauri-runtime` has no `RunEvent::Opened` there). From 88709bea12ea1a1baa7abd42a1083e750503a9dd Mon Sep 17 00:00:00 2001 From: lunar-seal Date: Fri, 28 Aug 2026 18:55:08 +0200 Subject: [PATCH 8/8] fix fmt --- src/wayland/webview.rs | 2 +- src/wayland/window.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/wayland/webview.rs b/src/wayland/webview.rs index 891b099..8262f76 100644 --- a/src/wayland/webview.rs +++ b/src/wayland/webview.rs @@ -281,7 +281,7 @@ pub(super) fn handle_message( 0.0 }); } - WebviewMessage::SetBackgroundColor(_) => {}, + WebviewMessage::SetBackgroundColor(_) => {} WebviewMessage::Url(tx) => _ = tx.send(Ok(child.url().unwrap_or_default())), WebviewMessage::Bounds(tx) => { _ = tx.send(Ok(Rect { diff --git a/src/wayland/window.rs b/src/wayland/window.rs index 88744bb..0ab30bc 100644 --- a/src/wayland/window.rs +++ b/src/wayland/window.rs @@ -22,7 +22,6 @@ use crate::{ window::{AppWindowAttrs, WindowMessage, winit_theme_to_tauri_theme}, }; - type BrowserCreated = Box; type Emit = Arc;