1use tauri::{Manager, Runtime};
2use victauri_core::WindowState;
3
4pub trait WebviewBridge: Send + Sync {
14 fn eval_webview(&self, label: Option<&str>, script: &str) -> Result<(), String>;
20 fn get_window_states(&self, label: Option<&str>) -> Vec<WindowState>;
22 fn try_get_window_states(&self, label: Option<&str>) -> Result<Vec<WindowState>, String> {
30 Ok(self.get_window_states(label))
31 }
32 fn list_window_labels(&self) -> Vec<String>;
34 fn try_list_window_labels(&self) -> Result<Vec<String>, String> {
43 Ok(self.list_window_labels())
44 }
45 fn eval_webview_resolved(&self, label: Option<&str>, script: &str) -> Result<String, String> {
54 self.eval_webview(label, script)?;
55 Ok(label.map(str::to_string).unwrap_or_default())
56 }
57 fn get_native_handle(&self, label: Option<&str>) -> Result<isize, String>;
64 fn manage_window(&self, label: Option<&str>, action: &str) -> Result<String, String>;
70 fn resize_window(&self, label: Option<&str>, width: u32, height: u32) -> Result<(), String>;
76 fn move_window(&self, label: Option<&str>, x: i32, y: i32) -> Result<(), String>;
82 fn set_window_title(&self, label: Option<&str>, title: &str) -> Result<(), String>;
88
89 fn native_type_text(&self, _label: Option<&str>, _text: &str) -> Result<(), String> {
103 Err(
104 "native (trusted) keyboard input is not implemented on this platform; \
105 use synthetic input via the `input` tool without `trusted`"
106 .to_string(),
107 )
108 }
109
110 fn native_key(&self, _label: Option<&str>, _key: &str) -> Result<(), String> {
116 Err(
117 "native (trusted) key input is not implemented on this platform; \
118 use synthetic input via the `input` tool without `trusted`"
119 .to_string(),
120 )
121 }
122
123 fn native_click(&self, _label: Option<&str>, _x: f64, _y: f64) -> Result<(), String> {
129 Err(
130 "native (trusted) mouse input is not implemented on this platform; \
131 use synthetic input via the `interact` tool"
132 .to_string(),
133 )
134 }
135
136 fn app_data_dir(&self) -> Result<std::path::PathBuf, String> {
144 Err("backend access not available".to_string())
145 }
146
147 fn app_config_dir(&self) -> Result<std::path::PathBuf, String> {
153 Err("backend access not available".to_string())
154 }
155
156 fn app_log_dir(&self) -> Result<std::path::PathBuf, String> {
162 Err("backend access not available".to_string())
163 }
164
165 fn app_local_data_dir(&self) -> Result<std::path::PathBuf, String> {
171 Err("backend access not available".to_string())
172 }
173
174 #[must_use]
176 fn tauri_config(&self) -> serde_json::Value {
177 serde_json::Value::Null
178 }
179}
180
181fn find_window<'a, R: Runtime>(
182 windows: &'a std::collections::HashMap<String, tauri::WebviewWindow<R>>,
183 label: Option<&str>,
184) -> Result<&'a tauri::WebviewWindow<R>, String> {
185 match label {
186 Some(l) => windows
187 .get(l)
188 .ok_or_else(|| format!("window not found: {l}")),
189 None => windows
190 .get("main")
191 .or_else(|| {
195 let mut visible: Vec<_> = windows
196 .iter()
197 .filter(|(_, w)| w.is_visible().unwrap_or(false))
198 .collect();
199 visible.sort_by(|a, b| a.0.cmp(b.0));
200 visible.first().map(|(_, w)| *w)
201 })
202 .or_else(|| windows.iter().min_by(|a, b| a.0.cmp(b.0)).map(|(_, w)| w))
203 .ok_or_else(|| "no window available".to_string()),
204 }
205}
206
207pub(crate) fn default_window_label<R: Runtime>(app: &tauri::AppHandle<R>) -> Option<String> {
211 let windows = app.webview_windows();
212 find_window(&windows, None)
213 .ok()
214 .map(|window| window.label().to_string())
215}
216
217static MAIN_DISPATCH_LOCK: DispatchGate = DispatchGate::new();
249
250struct DispatchGate {
259 lock: std::sync::Mutex<()>,
260 in_flight: std::sync::atomic::AtomicUsize,
261}
262
263impl DispatchGate {
264 const fn new() -> Self {
265 Self {
266 lock: std::sync::Mutex::new(()),
267 in_flight: std::sync::atomic::AtomicUsize::new(0),
268 }
269 }
270}
271
272struct InFlightGuard(&'static DispatchGate);
275
276impl Drop for InFlightGuard {
277 fn drop(&mut self) {
278 self.0
279 .in_flight
280 .fetch_sub(1, std::sync::atomic::Ordering::AcqRel);
281 }
282}
283
284static MAIN_THREAD: std::sync::OnceLock<std::thread::ThreadId> = std::sync::OnceLock::new();
286
287fn record_main_thread() {
290 let _ = MAIN_THREAD.set(std::thread::current().id());
291}
292
293thread_local! {
294 static MAIN_APP: std::cell::RefCell<Option<Box<dyn std::any::Any>>> =
299 const { std::cell::RefCell::new(None) };
300}
301
302fn install_main_app<R: Runtime>(app: &tauri::AppHandle<R>) {
306 MAIN_APP.with(|slot| *slot.borrow_mut() = Some(Box::new(app.clone())));
307}
308
309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
311pub(crate) enum MainThreadAdoption {
312 Inline,
314 Posted,
316}
317
318pub(crate) fn adopt_main_thread<R: Runtime>(
335 app: &tauri::AppHandle<R>,
336) -> Result<MainThreadAdoption, String> {
337 adopt_on_main(
338 app,
339 |job| app.run_on_main_thread(job).map_err(|e| e.to_string()),
340 |main: &tauri::AppHandle<R>| {
341 record_main_thread();
342 install_main_app(main);
343 },
344 )
345}
346
347fn adopt_on_main<H: Clone + Send + 'static>(
351 handle: &H,
352 post: impl Fn(Box<dyn FnOnce() + Send>) -> Result<(), String>,
353 adopt: impl Fn(&H) + Send + 'static,
354) -> Result<MainThreadAdoption, String> {
355 use std::sync::atomic::{AtomicBool, Ordering};
356
357 let here = std::thread::current().id();
358 let ran_here = std::sync::Arc::new(AtomicBool::new(false));
359 let flag = std::sync::Arc::clone(&ran_here);
360 post(Box::new(move || {
363 if std::thread::current().id() == here {
364 flag.store(true, Ordering::Release);
365 }
366 }))?;
367 if ran_here.load(Ordering::Acquire) {
368 adopt(handle);
369 return Ok(MainThreadAdoption::Inline);
370 }
371 let owned = handle.clone();
372 post(Box::new(move || adopt(&owned)))?;
373 Ok(MainThreadAdoption::Posted)
374}
375
376fn main_app<R: Runtime>() -> Option<tauri::AppHandle<R>> {
378 MAIN_APP.with(|slot| {
379 slot.borrow()
380 .as_ref()
381 .and_then(|any| any.downcast_ref::<tauri::AppHandle<R>>())
382 .cloned()
383 })
384}
385
386fn is_main_thread() -> bool {
388 MAIN_THREAD
389 .get()
390 .is_some_and(|id| *id == std::thread::current().id())
391}
392
393fn on_main<R, T, F>(app: &tauri::AppHandle<R>, what: &str, f: F) -> Result<T, String>
448where
449 R: Runtime,
450 T: Send + 'static,
451 F: FnOnce(&tauri::AppHandle<R>) -> T + Send + 'static,
452{
453 let grace = MAIN_THREAD_TIMEOUT;
454 let timeout = start_timeout();
457
458 if is_main_thread() {
465 return std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(app)))
466 .map_err(|_| format!("{what} {PANICKED_ON_MAIN}"));
467 }
468 if timeout.is_zero() {
470 return Err(format!(
471 "{what} was not sent to the main thread: the call's deadline had passed (it will not \
472 run)"
473 ));
474 }
475
476 let round_trip = move || -> Result<T, String> {
477 let owned_what = what.to_string();
480 serialized_round_trip(
481 &MAIN_DISPATCH_LOCK,
482 what,
483 timeout,
484 grace,
485 |job| {
486 app.run_on_main_thread(job)
487 .map_err(|e| format!("failed to dispatch {what} to the main thread: {e}"))
488 },
489 move || {
490 main_app::<R>().map(|main| f(&main)).ok_or_else(|| {
491 format!(
492 "{owned_what}: the main-thread app handle is not installed (the plugin \
493 setup has not run)"
494 )
495 })
496 },
497 )?
498 };
499
500 match tokio::runtime::Handle::try_current().map(|h| h.runtime_flavor()) {
505 Ok(tokio::runtime::RuntimeFlavor::MultiThread) => tokio::task::block_in_place(round_trip),
506 _ => round_trip(),
507 }
508}
509
510fn serialized_round_trip<T, F>(
522 gate: &'static DispatchGate,
523 what: &str,
524 timeout: std::time::Duration,
525 grace: std::time::Duration,
526 post: impl FnOnce(Box<dyn FnOnce() + Send>) -> Result<(), String>,
527 f: F,
528) -> Result<T, String>
529where
530 T: Send + 'static,
531 F: FnOnce() -> T + Send + 'static,
532{
533 let deadline = std::time::Instant::now() + timeout;
534 let lock_timeout = || {
535 format!(
536 "{what} did not complete on the main thread: ran out of time waiting for the \
537 main-thread dispatch lock (it will not run)"
538 )
539 };
540 let _serialize = loop {
541 match gate.lock.try_lock() {
542 Ok(guard) => break guard,
543 Err(std::sync::TryLockError::Poisoned(poisoned)) => break poisoned.into_inner(),
544 Err(std::sync::TryLockError::WouldBlock) => {
545 let remaining = deadline.saturating_duration_since(std::time::Instant::now());
546 if remaining.is_zero() {
547 return Err(lock_timeout());
548 }
549 std::thread::sleep(remaining.min(std::time::Duration::from_millis(2)));
550 }
551 }
552 };
553 while gate.in_flight.load(std::sync::atomic::Ordering::Acquire) != 0 {
561 let remaining = deadline.saturating_duration_since(std::time::Instant::now());
562 if remaining.is_zero() {
563 return Err(format!(
564 "{what} did not complete on the main thread: timed out waiting for an earlier \
565 main-thread call that is still running (it will not run)"
566 ));
567 }
568 std::thread::sleep(remaining.min(std::time::Duration::from_millis(2)));
569 }
570 let remaining = deadline.saturating_duration_since(std::time::Instant::now());
571 if remaining.is_zero() {
572 return Err(lock_timeout());
573 }
574 let counted_post = move |job: Box<dyn FnOnce() + Send>| {
578 post(Box::new(move || {
579 gate.in_flight
580 .fetch_add(1, std::sync::atomic::Ordering::AcqRel);
581 let _done = InFlightGuard(gate);
582 job();
583 }))
584 };
585 dispatch_and_wait(what, remaining, grace, counted_post, f)
588}
589
590const MAIN_THREAD_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
593
594std::thread_local! {
595 static CALL_DEADLINE: std::cell::Cell<Option<std::time::Instant>> =
597 const { std::cell::Cell::new(None) };
598}
599
600pub(crate) struct CallBudget {
609 previous: Option<std::time::Instant>,
610 _same_thread: std::marker::PhantomData<*const ()>,
611}
612
613impl CallBudget {
614 pub(crate) fn until(deadline: std::time::Instant) -> Self {
616 let previous = CALL_DEADLINE.with(|d| d.replace(Some(deadline)));
617 Self {
618 previous,
619 _same_thread: std::marker::PhantomData,
620 }
621 }
622}
623
624impl Drop for CallBudget {
625 fn drop(&mut self) {
626 CALL_DEADLINE.with(|d| d.set(self.previous));
627 }
628}
629
630pub(crate) fn start_timeout() -> std::time::Duration {
632 CALL_DEADLINE
633 .with(std::cell::Cell::get)
634 .map_or(MAIN_THREAD_TIMEOUT, |deadline| {
635 deadline
636 .saturating_duration_since(std::time::Instant::now())
637 .min(MAIN_THREAD_TIMEOUT)
638 })
639}
640
641const JOB_QUEUED: u8 = 0;
643const JOB_RUNNING: u8 = 1;
645const JOB_ABANDONED: u8 = 2;
647
648fn dispatch_and_wait<T, F>(
658 what: &str,
659 remaining: std::time::Duration,
660 grace: std::time::Duration,
661 post: impl FnOnce(Box<dyn FnOnce() + Send>) -> Result<(), String>,
662 f: F,
663) -> Result<T, String>
664where
665 T: Send + 'static,
666 F: FnOnce() -> T + Send + 'static,
667{
668 use std::sync::atomic::{AtomicU8, Ordering};
669 use std::sync::mpsc::RecvTimeoutError;
670
671 let (tx, rx) = std::sync::mpsc::channel();
672 let state = std::sync::Arc::new(AtomicU8::new(JOB_QUEUED));
673 let job_state = std::sync::Arc::clone(&state);
674 post(Box::new(move || {
675 if job_state
676 .compare_exchange(JOB_QUEUED, JOB_RUNNING, Ordering::AcqRel, Ordering::Acquire)
677 .is_err()
678 {
679 return;
680 }
681 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
682 let _ = tx.send(result);
684 }))?;
685
686 let outcome = match rx.recv_timeout(remaining) {
687 Ok(outcome) => outcome,
688 Err(RecvTimeoutError::Disconnected) => {
689 return Err(format!(
690 "{what} did not complete on the main thread: the event loop dropped it"
691 ));
692 }
693 Err(RecvTimeoutError::Timeout) => {
694 if state
695 .compare_exchange(
696 JOB_QUEUED,
697 JOB_ABANDONED,
698 Ordering::AcqRel,
699 Ordering::Acquire,
700 )
701 .is_ok()
702 {
703 return Err(format!(
704 "{what} did not complete on the main thread: timed out before it started \
705 (it will not run)"
706 ));
707 }
708 rx.recv_timeout(grace)
710 .map_err(|_| format!("{what} {STARTED_OUTCOME_UNKNOWN}"))?
711 }
712 };
713 outcome.map_err(|_panic| format!("{what} {PANICKED_ON_MAIN}"))
714}
715
716const STARTED_OUTCOME_UNKNOWN: &str =
718 "started on the main thread but did not finish in time; its outcome is unknown";
719const PANICKED_ON_MAIN: &str = "panicked on the main thread";
721
722pub(crate) fn closure_may_have_run(error: &str) -> bool {
726 error.contains(STARTED_OUTCOME_UNKNOWN) || error.contains(PANICKED_ON_MAIN)
727}
728
729impl<R: Runtime> WebviewBridge for tauri::AppHandle<R> {
730 fn eval_webview(&self, label: Option<&str>, script: &str) -> Result<(), String> {
731 let label = label.map(str::to_string);
732 let script = script.to_string();
733 on_main(self, "eval_webview", move |app| {
734 let windows = app.webview_windows();
735 let webview = find_window(&windows, label.as_deref())?;
736 webview.eval(&script).map_err(|e| e.to_string())
737 })?
738 }
739
740 fn get_window_states(&self, label: Option<&str>) -> Vec<WindowState> {
741 self.try_get_window_states(label).unwrap_or_else(|e| {
744 tracing::warn!("get_window_states: {e}");
745 Vec::new()
746 })
747 }
748
749 fn try_get_window_states(&self, label: Option<&str>) -> Result<Vec<WindowState>, String> {
750 let label = label.map(str::to_string);
751 on_main(self, "get_window_states", move |app| {
752 let windows = app.webview_windows();
753 let mut states = Vec::new();
754
755 for (win_label, window) in &windows {
756 if let Some(filter) = label.as_deref()
757 && win_label != filter
758 {
759 continue;
760 }
761
762 let pos = window.outer_position().unwrap_or_default();
763 let size = window.inner_size().unwrap_or_default();
764
765 states.push(
766 WindowState::new(win_label.clone())
767 .with_title(window.title().unwrap_or_default())
768 .with_url(window.url().map(|u| u.to_string()).unwrap_or_default())
769 .with_visible(window.is_visible().unwrap_or(false))
770 .with_focused(window.is_focused().unwrap_or(false))
771 .with_maximized(window.is_maximized().unwrap_or(false))
772 .with_minimized(window.is_minimized().unwrap_or(false))
773 .with_fullscreen(window.is_fullscreen().unwrap_or(false))
774 .with_position(pos.x, pos.y)
775 .with_size(size.width, size.height),
776 );
777 }
778
779 states
780 })
781 }
782
783 fn list_window_labels(&self) -> Vec<String> {
784 self.try_list_window_labels().unwrap_or_else(|e| {
785 tracing::warn!("list_window_labels: {e}");
786 Vec::new()
787 })
788 }
789
790 fn try_list_window_labels(&self) -> Result<Vec<String>, String> {
791 on_main(self, "list_window_labels", |app| {
792 app.webview_windows().keys().cloned().collect()
793 })
794 }
795
796 fn eval_webview_resolved(&self, label: Option<&str>, script: &str) -> Result<String, String> {
797 let label = label.map(str::to_string);
798 let script = script.to_string();
799 on_main(self, "eval_webview", move |app| {
800 let windows = app.webview_windows();
801 let webview = find_window(&windows, label.as_deref())?;
802 webview.eval(&script).map_err(|e| e.to_string())?;
803 Ok(webview.label().to_string())
804 })?
805 }
806
807 fn get_native_handle(&self, label: Option<&str>) -> Result<isize, String> {
808 let label = label.map(str::to_string);
809 on_main(self, "get_native_handle", move |app| {
810 use raw_window_handle::{HasWindowHandle, RawWindowHandle};
811
812 let windows = app.webview_windows();
813 let _webview = find_window(&windows, label.as_deref())?;
814 let handle = _webview.window_handle().map_err(|e| e.to_string())?;
815 match handle.as_raw() {
816 #[cfg(windows)]
817 RawWindowHandle::Win32(h) => Ok(h.hwnd.get()),
818 #[cfg(target_os = "macos")]
819 RawWindowHandle::AppKit(h) => {
820 macos_window_number(h.ns_view.as_ptr())
823 }
824 #[cfg(target_os = "linux")]
825 RawWindowHandle::Xlib(h) => Ok(h.window as isize),
826 #[cfg(target_os = "linux")]
827 RawWindowHandle::Xcb(h) => Ok(h.window.get() as isize),
828 _ => Err("unsupported window handle type on this platform".to_string()),
829 }
830 })?
831 }
832
833 #[cfg(windows)]
834 fn native_type_text(&self, label: Option<&str>, text: &str) -> Result<(), String> {
835 let hwnd = self.get_native_handle(label)?;
836 win_focus(hwnd)?;
837 win_send_text(text)
838 }
839
840 #[cfg(windows)]
841 fn native_key(&self, label: Option<&str>, key: &str) -> Result<(), String> {
842 let hwnd = self.get_native_handle(label)?;
843 win_focus(hwnd)?;
844 win_send_key(key)
845 }
846
847 #[cfg(windows)]
848 fn native_click(&self, label: Option<&str>, x: f64, y: f64) -> Result<(), String> {
849 let hwnd = self.get_native_handle(label)?;
850 win_focus(hwnd)?;
851 win_click(hwnd, x, y)
852 }
853
854 fn manage_window(&self, label: Option<&str>, action: &str) -> Result<String, String> {
855 let label = label.map(str::to_string);
856 let action = action.to_string();
857 on_main(self, "manage_window", move |app| {
858 let windows = app.webview_windows();
859 let window = find_window(&windows, label.as_deref())?;
860
861 match action.as_str() {
862 "minimize" => window.minimize().map_err(|e| e.to_string())?,
863 "unminimize" => window.unminimize().map_err(|e| e.to_string())?,
864 "maximize" => window.maximize().map_err(|e| e.to_string())?,
865 "unmaximize" => window.unmaximize().map_err(|e| e.to_string())?,
866 "close" => window.close().map_err(|e| e.to_string())?,
867 "focus" => window.set_focus().map_err(|e| e.to_string())?,
868 "show" => window.show().map_err(|e| e.to_string())?,
869 "hide" => window.hide().map_err(|e| e.to_string())?,
870 "fullscreen" => window.set_fullscreen(true).map_err(|e| e.to_string())?,
871 "unfullscreen" => window.set_fullscreen(false).map_err(|e| e.to_string())?,
872 "always_on_top" => window.set_always_on_top(true).map_err(|e| e.to_string())?,
873 "not_always_on_top" => {
874 window.set_always_on_top(false).map_err(|e| e.to_string())?;
875 }
876 _ => return Err(format!("unknown action: {action}")),
877 }
878
879 Ok(format!("{action} executed"))
880 })?
881 }
882
883 fn resize_window(&self, label: Option<&str>, width: u32, height: u32) -> Result<(), String> {
884 let label = label.map(str::to_string);
885 on_main(self, "resize_window", move |app| {
886 let windows = app.webview_windows();
887 let window = find_window(&windows, label.as_deref())?;
888
889 window
890 .set_size(tauri::LogicalSize::new(width, height))
891 .map_err(|e| e.to_string())
892 })?
893 }
894
895 fn move_window(&self, label: Option<&str>, x: i32, y: i32) -> Result<(), String> {
896 let label = label.map(str::to_string);
897 on_main(self, "move_window", move |app| {
898 let windows = app.webview_windows();
899 let window = find_window(&windows, label.as_deref())?;
900
901 window
902 .set_position(tauri::LogicalPosition::new(x, y))
903 .map_err(|e| e.to_string())
904 })?
905 }
906
907 fn set_window_title(&self, label: Option<&str>, title: &str) -> Result<(), String> {
908 let label = label.map(str::to_string);
909 let title = title.to_string();
910 on_main(self, "set_window_title", move |app| {
911 let windows = app.webview_windows();
912 let window = find_window(&windows, label.as_deref())?;
913
914 window.set_title(&title).map_err(|e| e.to_string())
915 })?
916 }
917
918 fn app_data_dir(&self) -> Result<std::path::PathBuf, String> {
919 self.path().app_data_dir().map_err(|e| e.to_string())
920 }
921
922 fn app_config_dir(&self) -> Result<std::path::PathBuf, String> {
923 self.path().app_config_dir().map_err(|e| e.to_string())
924 }
925
926 fn app_log_dir(&self) -> Result<std::path::PathBuf, String> {
927 self.path().app_log_dir().map_err(|e| e.to_string())
928 }
929
930 fn app_local_data_dir(&self) -> Result<std::path::PathBuf, String> {
931 self.path().app_local_data_dir().map_err(|e| e.to_string())
932 }
933
934 fn tauri_config(&self) -> serde_json::Value {
935 let config = self.config();
936
937 let windows: Vec<serde_json::Value> = config
938 .app
939 .windows
940 .iter()
941 .map(|w| {
942 serde_json::json!({
943 "label": w.label,
944 "title": w.title,
945 "url": format!("{}", w.url),
946 "width": w.width,
947 "height": w.height,
948 "visible": w.visible,
949 "resizable": w.resizable,
950 "fullscreen": w.fullscreen,
951 "decorations": w.decorations,
952 "transparent": w.transparent,
953 "always_on_top": w.always_on_top,
954 })
955 })
956 .collect();
957
958 let plugins: Vec<String> = config.plugins.0.keys().cloned().collect();
959
960 let security = serde_json::json!({
961 "csp": config.app.security.csp.as_ref().map(|c| format!("{c}")),
962 "freeze_prototype": config.app.security.freeze_prototype,
963 "capabilities": config.app.security.capabilities.iter().map(|c| {
964 match c {
965 tauri::utils::config::CapabilityEntry::Inlined(cap) => {
966 serde_json::json!({
967 "identifier": cap.identifier,
968 "description": cap.description,
969 "windows": cap.windows,
970 "webviews": cap.webviews,
971 "permissions": cap.permissions.iter().map(|p| format!("{p:?}")).collect::<Vec<_>>(),
972 "platforms": cap.platforms,
973 })
974 }
975 tauri::utils::config::CapabilityEntry::Reference(path) => {
976 serde_json::json!({ "reference": path })
977 }
978 }
979 }).collect::<Vec<_>>(),
980 });
981
982 serde_json::json!({
983 "identifier": config.identifier,
984 "product_name": config.product_name,
985 "version": config.version,
986 "windows": windows,
987 "plugins": plugins,
988 "security": security,
989 })
990 }
991}
992
993#[cfg(target_os = "macos")]
994#[allow(unsafe_code)]
995fn macos_window_number(ns_view: *mut std::ffi::c_void) -> Result<isize, String> {
996 unsafe extern "C" {
997 fn objc_msgSend(obj: *mut std::ffi::c_void, sel: *mut std::ffi::c_void) -> isize;
998 fn sel_registerName(name: *const std::ffi::c_char) -> *mut std::ffi::c_void;
999 }
1000
1001 if ns_view.is_null() {
1002 return Err("null NSView handle".to_string());
1003 }
1004
1005 unsafe {
1009 let sel_window = sel_registerName(c"window".as_ptr());
1010 let ns_window = objc_msgSend(ns_view, sel_window);
1011 if ns_window == 0 {
1012 return Err("NSView has no parent NSWindow".to_string());
1013 }
1014 let sel_window_number = sel_registerName(c"windowNumber".as_ptr());
1015 let ns_window_ptr = ns_window as *mut std::ffi::c_void;
1016 let window_number = objc_msgSend(ns_window_ptr, sel_window_number);
1017 if window_number <= 0 {
1018 return Err(format!("invalid CGWindowID: {window_number}"));
1019 }
1020 Ok(window_number)
1021 }
1022}
1023
1024#[cfg(windows)]
1030fn win_hwnd(hwnd: isize) -> windows::Win32::Foundation::HWND {
1031 windows::Win32::Foundation::HWND(hwnd as *mut core::ffi::c_void)
1032}
1033
1034#[cfg(any(windows, test))]
1037fn foreground_verdict(target_root: isize, foreground_root: isize) -> Result<(), String> {
1038 if target_root != 0 && target_root == foreground_root {
1039 Ok(())
1040 } else {
1041 Err(
1042 "refusing trusted input: the app window could not be brought to the foreground \
1043 (Windows' foreground lock keeps focus with the app the user is working in, e.g. a \
1044 terminal), so the keystrokes or click would go to that app instead. Bring the app \
1045 window to the front and retry, or omit `trusted` to use synthetic input."
1046 .to_string(),
1047 )
1048 }
1049}
1050
1051#[cfg(any(windows, test))]
1053fn click_target_verdict(target_root: isize, point_root: isize) -> Result<(), String> {
1054 if target_root != 0 && target_root == point_root {
1055 Ok(())
1056 } else {
1057 Err(
1058 "refusing trusted click: the point is covered by another window (or lies outside \
1059 this one), so the click would land there. Make the element visible on screen and \
1060 retry, or omit `trusted` to use a synthetic click."
1061 .to_string(),
1062 )
1063 }
1064}
1065
1066#[cfg(any(windows, test))]
1072fn client_point_verdict(x: f64, y: f64, client_w: i32, client_h: i32) -> Result<(), String> {
1073 let inside = x.is_finite()
1074 && y.is_finite()
1075 && x >= 0.0
1076 && y >= 0.0
1077 && x < f64::from(client_w)
1078 && y < f64::from(client_h);
1079 if inside {
1080 Ok(())
1081 } else {
1082 Err(format!(
1083 "refusing trusted click: ({x}, {y}) lies outside the window's content area \
1084 ({client_w}x{client_h} physical px), so it would land on the title bar, a caption \
1085 button or the border. Scroll the element into view and retry, or omit `trusted` to \
1086 use a synthetic click."
1087 ))
1088 }
1089}
1090
1091#[allow(unsafe_code)]
1093#[cfg(windows)]
1094fn win_root(hwnd: windows::Win32::Foundation::HWND) -> isize {
1095 use windows::Win32::UI::WindowsAndMessaging::{GA_ROOT, GetAncestor};
1096 unsafe { GetAncestor(hwnd, GA_ROOT) }.0 as isize
1098}
1099
1100#[allow(unsafe_code)]
1105#[cfg(windows)]
1106fn win_focus(hwnd: isize) -> Result<(), String> {
1107 use windows::Win32::UI::WindowsAndMessaging::{GetForegroundWindow, SetForegroundWindow};
1108 let target = win_hwnd(hwnd);
1109 let target_root = win_root(target);
1110 unsafe {
1113 let _ = SetForegroundWindow(target);
1114 }
1115 std::thread::sleep(std::time::Duration::from_millis(40));
1117 let mut verdict = Ok(());
1118 for _ in 0..8 {
1119 verdict = foreground_verdict(target_root, win_root(unsafe { GetForegroundWindow() }));
1121 if verdict.is_ok() {
1122 break;
1123 }
1124 std::thread::sleep(std::time::Duration::from_millis(20));
1125 }
1126 verdict
1127}
1128
1129#[cfg(windows)]
1130fn win_keyboard_input(
1131 vk: u16,
1132 scan: u16,
1133 key_up: bool,
1134 unicode: bool,
1135) -> windows::Win32::UI::Input::KeyboardAndMouse::INPUT {
1136 use windows::Win32::UI::Input::KeyboardAndMouse::{
1137 INPUT, INPUT_0, INPUT_KEYBOARD, KEYBD_EVENT_FLAGS, KEYBDINPUT, KEYEVENTF_KEYUP,
1138 KEYEVENTF_UNICODE, VIRTUAL_KEY,
1139 };
1140 let mut flags = KEYBD_EVENT_FLAGS(0);
1141 if unicode {
1142 flags |= KEYEVENTF_UNICODE;
1143 }
1144 if key_up {
1145 flags |= KEYEVENTF_KEYUP;
1146 }
1147 INPUT {
1148 r#type: INPUT_KEYBOARD,
1149 Anonymous: INPUT_0 {
1150 ki: KEYBDINPUT {
1151 wVk: VIRTUAL_KEY(vk),
1152 wScan: scan,
1153 dwFlags: flags,
1154 time: 0,
1155 dwExtraInfo: 0,
1156 },
1157 },
1158 }
1159}
1160
1161#[allow(unsafe_code)]
1163#[cfg(windows)]
1164fn win_send_text(text: &str) -> Result<(), String> {
1165 use windows::Win32::UI::Input::KeyboardAndMouse::{INPUT, SendInput};
1166 let mut inputs: Vec<INPUT> = Vec::new();
1167 for unit in text.encode_utf16() {
1168 inputs.push(win_keyboard_input(0, unit, false, true));
1169 inputs.push(win_keyboard_input(0, unit, true, true));
1170 }
1171 if inputs.is_empty() {
1172 return Ok(());
1173 }
1174 let cb = i32::try_from(std::mem::size_of::<INPUT>()).unwrap_or(0);
1175 let sent = unsafe { SendInput(&inputs, cb) } as usize;
1177 if sent == inputs.len() {
1178 Ok(())
1179 } else {
1180 Err(format!(
1181 "SendInput delivered {sent}/{} key events",
1182 inputs.len()
1183 ))
1184 }
1185}
1186
1187#[cfg(windows)]
1189fn win_vk_for_key(key: &str) -> Option<u16> {
1190 use windows::Win32::UI::Input::KeyboardAndMouse as k;
1191 let vk = match key {
1192 "Enter" | "Return" => k::VK_RETURN,
1193 "Tab" => k::VK_TAB,
1194 "Escape" | "Esc" => k::VK_ESCAPE,
1195 "Backspace" => k::VK_BACK,
1196 "Delete" | "Del" => k::VK_DELETE,
1197 "ArrowUp" | "Up" => k::VK_UP,
1198 "ArrowDown" | "Down" => k::VK_DOWN,
1199 "ArrowLeft" | "Left" => k::VK_LEFT,
1200 "ArrowRight" | "Right" => k::VK_RIGHT,
1201 "Home" => k::VK_HOME,
1202 "End" => k::VK_END,
1203 "PageUp" => k::VK_PRIOR,
1204 "PageDown" => k::VK_NEXT,
1205 "Space" | " " => k::VK_SPACE,
1206 "F1" => k::VK_F1,
1207 "F2" => k::VK_F2,
1208 "F3" => k::VK_F3,
1209 "F4" => k::VK_F4,
1210 "F5" => k::VK_F5,
1211 "F6" => k::VK_F6,
1212 "F7" => k::VK_F7,
1213 "F8" => k::VK_F8,
1214 "F9" => k::VK_F9,
1215 "F10" => k::VK_F10,
1216 "F11" => k::VK_F11,
1217 "F12" => k::VK_F12,
1218 _ => return None,
1219 };
1220 Some(vk.0)
1221}
1222
1223#[allow(unsafe_code)]
1225#[cfg(windows)]
1226fn win_send_key(key: &str) -> Result<(), String> {
1227 use windows::Win32::UI::Input::KeyboardAndMouse::{INPUT, SendInput};
1228 let inputs: Vec<INPUT> = if let Some(vk) = win_vk_for_key(key) {
1229 vec![
1230 win_keyboard_input(vk, 0, false, false),
1231 win_keyboard_input(vk, 0, true, false),
1232 ]
1233 } else {
1234 let mut chars = key.chars();
1236 let (Some(c), None) = (chars.next(), chars.next()) else {
1237 return Err(format!(
1238 "unknown key '{key}' (use a named key or a single character)"
1239 ));
1240 };
1241 let mut buf = [0u16; 2];
1242 let mut v = Vec::new();
1243 for unit in c.encode_utf16(&mut buf) {
1244 v.push(win_keyboard_input(0, *unit, false, true));
1245 v.push(win_keyboard_input(0, *unit, true, true));
1246 }
1247 v
1248 };
1249 let cb = i32::try_from(std::mem::size_of::<INPUT>()).unwrap_or(0);
1250 let sent = unsafe { SendInput(&inputs, cb) } as usize;
1252 if sent == inputs.len() {
1253 Ok(())
1254 } else {
1255 Err(format!(
1256 "SendInput delivered {sent}/{} key events",
1257 inputs.len()
1258 ))
1259 }
1260}
1261
1262#[allow(unsafe_code)]
1265#[cfg(windows)]
1266fn win_click(hwnd: isize, x: f64, y: f64) -> Result<(), String> {
1267 use windows::Win32::Foundation::POINT;
1268 use windows::Win32::Foundation::RECT;
1269 use windows::Win32::Graphics::Gdi::ClientToScreen;
1270 use windows::Win32::UI::HiDpi::GetDpiForWindow;
1271 use windows::Win32::UI::Input::KeyboardAndMouse::{
1272 INPUT, INPUT_0, INPUT_MOUSE, MOUSE_EVENT_FLAGS, MOUSEEVENTF_ABSOLUTE, MOUSEEVENTF_LEFTDOWN,
1273 MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_VIRTUALDESK, MOUSEINPUT, SendInput,
1274 };
1275 use windows::Win32::UI::WindowsAndMessaging::{
1276 GetClientRect, GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN,
1277 SM_YVIRTUALSCREEN, WindowFromPoint,
1278 };
1279 let h = win_hwnd(hwnd);
1280 let (nx, ny) = unsafe {
1283 let dpi = GetDpiForWindow(h);
1284 let scale = if dpi == 0 { 1.0 } else { f64::from(dpi) / 96.0 };
1285 let (px, py) = (x * scale, y * scale);
1286 let mut client = RECT::default();
1288 GetClientRect(h, &mut client)
1289 .map_err(|e| format!("refusing trusted click: cannot read the client area: {e}"))?;
1290 client_point_verdict(px, py, client.right, client.bottom)?;
1291 let mut pt = POINT {
1292 x: px as i32,
1293 y: py as i32,
1294 };
1295 let _ = ClientToScreen(h, &mut pt);
1296 click_target_verdict(win_root(h), win_root(WindowFromPoint(pt)))?;
1298 let vx = GetSystemMetrics(SM_XVIRTUALSCREEN);
1299 let vy = GetSystemMetrics(SM_YVIRTUALSCREEN);
1300 let vw = GetSystemMetrics(SM_CXVIRTUALSCREEN);
1301 let vh = GetSystemMetrics(SM_CYVIRTUALSCREEN);
1302 if vw <= 1 || vh <= 1 {
1303 return Err("virtual screen metrics unavailable".to_string());
1304 }
1305 let nx = ((f64::from(pt.x - vx)) * 65535.0 / f64::from(vw - 1)) as i32;
1306 let ny = ((f64::from(pt.y - vy)) * 65535.0 / f64::from(vh - 1)) as i32;
1307 (nx, ny)
1308 };
1309 let make = |flags: MOUSE_EVENT_FLAGS| INPUT {
1310 r#type: INPUT_MOUSE,
1311 Anonymous: INPUT_0 {
1312 mi: MOUSEINPUT {
1313 dx: nx,
1314 dy: ny,
1315 mouseData: 0,
1316 dwFlags: flags,
1317 time: 0,
1318 dwExtraInfo: 0,
1319 },
1320 },
1321 };
1322 let base = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK;
1323 let inputs = [
1324 make(base | MOUSEEVENTF_MOVE),
1325 make(base | MOUSEEVENTF_LEFTDOWN),
1326 make(base | MOUSEEVENTF_LEFTUP),
1327 ];
1328 let cb = i32::try_from(std::mem::size_of::<INPUT>()).unwrap_or(0);
1329 let sent = unsafe { SendInput(&inputs, cb) } as usize;
1331 if sent == inputs.len() {
1332 Ok(())
1333 } else {
1334 Err(format!(
1335 "SendInput delivered {sent}/{} mouse events",
1336 inputs.len()
1337 ))
1338 }
1339}
1340
1341#[cfg(test)]
1342mod tests {
1343 use super::{dispatch_and_wait, is_main_thread, record_main_thread};
1344 use std::sync::Arc;
1345 use std::sync::atomic::{AtomicUsize, Ordering};
1346 use std::time::Duration;
1347
1348 fn post_after(delay: Duration) -> impl FnOnce(Box<dyn FnOnce() + Send>) -> Result<(), String> {
1350 move |job| {
1351 std::thread::spawn(move || {
1352 std::thread::sleep(delay);
1353 job();
1354 });
1355 Ok(())
1356 }
1357 }
1358
1359 fn post_started(
1365 started: std::sync::mpsc::Receiver<()>,
1366 ) -> impl FnOnce(Box<dyn FnOnce() + Send>) -> Result<(), String> {
1367 move |job| {
1368 std::thread::spawn(job);
1369 started
1370 .recv_timeout(Duration::from_secs(60))
1371 .map_err(|_| "the stand-in main thread never started the job".to_string())
1372 }
1373 }
1374
1375 #[test]
1376 fn a_job_still_queued_at_the_deadline_never_runs() {
1377 let ran = Arc::new(AtomicUsize::new(0));
1378 let r = Arc::clone(&ran);
1379 let out = dispatch_and_wait(
1380 "resize_window",
1381 Duration::from_millis(50),
1382 Duration::from_secs(5),
1383 post_after(Duration::from_millis(300)),
1384 move || r.fetch_add(1, Ordering::SeqCst),
1385 );
1386 assert!(out.unwrap_err().contains("will not run"));
1387 std::thread::sleep(Duration::from_millis(500));
1388 assert_eq!(ran.load(Ordering::SeqCst), 0, "an abandoned job ran anyway");
1389 }
1390
1391 #[test]
1392 fn a_job_running_at_the_deadline_reports_its_real_outcome() {
1393 let (started_tx, started_rx) = std::sync::mpsc::channel();
1396 let out = dispatch_and_wait(
1397 "eval_webview",
1398 Duration::from_millis(50),
1399 Duration::from_secs(5),
1400 post_started(started_rx),
1401 move || {
1402 let _ = started_tx.send(());
1403 std::thread::sleep(Duration::from_millis(300));
1404 7
1405 },
1406 );
1407 assert_eq!(out, Ok(7));
1408 }
1409
1410 #[test]
1411 fn trusted_input_is_refused_unless_the_target_is_in_front() {
1412 use super::{click_target_verdict, foreground_verdict};
1413 assert!(foreground_verdict(0x42, 0x42).is_ok());
1414 let other = foreground_verdict(0x42, 0x99).unwrap_err();
1415 assert!(other.contains("foreground"), "{other}");
1416 assert!(foreground_verdict(0x42, 0).is_err(), "no foreground window");
1417 assert!(foreground_verdict(0, 0).is_err(), "unknown target window");
1418 assert!(click_target_verdict(0x42, 0x42).is_ok());
1419 assert!(
1420 click_target_verdict(0x42, 0x99)
1421 .unwrap_err()
1422 .contains("covered")
1423 );
1424 assert!(click_target_verdict(0, 0).is_err());
1425 }
1426
1427 fn leaked_gate() -> &'static super::DispatchGate {
1428 Box::leak(Box::new(super::DispatchGate::new()))
1429 }
1430
1431 #[test]
1435 fn a_call_budget_caps_the_start_wait_and_restores_on_drop() {
1436 use super::{CallBudget, MAIN_THREAD_TIMEOUT, start_timeout};
1437 use std::time::Instant;
1438 assert_eq!(start_timeout(), MAIN_THREAD_TIMEOUT);
1439 {
1440 let _outer = CallBudget::until(Instant::now() + Duration::from_secs(5));
1441 assert!(start_timeout() <= Duration::from_secs(5));
1442 {
1443 let _inner = CallBudget::until(Instant::now() - Duration::from_millis(1));
1444 assert_eq!(
1445 start_timeout(),
1446 Duration::ZERO,
1447 "an expired budget waits for nothing"
1448 );
1449 }
1450 let back = start_timeout();
1451 assert!(
1452 back > Duration::ZERO && back <= Duration::from_secs(5),
1453 "the inner budget restored the outer one: {back:?}"
1454 );
1455 let _long = CallBudget::until(Instant::now() + Duration::from_secs(3600));
1456 assert_eq!(
1457 start_timeout(),
1458 MAIN_THREAD_TIMEOUT,
1459 "never above the default"
1460 );
1461 }
1462 assert_eq!(
1463 start_timeout(),
1464 MAIN_THREAD_TIMEOUT,
1465 "dropped: back to no budget"
1466 );
1467 let _here = CallBudget::until(Instant::now());
1469 let elsewhere = std::thread::spawn(start_timeout).join().unwrap();
1470 assert_eq!(elsewhere, MAIN_THREAD_TIMEOUT);
1471 }
1472
1473 #[test]
1476 fn a_round_trip_under_a_call_budget_gives_up_at_the_calls_deadline() {
1477 use super::{CallBudget, serialized_round_trip, start_timeout};
1478 use std::time::Instant;
1479 let gate = leaked_gate();
1480 let _budget = CallBudget::until(Instant::now() + Duration::from_millis(80));
1481 let begun = Instant::now();
1482 let parked: Arc<std::sync::Mutex<Vec<Box<dyn FnOnce() + Send>>>> = Arc::default();
1483 let park = Arc::clone(&parked);
1484 let refused = serialized_round_trip(
1485 gate,
1486 "budgeted",
1487 start_timeout(),
1488 Duration::from_secs(5),
1489 move |job| {
1490 park.lock().unwrap().push(job);
1491 Ok(())
1492 },
1493 || (),
1494 );
1495 assert!(refused.unwrap_err().contains("it will not run"));
1496 assert!(
1497 begun.elapsed() < Duration::from_secs(2),
1498 "{:?}",
1499 begun.elapsed()
1500 );
1501 }
1502
1503 #[test]
1506 fn a_short_start_budget_abandons_an_unstarted_job_but_waits_for_a_started_one() {
1507 use super::serialized_round_trip;
1508 use std::time::Instant;
1509 let gate = leaked_gate();
1510 let ran = Arc::new(std::sync::atomic::AtomicBool::new(false));
1511 let r = Arc::clone(&ran);
1512 let begun = Instant::now();
1513 let parked: Arc<std::sync::Mutex<Vec<Box<dyn FnOnce() + Send>>>> = Arc::default();
1515 let park = Arc::clone(&parked);
1516 let refused = serialized_round_trip(
1517 gate,
1518 "never-started",
1519 Duration::from_millis(80),
1520 Duration::from_secs(5),
1521 move |job| {
1522 park.lock().unwrap().push(job);
1523 Ok(())
1524 },
1525 move || r.store(true, std::sync::atomic::Ordering::SeqCst),
1526 );
1527 assert!(refused.unwrap_err().contains("it will not run"));
1528 assert!(
1529 begun.elapsed() < Duration::from_secs(2),
1530 "{:?}",
1531 begun.elapsed()
1532 );
1533 for job in parked.lock().unwrap().drain(..) {
1535 job();
1536 }
1537 assert!(!ran.load(std::sync::atomic::Ordering::SeqCst));
1538
1539 let (started_tx, started_rx) = std::sync::mpsc::channel();
1541 let slow = serialized_round_trip(
1542 gate,
1543 "started",
1544 Duration::from_millis(80),
1545 Duration::from_secs(5),
1546 post_started(started_rx),
1547 move || {
1548 let _ = started_tx.send(());
1549 std::thread::sleep(Duration::from_millis(200));
1550 "done"
1551 },
1552 );
1553 assert_eq!(slow.unwrap(), "done");
1554 }
1555
1556 #[test]
1561 fn a_job_outliving_its_caller_blocks_the_next_round_trip_until_it_finishes() {
1562 use super::serialized_round_trip;
1563 use std::time::Instant;
1564 let gate = leaked_gate();
1565 let first_end = Arc::new(std::sync::Mutex::new(None::<Instant>));
1566 let fe = Arc::clone(&first_end);
1567 let (started_tx, started_rx) = std::sync::mpsc::channel();
1570 let (release_tx, release_rx) = std::sync::mpsc::channel::<()>();
1571 let first = serialized_round_trip(
1572 gate,
1573 "first",
1574 Duration::from_millis(50),
1575 Duration::from_millis(50),
1576 post_started(started_rx),
1577 move || {
1578 let _ = started_tx.send(());
1579 let _ = release_rx.recv();
1580 *fe.lock().unwrap() = Some(Instant::now());
1581 },
1582 );
1583 assert!(
1584 first.unwrap_err().contains("outcome is unknown"),
1585 "precondition: the first caller gave up while its job was running"
1586 );
1587 let releaser = std::thread::spawn(move || {
1590 while gate.lock.try_lock().is_ok() {
1591 std::thread::sleep(Duration::from_millis(1));
1592 }
1593 std::thread::sleep(Duration::from_millis(20));
1594 release_tx.send(()).unwrap();
1595 });
1596 let second_start = serialized_round_trip(
1597 gate,
1598 "second",
1599 Duration::from_secs(5),
1600 Duration::from_secs(5),
1601 post_after(Duration::ZERO),
1602 Instant::now,
1603 )
1604 .expect("the second round trip runs once the first job is done");
1605 releaser.join().unwrap();
1606 let first_end = first_end
1607 .lock()
1608 .unwrap()
1609 .expect("the first job finished before the second started");
1610 assert!(
1611 second_start >= first_end,
1612 "second round trip started {:?} before the abandoned first job finished",
1613 first_end - second_start
1614 );
1615 }
1616
1617 #[test]
1620 fn waiting_for_an_abandoned_job_is_bounded_by_the_callers_deadline() {
1621 use super::serialized_round_trip;
1622 let gate = leaked_gate();
1623 let (started_tx, started_rx) = std::sync::mpsc::channel();
1624 let first = serialized_round_trip(
1625 gate,
1626 "first",
1627 Duration::from_millis(30),
1628 Duration::from_millis(30),
1629 post_started(started_rx),
1630 move || {
1631 let _ = started_tx.send(());
1632 std::thread::sleep(Duration::from_millis(1500));
1633 },
1634 );
1635 assert!(first.unwrap_err().contains("outcome is unknown"));
1636 let ran = Arc::new(AtomicUsize::new(0));
1637 let r = Arc::clone(&ran);
1638 let started = std::time::Instant::now();
1639 let second = serialized_round_trip(
1640 gate,
1641 "second",
1642 Duration::from_millis(200),
1643 Duration::from_millis(200),
1644 post_after(Duration::ZERO),
1645 move || r.fetch_add(1, Ordering::SeqCst),
1646 );
1647 let waited = started.elapsed();
1648 let err = second.unwrap_err();
1649 assert!(err.contains("still running"), "{err}");
1650 assert!(waited < Duration::from_millis(1000), "waited {waited:?}");
1651 std::thread::sleep(Duration::from_millis(1600));
1652 assert_eq!(
1653 ran.load(Ordering::SeqCst),
1654 0,
1655 "a caller that gave up ran its job"
1656 );
1657 }
1658
1659 #[test]
1662 fn trusted_click_must_land_inside_the_client_area() {
1663 use super::client_point_verdict;
1664 assert!(client_point_verdict(0.0, 0.0, 800, 600).is_ok());
1665 assert!(client_point_verdict(799.9, 599.9, 800, 600).is_ok());
1666 for (x, y) in [
1667 (-1.0, 10.0), (10.0, -30.0), (800.0, 10.0), (10.0, 600.0),
1671 (f64::NAN, 10.0), (10.0, f64::INFINITY),
1673 (1e12, 10.0), ] {
1675 let err = client_point_verdict(x, y, 800, 600).unwrap_err();
1676 assert!(err.contains("outside"), "({x}, {y}): {err}");
1677 }
1678 assert!(
1679 client_point_verdict(0.0, 0.0, 0, 0).is_err(),
1680 "empty client area"
1681 );
1682 }
1683
1684 #[test]
1685 fn a_panicking_job_is_an_error_not_a_hang() {
1686 let out: Result<(), String> = dispatch_and_wait(
1687 "get_native_handle",
1688 Duration::from_secs(5),
1689 Duration::from_secs(5),
1690 post_after(Duration::ZERO),
1691 || panic!("boom"),
1692 );
1693 assert!(out.unwrap_err().contains("panicked"));
1694 }
1695
1696 struct FakeMainThread {
1699 jobs: std::sync::mpsc::Sender<Box<dyn FnOnce() + Send>>,
1700 id: std::thread::ThreadId,
1701 }
1702
1703 impl FakeMainThread {
1704 fn start() -> Self {
1705 let (jobs, rx) = std::sync::mpsc::channel::<Box<dyn FnOnce() + Send>>();
1706 let (id_tx, id_rx) = std::sync::mpsc::channel();
1707 std::thread::spawn(move || {
1708 id_tx.send(std::thread::current().id()).unwrap();
1709 while let Ok(job) = rx.recv() {
1710 job();
1711 }
1712 });
1713 Self {
1714 jobs,
1715 id: id_rx.recv().unwrap(),
1716 }
1717 }
1718
1719 fn post(&self) -> impl Fn(Box<dyn FnOnce() + Send>) -> Result<(), String> + '_ {
1720 move |job| {
1721 if std::thread::current().id() == self.id {
1722 job();
1723 Ok(())
1724 } else {
1725 self.jobs.send(job).map_err(|e| e.to_string())
1726 }
1727 }
1728 }
1729
1730 fn run<T: Send + 'static>(&self, f: impl FnOnce() -> T + Send + 'static) -> T {
1732 let (tx, rx) = std::sync::mpsc::channel();
1733 self.jobs
1734 .send(Box::new(move || tx.send(f()).unwrap()))
1735 .unwrap();
1736 rx.recv().unwrap()
1737 }
1738 }
1739
1740 type Adopted = Arc<std::sync::Mutex<Vec<(std::thread::ThreadId, String)>>>;
1741
1742 fn recording_adopt(log: &Adopted) -> impl Fn(&String) + Send + 'static {
1743 let log = Arc::clone(log);
1744 move |h: &String| {
1745 log.lock()
1746 .unwrap()
1747 .push((std::thread::current().id(), h.clone()));
1748 }
1749 }
1750
1751 #[test]
1757 fn setup_off_the_main_thread_adopts_the_real_main_thread() {
1758 let main = FakeMainThread::start();
1759 let log: Adopted = Arc::default();
1760 let handle = "app".to_string();
1761 let outcome = super::adopt_on_main(&handle, main.post(), recording_adopt(&log));
1762 assert_eq!(outcome, Ok(super::MainThreadAdoption::Posted));
1763 main.run(|| ());
1765 let adopted = log.lock().unwrap().clone();
1766 assert_eq!(adopted, vec![(main.id, "app".to_string())]);
1767 }
1768
1769 #[test]
1771 fn setup_on_the_main_thread_adopts_inline() {
1772 let main = Arc::new(FakeMainThread::start());
1773 let log: Adopted = Arc::default();
1774 let (m, l) = (Arc::clone(&main), Arc::clone(&log));
1775 let outcome = main.run(move || {
1776 let outcome = super::adopt_on_main(&"app".to_string(), m.post(), recording_adopt(&l));
1777 let adopted_now = l.lock().unwrap().len();
1779 (outcome, adopted_now)
1780 });
1781 assert_eq!(outcome, (Ok(super::MainThreadAdoption::Inline), 1));
1782 assert_eq!(log.lock().unwrap()[0].0, main.id);
1783 }
1784
1785 #[test]
1786 fn main_thread_is_recorded_once_and_only_that_thread_matches() {
1787 std::thread::spawn(|| {
1790 record_main_thread();
1791 assert!(is_main_thread(), "the recording thread is the main thread");
1792 })
1793 .join()
1794 .unwrap();
1795 assert!(
1796 !is_main_thread(),
1797 "a different thread must not be treated as main"
1798 );
1799 std::thread::spawn(|| assert!(!is_main_thread()))
1800 .join()
1801 .unwrap();
1802 std::thread::spawn(record_main_thread).join().unwrap();
1804 assert!(!is_main_thread());
1805 }
1806}