1use tauri::{Manager, Runtime};
2use victauri_core::WindowState;
3
4pub trait WebviewBridge: Send + Sync {
8 fn eval_webview(&self, label: Option<&str>, script: &str) -> Result<(), String>;
14 fn get_window_states(&self, label: Option<&str>) -> Vec<WindowState>;
16 fn list_window_labels(&self) -> Vec<String>;
18 fn get_native_handle(&self, label: Option<&str>) -> Result<isize, String>;
25 fn manage_window(&self, label: Option<&str>, action: &str) -> Result<String, String>;
31 fn resize_window(&self, label: Option<&str>, width: u32, height: u32) -> Result<(), String>;
37 fn move_window(&self, label: Option<&str>, x: i32, y: i32) -> Result<(), String>;
43 fn set_window_title(&self, label: Option<&str>, title: &str) -> Result<(), String>;
49
50 fn native_type_text(&self, _label: Option<&str>, _text: &str) -> Result<(), String> {
64 Err(
65 "native (trusted) keyboard input is not implemented on this platform; \
66 use synthetic input via the `input` tool without `trusted`"
67 .to_string(),
68 )
69 }
70
71 fn native_key(&self, _label: Option<&str>, _key: &str) -> Result<(), String> {
77 Err(
78 "native (trusted) key input is not implemented on this platform; \
79 use synthetic input via the `input` tool without `trusted`"
80 .to_string(),
81 )
82 }
83
84 fn native_click(&self, _label: Option<&str>, _x: f64, _y: f64) -> Result<(), String> {
90 Err(
91 "native (trusted) mouse input is not implemented on this platform; \
92 use synthetic input via the `interact` tool"
93 .to_string(),
94 )
95 }
96
97 fn app_data_dir(&self) -> Result<std::path::PathBuf, String> {
105 Err("backend access not available".to_string())
106 }
107
108 fn app_config_dir(&self) -> Result<std::path::PathBuf, String> {
114 Err("backend access not available".to_string())
115 }
116
117 fn app_log_dir(&self) -> Result<std::path::PathBuf, String> {
123 Err("backend access not available".to_string())
124 }
125
126 fn app_local_data_dir(&self) -> Result<std::path::PathBuf, String> {
132 Err("backend access not available".to_string())
133 }
134
135 #[must_use]
137 fn tauri_config(&self) -> serde_json::Value {
138 serde_json::Value::Null
139 }
140}
141
142fn find_window<'a, R: Runtime>(
143 windows: &'a std::collections::HashMap<String, tauri::WebviewWindow<R>>,
144 label: Option<&str>,
145) -> Result<&'a tauri::WebviewWindow<R>, String> {
146 match label {
147 Some(l) => windows
148 .get(l)
149 .ok_or_else(|| format!("window not found: {l}")),
150 None => windows
151 .get("main")
152 .or_else(|| windows.values().find(|w| w.is_visible().unwrap_or(false)))
153 .or_else(|| windows.values().next())
154 .ok_or_else(|| "no window available".to_string()),
155 }
156}
157
158impl<R: Runtime> WebviewBridge for tauri::AppHandle<R> {
159 fn eval_webview(&self, label: Option<&str>, script: &str) -> Result<(), String> {
160 let windows = self.webview_windows();
161 let webview = find_window(&windows, label)?;
162 webview.eval(script).map_err(|e| e.to_string())
163 }
164
165 fn get_window_states(&self, label: Option<&str>) -> Vec<WindowState> {
166 let windows = self.webview_windows();
167 let mut states = Vec::new();
168
169 for (win_label, window) in &windows {
170 if let Some(filter) = label
171 && win_label != filter
172 {
173 continue;
174 }
175
176 let pos = window.outer_position().unwrap_or_default();
177 let size = window.inner_size().unwrap_or_default();
178
179 states.push(WindowState {
180 label: win_label.clone(),
181 title: window.title().unwrap_or_default(),
182 url: window.url().map(|u| u.to_string()).unwrap_or_default(),
183 visible: window.is_visible().unwrap_or(false),
184 focused: window.is_focused().unwrap_or(false),
185 maximized: window.is_maximized().unwrap_or(false),
186 minimized: window.is_minimized().unwrap_or(false),
187 fullscreen: window.is_fullscreen().unwrap_or(false),
188 position: (pos.x, pos.y),
189 size: (size.width, size.height),
190 });
191 }
192
193 states
194 }
195
196 fn list_window_labels(&self) -> Vec<String> {
197 self.webview_windows().keys().cloned().collect()
198 }
199
200 fn get_native_handle(&self, label: Option<&str>) -> Result<isize, String> {
201 use raw_window_handle::{HasWindowHandle, RawWindowHandle};
202
203 let windows = self.webview_windows();
204 let _webview = find_window(&windows, label)?;
205 let handle = _webview.window_handle().map_err(|e| e.to_string())?;
206 match handle.as_raw() {
207 #[cfg(windows)]
208 RawWindowHandle::Win32(h) => Ok(h.hwnd.get()),
209 #[cfg(target_os = "macos")]
210 RawWindowHandle::AppKit(h) => {
211 macos_window_number(h.ns_view.as_ptr())
214 }
215 #[cfg(target_os = "linux")]
216 RawWindowHandle::Xlib(h) => Ok(h.window as isize),
217 #[cfg(target_os = "linux")]
218 RawWindowHandle::Xcb(h) => Ok(h.window.get() as isize),
219 _ => Err("unsupported window handle type on this platform".to_string()),
220 }
221 }
222
223 #[cfg(windows)]
224 fn native_type_text(&self, label: Option<&str>, text: &str) -> Result<(), String> {
225 let hwnd = self.get_native_handle(label)?;
226 win_focus(hwnd);
227 win_send_text(text)
228 }
229
230 #[cfg(windows)]
231 fn native_key(&self, label: Option<&str>, key: &str) -> Result<(), String> {
232 let hwnd = self.get_native_handle(label)?;
233 win_focus(hwnd);
234 win_send_key(key)
235 }
236
237 #[cfg(windows)]
238 fn native_click(&self, label: Option<&str>, x: f64, y: f64) -> Result<(), String> {
239 let hwnd = self.get_native_handle(label)?;
240 win_focus(hwnd);
241 win_click(hwnd, x, y)
242 }
243
244 fn manage_window(&self, label: Option<&str>, action: &str) -> Result<String, String> {
245 let windows = self.webview_windows();
246 let window = find_window(&windows, label)?;
247
248 match action {
249 "minimize" => window.minimize().map_err(|e| e.to_string())?,
250 "unminimize" => window.unminimize().map_err(|e| e.to_string())?,
251 "maximize" => window.maximize().map_err(|e| e.to_string())?,
252 "unmaximize" => window.unmaximize().map_err(|e| e.to_string())?,
253 "close" => window.close().map_err(|e| e.to_string())?,
254 "focus" => window.set_focus().map_err(|e| e.to_string())?,
255 "show" => window.show().map_err(|e| e.to_string())?,
256 "hide" => window.hide().map_err(|e| e.to_string())?,
257 "fullscreen" => window.set_fullscreen(true).map_err(|e| e.to_string())?,
258 "unfullscreen" => window.set_fullscreen(false).map_err(|e| e.to_string())?,
259 "always_on_top" => window.set_always_on_top(true).map_err(|e| e.to_string())?,
260 "not_always_on_top" => window.set_always_on_top(false).map_err(|e| e.to_string())?,
261 _ => return Err(format!("unknown action: {action}")),
262 }
263
264 Ok(format!("{action} executed"))
265 }
266
267 fn resize_window(&self, label: Option<&str>, width: u32, height: u32) -> Result<(), String> {
268 let windows = self.webview_windows();
269 let window = find_window(&windows, label)?;
270
271 window
272 .set_size(tauri::LogicalSize::new(width, height))
273 .map_err(|e| e.to_string())
274 }
275
276 fn move_window(&self, label: Option<&str>, x: i32, y: i32) -> Result<(), String> {
277 let windows = self.webview_windows();
278 let window = find_window(&windows, label)?;
279
280 window
281 .set_position(tauri::LogicalPosition::new(x, y))
282 .map_err(|e| e.to_string())
283 }
284
285 fn set_window_title(&self, label: Option<&str>, title: &str) -> Result<(), String> {
286 let windows = self.webview_windows();
287 let window = find_window(&windows, label)?;
288
289 window.set_title(title).map_err(|e| e.to_string())
290 }
291
292 fn app_data_dir(&self) -> Result<std::path::PathBuf, String> {
293 self.path().app_data_dir().map_err(|e| e.to_string())
294 }
295
296 fn app_config_dir(&self) -> Result<std::path::PathBuf, String> {
297 self.path().app_config_dir().map_err(|e| e.to_string())
298 }
299
300 fn app_log_dir(&self) -> Result<std::path::PathBuf, String> {
301 self.path().app_log_dir().map_err(|e| e.to_string())
302 }
303
304 fn app_local_data_dir(&self) -> Result<std::path::PathBuf, String> {
305 self.path().app_local_data_dir().map_err(|e| e.to_string())
306 }
307
308 fn tauri_config(&self) -> serde_json::Value {
309 let config = self.config();
310
311 let windows: Vec<serde_json::Value> = config
312 .app
313 .windows
314 .iter()
315 .map(|w| {
316 serde_json::json!({
317 "label": w.label,
318 "title": w.title,
319 "url": format!("{}", w.url),
320 "width": w.width,
321 "height": w.height,
322 "visible": w.visible,
323 "resizable": w.resizable,
324 "fullscreen": w.fullscreen,
325 "decorations": w.decorations,
326 "transparent": w.transparent,
327 "always_on_top": w.always_on_top,
328 })
329 })
330 .collect();
331
332 let plugins: Vec<String> = config.plugins.0.keys().cloned().collect();
333
334 let security = serde_json::json!({
335 "csp": config.app.security.csp.as_ref().map(|c| format!("{c}")),
336 "freeze_prototype": config.app.security.freeze_prototype,
337 "capabilities": config.app.security.capabilities.iter().map(|c| {
338 match c {
339 tauri::utils::config::CapabilityEntry::Inlined(cap) => {
340 serde_json::json!({
341 "identifier": cap.identifier,
342 "description": cap.description,
343 "windows": cap.windows,
344 "webviews": cap.webviews,
345 "permissions": cap.permissions.iter().map(|p| format!("{p:?}")).collect::<Vec<_>>(),
346 "platforms": cap.platforms,
347 })
348 }
349 tauri::utils::config::CapabilityEntry::Reference(path) => {
350 serde_json::json!({ "reference": path })
351 }
352 }
353 }).collect::<Vec<_>>(),
354 });
355
356 serde_json::json!({
357 "identifier": config.identifier,
358 "product_name": config.product_name,
359 "version": config.version,
360 "windows": windows,
361 "plugins": plugins,
362 "security": security,
363 })
364 }
365}
366
367#[cfg(target_os = "macos")]
368#[allow(unsafe_code)]
369fn macos_window_number(ns_view: *mut std::ffi::c_void) -> Result<isize, String> {
370 unsafe extern "C" {
371 fn objc_msgSend(obj: *mut std::ffi::c_void, sel: *mut std::ffi::c_void) -> isize;
372 fn sel_registerName(name: *const std::ffi::c_char) -> *mut std::ffi::c_void;
373 }
374
375 if ns_view.is_null() {
376 return Err("null NSView handle".to_string());
377 }
378
379 unsafe {
383 let sel_window = sel_registerName(c"window".as_ptr());
384 let ns_window = objc_msgSend(ns_view, sel_window);
385 if ns_window == 0 {
386 return Err("NSView has no parent NSWindow".to_string());
387 }
388 let sel_window_number = sel_registerName(c"windowNumber".as_ptr());
389 let ns_window_ptr = ns_window as *mut std::ffi::c_void;
390 let window_number = objc_msgSend(ns_window_ptr, sel_window_number);
391 if window_number <= 0 {
392 return Err(format!("invalid CGWindowID: {window_number}"));
393 }
394 Ok(window_number)
395 }
396}
397
398#[cfg(windows)]
404fn win_hwnd(hwnd: isize) -> windows::Win32::Foundation::HWND {
405 windows::Win32::Foundation::HWND(hwnd as *mut core::ffi::c_void)
406}
407
408#[allow(unsafe_code)]
411#[cfg(windows)]
412fn win_focus(hwnd: isize) {
413 use windows::Win32::UI::WindowsAndMessaging::SetForegroundWindow;
414 unsafe {
417 let _ = SetForegroundWindow(win_hwnd(hwnd));
418 }
419 std::thread::sleep(std::time::Duration::from_millis(40));
420}
421
422#[cfg(windows)]
423fn win_keyboard_input(
424 vk: u16,
425 scan: u16,
426 key_up: bool,
427 unicode: bool,
428) -> windows::Win32::UI::Input::KeyboardAndMouse::INPUT {
429 use windows::Win32::UI::Input::KeyboardAndMouse::{
430 INPUT, INPUT_0, INPUT_KEYBOARD, KEYBD_EVENT_FLAGS, KEYBDINPUT, KEYEVENTF_KEYUP,
431 KEYEVENTF_UNICODE, VIRTUAL_KEY,
432 };
433 let mut flags = KEYBD_EVENT_FLAGS(0);
434 if unicode {
435 flags |= KEYEVENTF_UNICODE;
436 }
437 if key_up {
438 flags |= KEYEVENTF_KEYUP;
439 }
440 INPUT {
441 r#type: INPUT_KEYBOARD,
442 Anonymous: INPUT_0 {
443 ki: KEYBDINPUT {
444 wVk: VIRTUAL_KEY(vk),
445 wScan: scan,
446 dwFlags: flags,
447 time: 0,
448 dwExtraInfo: 0,
449 },
450 },
451 }
452}
453
454#[allow(unsafe_code)]
456#[cfg(windows)]
457fn win_send_text(text: &str) -> Result<(), String> {
458 use windows::Win32::UI::Input::KeyboardAndMouse::{INPUT, SendInput};
459 let mut inputs: Vec<INPUT> = Vec::new();
460 for unit in text.encode_utf16() {
461 inputs.push(win_keyboard_input(0, unit, false, true));
462 inputs.push(win_keyboard_input(0, unit, true, true));
463 }
464 if inputs.is_empty() {
465 return Ok(());
466 }
467 let cb = i32::try_from(std::mem::size_of::<INPUT>()).unwrap_or(0);
468 let sent = unsafe { SendInput(&inputs, cb) } as usize;
470 if sent == inputs.len() {
471 Ok(())
472 } else {
473 Err(format!(
474 "SendInput delivered {sent}/{} key events",
475 inputs.len()
476 ))
477 }
478}
479
480#[cfg(windows)]
482fn win_vk_for_key(key: &str) -> Option<u16> {
483 use windows::Win32::UI::Input::KeyboardAndMouse as k;
484 let vk = match key {
485 "Enter" | "Return" => k::VK_RETURN,
486 "Tab" => k::VK_TAB,
487 "Escape" | "Esc" => k::VK_ESCAPE,
488 "Backspace" => k::VK_BACK,
489 "Delete" | "Del" => k::VK_DELETE,
490 "ArrowUp" | "Up" => k::VK_UP,
491 "ArrowDown" | "Down" => k::VK_DOWN,
492 "ArrowLeft" | "Left" => k::VK_LEFT,
493 "ArrowRight" | "Right" => k::VK_RIGHT,
494 "Home" => k::VK_HOME,
495 "End" => k::VK_END,
496 "PageUp" => k::VK_PRIOR,
497 "PageDown" => k::VK_NEXT,
498 "Space" | " " => k::VK_SPACE,
499 "F1" => k::VK_F1,
500 "F2" => k::VK_F2,
501 "F3" => k::VK_F3,
502 "F4" => k::VK_F4,
503 "F5" => k::VK_F5,
504 "F6" => k::VK_F6,
505 "F7" => k::VK_F7,
506 "F8" => k::VK_F8,
507 "F9" => k::VK_F9,
508 "F10" => k::VK_F10,
509 "F11" => k::VK_F11,
510 "F12" => k::VK_F12,
511 _ => return None,
512 };
513 Some(vk.0)
514}
515
516#[allow(unsafe_code)]
518#[cfg(windows)]
519fn win_send_key(key: &str) -> Result<(), String> {
520 use windows::Win32::UI::Input::KeyboardAndMouse::{INPUT, SendInput};
521 let inputs: Vec<INPUT> = if let Some(vk) = win_vk_for_key(key) {
522 vec![
523 win_keyboard_input(vk, 0, false, false),
524 win_keyboard_input(vk, 0, true, false),
525 ]
526 } else {
527 let mut chars = key.chars();
529 let (Some(c), None) = (chars.next(), chars.next()) else {
530 return Err(format!(
531 "unknown key '{key}' (use a named key or a single character)"
532 ));
533 };
534 let mut buf = [0u16; 2];
535 let mut v = Vec::new();
536 for unit in c.encode_utf16(&mut buf) {
537 v.push(win_keyboard_input(0, *unit, false, true));
538 v.push(win_keyboard_input(0, *unit, true, true));
539 }
540 v
541 };
542 let cb = i32::try_from(std::mem::size_of::<INPUT>()).unwrap_or(0);
543 let sent = unsafe { SendInput(&inputs, cb) } as usize;
545 if sent == inputs.len() {
546 Ok(())
547 } else {
548 Err(format!(
549 "SendInput delivered {sent}/{} key events",
550 inputs.len()
551 ))
552 }
553}
554
555#[allow(unsafe_code)]
558#[cfg(windows)]
559fn win_click(hwnd: isize, x: f64, y: f64) -> Result<(), String> {
560 use windows::Win32::Foundation::POINT;
561 use windows::Win32::Graphics::Gdi::ClientToScreen;
562 use windows::Win32::UI::HiDpi::GetDpiForWindow;
563 use windows::Win32::UI::Input::KeyboardAndMouse::{
564 INPUT, INPUT_0, INPUT_MOUSE, MOUSE_EVENT_FLAGS, MOUSEEVENTF_ABSOLUTE, MOUSEEVENTF_LEFTDOWN,
565 MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_VIRTUALDESK, MOUSEINPUT, SendInput,
566 };
567 use windows::Win32::UI::WindowsAndMessaging::{
568 GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN,
569 SM_YVIRTUALSCREEN,
570 };
571 let h = win_hwnd(hwnd);
572 let (nx, ny) = unsafe {
575 let dpi = GetDpiForWindow(h);
576 let scale = if dpi == 0 { 1.0 } else { f64::from(dpi) / 96.0 };
577 let mut pt = POINT {
578 x: (x * scale) as i32,
579 y: (y * scale) as i32,
580 };
581 let _ = ClientToScreen(h, &mut pt);
582 let vx = GetSystemMetrics(SM_XVIRTUALSCREEN);
583 let vy = GetSystemMetrics(SM_YVIRTUALSCREEN);
584 let vw = GetSystemMetrics(SM_CXVIRTUALSCREEN);
585 let vh = GetSystemMetrics(SM_CYVIRTUALSCREEN);
586 if vw <= 1 || vh <= 1 {
587 return Err("virtual screen metrics unavailable".to_string());
588 }
589 let nx = ((f64::from(pt.x - vx)) * 65535.0 / f64::from(vw - 1)) as i32;
590 let ny = ((f64::from(pt.y - vy)) * 65535.0 / f64::from(vh - 1)) as i32;
591 (nx, ny)
592 };
593 let make = |flags: MOUSE_EVENT_FLAGS| INPUT {
594 r#type: INPUT_MOUSE,
595 Anonymous: INPUT_0 {
596 mi: MOUSEINPUT {
597 dx: nx,
598 dy: ny,
599 mouseData: 0,
600 dwFlags: flags,
601 time: 0,
602 dwExtraInfo: 0,
603 },
604 },
605 };
606 let base = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK;
607 let inputs = [
608 make(base | MOUSEEVENTF_MOVE),
609 make(base | MOUSEEVENTF_LEFTDOWN),
610 make(base | MOUSEEVENTF_LEFTUP),
611 ];
612 let cb = i32::try_from(std::mem::size_of::<INPUT>()).unwrap_or(0);
613 let sent = unsafe { SendInput(&inputs, cb) } as usize;
615 if sent == inputs.len() {
616 Ok(())
617 } else {
618 Err(format!(
619 "SendInput delivered {sent}/{} mouse events",
620 inputs.len()
621 ))
622 }
623}