| | | 1 | | // SPDX-FileCopyrightText: 2026 Alper Çelik |
| | | 2 | | // SPDX-License-Identifier: MIT OR Apache-2.0 |
| | | 3 | | |
| | | 4 | | //! Rust <-> Qt bridge for the runtime. |
| | | 5 | | //! |
| | | 6 | | //! The C++ side owns the `QApplication` singleton, the window/webview registries |
| | | 7 | | //! and the custom URL scheme handlers; Rust drives it through this bridge. |
| | | 8 | | //! |
| | | 9 | | //! C++ translation units must include the cxx-generated header for this bridge |
| | | 10 | | //! (it declares the `extern "Rust"` callbacks below with the correct linkage): |
| | | 11 | | //! `#include "tauri-runtime-qt/src/qt.rs.h"`. |
| | | 12 | | //! |
| | | 13 | | //! The payload encoding the bridge speaks (header block, base64 body marker, |
| | | 14 | | //! response framing) lives in `tauri-runtime-qt-core::scheme`; the callbacks and |
| | | 15 | | //! the bridge declaration stay here. |
| | | 16 | | |
| | | 17 | | use parking_lot::Mutex; |
| | | 18 | | use tauri_runtime as rt; |
| | | 19 | | use tauri_runtime::window::WindowEvent; |
| | | 20 | | use tauri_runtime_qt_core as qtcore; |
| | | 21 | | |
| | | 22 | | #[cxx::bridge] |
| | | 23 | | pub mod ffi { |
| | | 24 | | unsafe extern "C++" { |
| | | 25 | | include!("qt_app.hpp"); |
| | | 26 | | include!("qt_scheme.hpp"); |
| | | 27 | | |
| | | 28 | | // ---- application bootstrap (qt_app.cpp) ---- |
| | | 29 | | /// Creates the `QApplication` and registers Tauri's URL schemes. |
| | | 30 | | /// Must be called on the main thread before any GUI object exists. |
| | | 31 | | /// Returns 0 on success, non-zero if an application already exists. |
| | 20 | 32 | | fn qt_init(argc: i32) -> i32; |
| | | 33 | | |
| | | 34 | | /// Runs the Qt event loop; returns the process exit code. |
| | 20 | 35 | | fn qt_run() -> i32; |
| | | 36 | | |
| | | 37 | | /// Version string of the Qt runtime actually in use (e.g. "6.11.2"). |
| | 0 | 38 | | fn qt_version() -> String; |
| | | 39 | | |
| | | 40 | | // ---- custom schemes (qt_scheme.cpp) ---- |
| | | 41 | | /// Registers ipc/tauri/asset schemes; must run before the QApplication. |
| | 0 | 42 | | fn qt_scheme_register_schemes(); |
| | | 43 | | /// Installs the scheme handlers into the webview's profile. |
| | 20 | 44 | | fn qt_scheme_install_handler(webview_id: u64); |
| | | 45 | | /// Completes a request that `qt_scheme_request` reported as pending |
| | | 46 | | /// (`handled == 2`). |
| | 20 | 47 | | fn qt_scheme_respond(request_id: u64, status: i32, headers: &str, body: &str); |
| | | 48 | | } |
| | | 49 | | |
| | | 50 | | extern "Rust" { |
| | | 51 | | /// Window events emitted from Qt: 0 = close requested, 1 = focused, |
| | | 52 | | /// 2 = unfocused, 3 = resized ("WxH"), 4 = moved ("x,y"). |
| | | 53 | | fn qt_on_window_event(id: u64, kind: i32, payload: &str); |
| | | 54 | | |
| | | 55 | | /// Webview events: 1 = load started (url), 2 = load finished (url), |
| | | 56 | | /// 3 = url changed, 4 = title changed, 5 = load failed. |
| | | 57 | | fn qt_on_webview_event(id: u64, kind: i32, payload: &str); |
| | | 58 | | |
| | | 59 | | /// Custom-scheme request routed to the handler tauri registered for the |
| | | 60 | | /// webview. `scheme_kind`: 0 = ipc, 1 = tauri, 2 = asset. `headers` is a |
| | | 61 | | /// newline-separated `k: v` list, and the response carries the same |
| | | 62 | | /// encoding back. |
| | | 63 | | fn qt_scheme_request( |
| | | 64 | | webview_id: u64, |
| | | 65 | | request_id: u64, |
| | | 66 | | scheme_kind: i32, |
| | | 67 | | method: &str, |
| | | 68 | | url: &str, |
| | | 69 | | headers: &str, |
| | | 70 | | body: &str, |
| | | 71 | | ) -> QtSchemeResponse; |
| | | 72 | | } |
| | | 73 | | |
| | | 74 | | /// Response produced for a custom-scheme request. |
| | | 75 | | struct QtSchemeResponse { |
| | | 76 | | /// 1 = handled (the fields below are valid), 0 = unhandled. |
| | | 77 | | handled: i32, |
| | | 78 | | status: i32, |
| | | 79 | | headers: String, |
| | | 80 | | body: String, |
| | | 81 | | } |
| | | 82 | | } |
| | | 83 | | |
| | | 84 | | /// Window events emitted from Qt (see the bridge declaration for the kinds). |
| | 0 | 85 | | pub fn qt_on_window_event(id: u64, kind: i32, payload: &str) { |
| | 0 | 86 | | let event = match kind { |
| | | 87 | | 0 => { |
| | 0 | 88 | | let (tx, _rx) = std::sync::mpsc::channel(); |
| | 0 | 89 | | WindowEvent::CloseRequested { signal_tx: tx } |
| | | 90 | | } |
| | 0 | 91 | | 1 => WindowEvent::Focused(true), |
| | 0 | 92 | | 2 => WindowEvent::Focused(false), |
| | | 93 | | 3 => { |
| | 0 | 94 | | let (w, h) = qtcore::window::parse_resize_payload(payload); |
| | 0 | 95 | | WindowEvent::Resized(rt::dpi::PhysicalSize::new(w, h)) |
| | | 96 | | } |
| | | 97 | | 4 => { |
| | 0 | 98 | | let (x, y) = qtcore::window::parse_move_payload(payload); |
| | 0 | 99 | | WindowEvent::Moved(rt::dpi::PhysicalPosition::new(x, y)) |
| | | 100 | | } |
| | 0 | 101 | | _ => return, |
| | | 102 | | }; |
| | | 103 | | |
| | 0 | 104 | | for (_, native, handler) in crate::WINDOW_HANDLERS.lock().iter() { |
| | 0 | 105 | | if *native == id { |
| | 0 | 106 | | handler(&event); |
| | 0 | 107 | | } |
| | | 108 | | } |
| | 0 | 109 | | } |
| | | 110 | | |
| | | 111 | | /// Webview events emitted from Qt (see the bridge declaration for the kinds). |
| | | 112 | | /// |
| | | 113 | | /// `tauri_runtime::window::WebviewEvent` only has `DragDrop` in 2.12; page-load |
| | | 114 | | /// and title events must go through the handlers tauri puts on `PendingWebview`. |
| | 120 | 115 | | pub fn qt_on_webview_event(id: u64, kind: i32, payload: &str) { |
| | 120 | 116 | | crate::qtrace!("[tauri-runtime-qt] webview {id} event {kind}: {payload}"); |
| | 120 | 117 | | } |
| | | 118 | | |
| | 0 | 119 | | fn unhandled() -> ffi::QtSchemeResponse { |
| | 0 | 120 | | ffi::QtSchemeResponse { |
| | 0 | 121 | | handled: 0, |
| | 0 | 122 | | status: 404, |
| | 0 | 123 | | headers: String::new(), |
| | 0 | 124 | | body: String::new(), |
| | 0 | 125 | | } |
| | 0 | 126 | | } |
| | | 127 | | |
| | | 128 | | /// Custom-scheme request routed to the handler tauri registered for this webview. |
| | 1260 | 129 | | pub fn qt_scheme_request( |
| | 1260 | 130 | | webview_id: u64, |
| | 1260 | 131 | | request_id: u64, |
| | 1260 | 132 | | scheme_kind: i32, |
| | 1260 | 133 | | method: &str, |
| | 1260 | 134 | | url: &str, |
| | 1260 | 135 | | headers: &str, |
| | 1260 | 136 | | body: &str, |
| | 1260 | 137 | | ) -> ffi::QtSchemeResponse { |
| | 1260 | 138 | | crate::qtrace!( |
| | | 139 | | "[tauri-runtime-qt] scheme_request webview={webview_id} kind={scheme_kind} {method} {url}" |
| | | 140 | | ); |
| | | 141 | | |
| | 1260 | 142 | | let scheme = match scheme_kind { |
| | 1240 | 143 | | 0 => "ipc", |
| | 20 | 144 | | 1 => "tauri", |
| | 0 | 145 | | 2 => "asset", |
| | 0 | 146 | | _ => return unhandled(), |
| | | 147 | | }; |
| | | 148 | | |
| | | 149 | | // QtWebEngine never hands us the POST body, so our invoke system carries it |
| | | 150 | | // in `X-Tauriqt-Payload` (base64); the core crate turns the bridge's header |
| | | 151 | | // block back into a normal request for tauri's IPC handler. |
| | 1260 | 152 | | let Some(request) = qtcore::scheme::decode_request(method, url, headers, body) else { |
| | 0 | 153 | | return unhandled(); |
| | | 154 | | }; |
| | | 155 | | |
| | | 156 | | // tauri answers most protocols asynchronously; the responder may fire after |
| | | 157 | | // this function returned. In that case we hand the response to C++ through |
| | | 158 | | // `qt_scheme_respond` and report `handled == 2` (pending). |
| | | 159 | | /// Decides, without a race, whether the responder fired inline or later. |
| | | 160 | | struct Gate { |
| | | 161 | | returned: bool, |
| | | 162 | | immediate: Option<ffi::QtSchemeResponse>, |
| | | 163 | | } |
| | 1260 | 164 | | let gate = std::sync::Arc::new(Mutex::new(Gate { |
| | 1260 | 165 | | returned: false, |
| | 1260 | 166 | | immediate: None, |
| | 1260 | 167 | | })); |
| | | 168 | | // The responder may outlive this call, so it must own its diagnostic text. |
| | 1260 | 169 | | let method_owned = method.to_owned(); |
| | 1260 | 170 | | let url_owned = url.to_owned(); |
| | | 171 | | |
| | | 172 | | { |
| | 1260 | 173 | | let guard = crate::SCHEME_PROTOCOLS.lock(); |
| | 1260 | 174 | | let Some(entry) = guard.iter().find(|(id, _, _)| *id == webview_id) else { |
| | 0 | 175 | | return unhandled(); |
| | | 176 | | }; |
| | 1260 | 177 | | let Some(handler) = entry.2.get(scheme) else { |
| | 0 | 178 | | return unhandled(); |
| | | 179 | | }; |
| | 1260 | 180 | | let label = entry.1.clone(); |
| | | 181 | | |
| | 1260 | 182 | | let gate = std::sync::Arc::clone(&gate); |
| | 1260 | 183 | | let method_owned = method_owned.clone(); |
| | 1260 | 184 | | let url_owned = url_owned.clone(); |
| | 1260 | 185 | | handler( |
| | 1260 | 186 | | &label, |
| | 1260 | 187 | | request, |
| | 1260 | 188 | | Box::new( |
| | 1260 | 189 | | move |response: http::Response<std::borrow::Cow<'static, [u8]>>| { |
| | 1260 | 190 | | let (status, headers, body) = qtcore::scheme::encode_response(response); |
| | 1260 | 191 | | crate::qtrace!( |
| | | 192 | | "[tauri-runtime-qt] {scheme} {method_owned} {url_owned} -> {status} ({} body bytes)", |
| | 1260 | 193 | | body.len() |
| | | 194 | | ); |
| | | 195 | | // If `qt_scheme_request` has not returned yet the response can |
| | | 196 | | // travel back inline; otherwise it must be delivered through |
| | | 197 | | // `qt_scheme_respond`, because this call outlived the bridge call. |
| | 1260 | 198 | | let deferred = { |
| | 1260 | 199 | | let mut gate = gate.lock(); |
| | 1260 | 200 | | if gate.returned { |
| | 20 | 201 | | true |
| | | 202 | | } else { |
| | 1240 | 203 | | gate.immediate = Some(ffi::QtSchemeResponse { |
| | 1240 | 204 | | handled: 1, |
| | 1240 | 205 | | status, |
| | 1240 | 206 | | headers: headers.clone(), |
| | 1240 | 207 | | body: body.clone(), |
| | 1240 | 208 | | }); |
| | 1240 | 209 | | false |
| | | 210 | | } |
| | | 211 | | }; |
| | 1260 | 212 | | if deferred { |
| | 20 | 213 | | ffi::qt_scheme_respond(request_id, status, &headers, &body); |
| | 1240 | 214 | | } |
| | 1260 | 215 | | }, |
| | | 216 | | ), |
| | | 217 | | ); |
| | | 218 | | } |
| | | 219 | | |
| | | 220 | | // Mark the bridge call as finished while holding the lock, so a responder |
| | | 221 | | // that fires concurrently either lands in `immediate` or takes the |
| | | 222 | | // `qt_scheme_respond` path — never neither. |
| | 1260 | 223 | | let inline = { |
| | 1260 | 224 | | let mut gate = gate.lock(); |
| | 1260 | 225 | | gate.returned = true; |
| | 1260 | 226 | | gate.immediate.take() |
| | | 227 | | }; |
| | 1260 | 228 | | if let Some(response) = inline { |
| | 1240 | 229 | | return response; |
| | 20 | 230 | | } |
| | | 231 | | |
| | | 232 | | // Not answered yet: the C++ side keeps the job alive and waits for |
| | | 233 | | // `qt_scheme_respond`. |
| | 20 | 234 | | ffi::QtSchemeResponse { |
| | 20 | 235 | | handled: 2, |
| | 20 | 236 | | status: 0, |
| | 20 | 237 | | headers: String::new(), |
| | 20 | 238 | | body: String::new(), |
| | 20 | 239 | | } |
| | 1260 | 240 | | } |