1use crate::json::esc;
24use std::fmt;
25use std::path::PathBuf;
26use std::process::{Child, Command, Stdio};
27use std::sync::Mutex;
28use std::time::{Duration, Instant};
29
30#[cfg(target_os = "macos")]
31#[path = "offscreen_mac.rs"]
32mod native;
33#[cfg(windows)]
34#[path = "offscreen_win.rs"]
35mod native;
36#[cfg(not(any(target_os = "macos", windows)))]
37mod native {
38 use super::*;
39 pub struct Session;
40 pub fn end(_: Session) -> Result<(), OffscreenError> {
41 Err(OffscreenError::UnsupportedPlatform)
42 }
43 pub fn session_qualification(_: &Session) -> Result<QualificationReport, OffscreenError> {
44 Err(OffscreenError::UnsupportedPlatform)
45 }
46 pub fn report(_: &Session) -> SessionReport {
47 unreachable!("no session exists on this platform")
48 }
49}
50
51pub const ENV_OFFSCREEN: &str = "RIGHTKIT_OFFSCREEN";
53pub const ENV_REPORT: &str = "RIGHTKIT_OFFSCREEN_REPORT";
55pub const OFFSCREEN_GAP: f64 = 1000.0;
57pub const PLACEMENT_TOLERANCE: f64 = 1.0;
59
60#[derive(Debug, Clone, PartialEq)]
63pub enum OffscreenError {
64 UnsupportedPlatform,
65 NoDisplays,
66 InvalidSize,
67 NotMainThread,
68 NullWindow,
69 AlreadyActive,
70 NoActiveSession,
71 ConstrainOverrideUnavailable,
73 OcclusionSpiNotCompiled,
75 OcclusionSpiUnavailable,
77 ActivityUnavailable,
79 ThrottlingOptOutFailed(u32),
81 PlacementNotApplied {
82 requested: Rect,
83 actual: Rect,
84 },
85 RestoreFailed {
86 requested: Rect,
87 actual: Rect,
88 },
89 WindowHidden,
90 WindowMiniaturized,
91 WindowOnDisplay {
92 frame: Rect,
93 },
94 InvalidBackingScale,
95 WindowGone,
96 Native(String),
98 Launch(String),
100 ReportTimeout,
101 ChildExited(String),
102 AppReportedError(String),
103 BadReport(String),
104 IdentityMismatch {
105 child_pid: u32,
106 reported_pid: u32,
107 },
108 WindowNotFound {
109 pid: u32,
110 window_id: u64,
111 },
112}
113
114impl fmt::Display for OffscreenError {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 write!(f, "{self:?}")
117 }
118}
119impl std::error::Error for OffscreenError {}
120
121#[derive(Debug, Clone, Copy, PartialEq)]
127pub struct Rect {
128 pub x: f64,
129 pub y: f64,
130 pub w: f64,
131 pub h: f64,
132}
133
134impl Rect {
135 pub fn new(x: f64, y: f64, w: f64, h: f64) -> Self {
136 Self { x, y, w, h }
137 }
138 pub fn right(&self) -> f64 {
139 self.x + self.w
140 }
141 pub fn bottom(&self) -> f64 {
142 self.y + self.h
143 }
144 pub fn intersects(&self, o: &Rect) -> bool {
146 self.x < o.right() && o.x < self.right() && self.y < o.bottom() && o.y < self.bottom()
147 }
148 fn near(&self, o: &Rect, tol: f64) -> bool {
149 (self.x - o.x).abs() <= tol
150 && (self.y - o.y).abs() <= tol
151 && (self.w - o.w).abs() <= tol
152 && (self.h - o.h).abs() <= tol
153 }
154}
155
156pub fn display_union(displays: &[Rect]) -> Option<Rect> {
158 let first = displays.first()?;
159 let (mut x0, mut y0, mut x1, mut y1) = (first.x, first.y, first.right(), first.bottom());
160 for d in &displays[1..] {
161 x0 = x0.min(d.x);
162 y0 = y0.min(d.y);
163 x1 = x1.max(d.right());
164 y1 = y1.max(d.bottom());
165 }
166 Some(Rect::new(x0, y0, x1 - x0, y1 - y0))
167}
168
169pub fn offscreen_frame(displays: &[Rect], size: (f64, f64)) -> Result<Rect, OffscreenError> {
173 if !(size.0.is_finite() && size.1.is_finite() && size.0 > 0.0 && size.1 > 0.0) {
174 return Err(OffscreenError::InvalidSize);
175 }
176 if displays
177 .iter()
178 .any(|d| !(d.x.is_finite() && d.y.is_finite() && d.w.is_finite() && d.h.is_finite()))
179 {
180 return Err(OffscreenError::NoDisplays);
181 }
182 let u = display_union(displays).ok_or(OffscreenError::NoDisplays)?;
183 Ok(Rect::new(u.right() + OFFSCREEN_GAP, u.y, size.0, size.1))
184}
185
186#[derive(Debug, Clone, Copy, PartialEq)]
189pub enum SpiStatus {
190 Responded,
192 NotCompiled,
193 Unavailable,
194 NotApplicable,
196}
197
198#[derive(Debug, Clone, Copy, PartialEq)]
199pub enum Platform {
200 Mac,
201 Windows,
202}
203
204impl Platform {
205 pub fn current() -> Self {
206 if cfg!(windows) {
207 Platform::Windows
208 } else {
209 Platform::Mac
210 }
211 }
212}
213
214#[derive(Debug, Clone, PartialEq)]
216pub struct WindowState {
217 pub frame: Rect,
218 pub displays: Vec<Rect>,
219 pub visible: bool,
220 pub miniaturized: bool,
221 pub backing_scale: f64,
222 pub spi: SpiStatus,
223}
224
225#[derive(Debug, Clone, PartialEq)]
226pub struct Check {
227 pub name: &'static str,
228 pub error: Option<OffscreenError>,
229}
230
231#[derive(Debug, Clone, PartialEq)]
232pub struct QualificationReport {
233 pub platform: Platform,
234 pub checks: Vec<Check>,
235}
236
237impl QualificationReport {
238 pub fn passed(&self) -> bool {
239 self.checks.iter().all(|c| c.error.is_none())
240 }
241 pub fn failures(&self) -> Vec<&Check> {
242 self.checks.iter().filter(|c| c.error.is_some()).collect()
243 }
244 pub fn into_result(self) -> Result<Self, OffscreenError> {
246 match self.checks.iter().find_map(|c| c.error.clone()) {
247 Some(e) => Err(e),
248 None => Ok(self),
249 }
250 }
251}
252
253pub fn qualify(state: &WindowState, platform: Platform) -> QualificationReport {
255 let on_display = state.displays.iter().any(|d| d.intersects(&state.frame))
256 || display_union(&state.displays).is_some_and(|u| u.intersects(&state.frame));
257 let mut checks = vec![
258 Check {
259 name: "off_all_displays",
260 error: if state.displays.is_empty() {
261 Some(OffscreenError::NoDisplays)
262 } else if on_display {
263 Some(OffscreenError::WindowOnDisplay { frame: state.frame })
264 } else {
265 None
266 },
267 },
268 Check {
269 name: "visible",
270 error: (!state.visible).then_some(OffscreenError::WindowHidden),
271 },
272 Check {
273 name: "not_miniaturized",
274 error: state
275 .miniaturized
276 .then_some(OffscreenError::WindowMiniaturized),
277 },
278 Check {
279 name: "backing_scale",
280 error: (!(state.backing_scale.is_finite() && state.backing_scale > 0.0))
281 .then_some(OffscreenError::InvalidBackingScale),
282 },
283 ];
284 if platform == Platform::Mac {
285 checks.push(Check {
286 name: "occlusion_spi_responded",
287 error: match state.spi {
288 SpiStatus::Responded => None,
289 SpiStatus::NotCompiled => Some(OffscreenError::OcclusionSpiNotCompiled),
290 SpiStatus::Unavailable | SpiStatus::NotApplicable => {
291 Some(OffscreenError::OcclusionSpiUnavailable)
292 }
293 },
294 });
295 }
296 QualificationReport { platform, checks }
297}
298
299pub trait WindowOps {
304 fn displays(&self) -> Result<Vec<Rect>, OffscreenError>;
305 fn frame(&self) -> Result<Rect, OffscreenError>;
306 fn backing_scale(&self) -> Result<f64, OffscreenError>;
307 fn is_visible(&self) -> Result<bool, OffscreenError>;
308 fn is_miniaturized(&self) -> Result<bool, OffscreenError>;
309 fn precheck(&self) -> Result<(), OffscreenError> {
311 Ok(())
312 }
313 fn set_origin(&mut self, x: f64, y: f64) -> Result<(), OffscreenError>;
314 fn show(&mut self) -> Result<(), OffscreenError>;
316 fn hide_for_restore(&mut self) -> Result<(), OffscreenError> {
319 Ok(())
320 }
321}
322
323#[derive(Debug, Clone, Copy, PartialEq)]
325pub struct Placement {
326 pub original: Rect,
327 pub placed: Rect,
328 pub backing_scale: f64,
329}
330
331pub fn place<W: WindowOps>(w: &mut W) -> Result<Placement, OffscreenError> {
335 w.precheck()?;
336 let original = w.frame()?;
337 let target = offscreen_frame(&w.displays()?, (original.w, original.h))?;
338 w.set_origin(target.x, target.y)?;
339 w.show()?;
340 let placed = w.frame()?;
341 if !placed.near(&target, PLACEMENT_TOLERANCE) {
342 return Err(OffscreenError::PlacementNotApplied {
343 requested: target,
344 actual: placed,
345 });
346 }
347 Ok(Placement {
348 original,
349 placed,
350 backing_scale: w.backing_scale()?,
351 })
352}
353
354pub fn restore<W: WindowOps>(w: &mut W, p: &Placement) -> Result<(), OffscreenError> {
356 w.hide_for_restore()?;
357 w.set_origin(p.original.x, p.original.y)?;
358 let actual = w.frame()?;
359 if !actual.near(&p.original, PLACEMENT_TOLERANCE) {
360 return Err(OffscreenError::RestoreFailed {
361 requested: p.original,
362 actual,
363 });
364 }
365 Ok(())
366}
367
368pub fn snapshot<W: WindowOps>(w: &W, spi: SpiStatus) -> Result<WindowState, OffscreenError> {
370 Ok(WindowState {
371 frame: w.frame()?,
372 displays: w.displays()?,
373 visible: w.is_visible()?,
374 miniaturized: w.is_miniaturized()?,
375 backing_scale: w.backing_scale()?,
376 spi,
377 })
378}
379
380#[derive(Debug, Clone, PartialEq)]
384pub struct SessionReport {
385 pub pid: u32,
386 pub window_id: u64,
388 pub placed: Rect,
389 pub original: Rect,
390 pub backing_scale: f64,
391 pub qualified: bool,
392}
393
394fn rect_json(r: &Rect) -> String {
395 format!(
396 "{{\"x\":{},\"y\":{},\"w\":{},\"h\":{}}}",
397 r.x, r.y, r.w, r.h
398 )
399}
400
401impl SessionReport {
402 pub fn to_json(&self) -> String {
403 format!(
404 "{{\"pid\":{},\"window_id\":{},\"placed\":{},\"original\":{},\"backing_scale\":{},\"qualified\":{}}}",
405 self.pid,
406 self.window_id,
407 rect_json(&self.placed),
408 rect_json(&self.original),
409 self.backing_scale,
410 self.qualified
411 )
412 }
413 pub fn from_json(body: &str) -> Result<Self, OffscreenError> {
414 let bad = |k: &str| OffscreenError::BadReport(format!("missing {k}"));
415 let num = |scope: &str, k: &str| json_num(scope, k).ok_or_else(|| bad(k));
416 let sub = |k: &str| -> Result<Rect, OffscreenError> {
417 let i = body.find(&format!("\"{k}\":{{")).ok_or_else(|| bad(k))?;
418 let s = &body[i..];
419 let s = &s[..s.find('}').ok_or_else(|| bad(k))?];
420 Ok(Rect::new(
421 num(s, "x")?,
422 num(s, "y")?,
423 num(s, "w")?,
424 num(s, "h")?,
425 ))
426 };
427 Ok(Self {
428 pid: num(body, "pid")? as u32,
429 window_id: num(body, "window_id")? as u64,
430 placed: sub("placed")?,
431 original: sub("original")?,
432 backing_scale: num(body, "backing_scale")?,
433 qualified: body.contains("\"qualified\":true"),
434 })
435 }
436}
437
438fn json_num(body: &str, key: &str) -> Option<f64> {
439 let pat = format!("\"{key}\":");
440 let rest = &body[body.find(&pat)? + pat.len()..];
441 let end = rest
442 .find(|c: char| !(c.is_ascii_digit() || matches!(c, '-' | '.' | 'e' | 'E' | '+')))
443 .unwrap_or(rest.len());
444 rest[..end].parse().ok()
445}
446
447struct Active(native::Session);
450unsafe impl Send for Active {}
453
454static ACTIVE: Mutex<Option<Active>> = Mutex::new(None);
455
456fn lock_active() -> std::sync::MutexGuard<'static, Option<Active>> {
457 ACTIVE.lock().unwrap_or_else(|e| e.into_inner())
458}
459
460#[cfg_attr(not(any(target_os = "macos", windows)), allow(dead_code))]
461fn install(session: native::Session) -> Result<SessionReport, OffscreenError> {
462 let mut g = lock_active();
463 if g.is_some() {
464 return Err(OffscreenError::AlreadyActive);
465 }
466 let report = native::report(&session);
467 *g = Some(Active(session));
468 Ok(report)
469}
470
471#[cfg_attr(not(any(target_os = "macos", windows)), allow(dead_code))]
472fn is_active() -> bool {
473 lock_active().is_some()
474}
475
476pub fn offscreen_requested() -> bool {
478 std::env::var(ENV_OFFSCREEN).is_ok_and(|v| v == "1")
479}
480
481pub fn active_report() -> Option<SessionReport> {
483 lock_active().as_ref().map(|a| native::report(&a.0))
484}
485
486pub fn offscreen_qualification() -> Result<QualificationReport, OffscreenError> {
489 let g = lock_active();
490 let a = g.as_ref().ok_or(OffscreenError::NoActiveSession)?;
491 native::session_qualification(&a.0)
492}
493
494pub fn end_active() -> Result<(), OffscreenError> {
497 let a = lock_active()
498 .take()
499 .ok_or(OffscreenError::NoActiveSession)?;
500 native::end(a.0)
501}
502
503pub unsafe fn start_native(
510 ns_window: *mut std::ffi::c_void,
511 wk_webview: *mut std::ffi::c_void,
512) -> Result<SessionReport, OffscreenError> {
513 #[cfg(any(target_os = "macos", windows))]
514 {
515 if is_active() {
516 return Err(OffscreenError::AlreadyActive);
517 }
518 install(native::start(ns_window, wk_webview)?)
519 }
520 #[cfg(not(any(target_os = "macos", windows)))]
521 {
522 let _ = (ns_window, wk_webview);
523 Err(OffscreenError::UnsupportedPlatform)
524 }
525}
526
527pub fn write_report(result: &Result<SessionReport, OffscreenError>) {
529 let Some(path) = std::env::var_os(ENV_REPORT) else {
530 return;
531 };
532 let body = match result {
533 Ok(r) => r.to_json(),
534 Err(e) => format!("{{\"error\":\"{}\"}}", esc(&e.to_string())),
535 };
536 let path = PathBuf::from(path);
537 let tmp = path.with_extension("tmp");
538 if std::fs::write(&tmp, body).is_ok() {
539 let _ = std::fs::rename(&tmp, &path);
540 }
541}
542
543#[cfg(feature = "tauri-plugin")]
547pub fn start_for_window<R: tauri::Runtime>(
548 win: &tauri::WebviewWindow<R>,
549 done: impl FnOnce(Result<SessionReport, OffscreenError>) + Send + 'static,
550) {
551 #[cfg(target_os = "macos")]
552 let handle = win.ns_window().map(|p| p as usize);
553 #[cfg(windows)]
554 let handle = win.hwnd().map(|h| h.0 as usize);
555 #[cfg(not(any(target_os = "macos", windows)))]
556 let handle: tauri::Result<usize> = Ok(0);
557 let handle = match handle {
558 Ok(h) => h,
559 Err(e) => return done(Err(OffscreenError::Native(e.to_string()))),
560 };
561 let r = win.with_webview(move |pv| {
562 #[cfg(target_os = "macos")]
563 let wk = pv.inner();
564 #[cfg(not(target_os = "macos"))]
565 let wk = {
566 let _ = &pv;
567 std::ptr::null_mut()
568 };
569 done(unsafe { start_native(handle as *mut std::ffi::c_void, wk) });
571 });
572 if let Err(e) = r {
573 write_report(&Err(OffscreenError::Native(e.to_string())));
575 }
576}
577
578#[cfg(feature = "tauri-plugin")]
581pub fn start_from_env<R: tauri::Runtime>(
582 win: &tauri::WebviewWindow<R>,
583 done: impl FnOnce(&Result<SessionReport, OffscreenError>) + Send + 'static,
584) -> bool {
585 if !offscreen_requested() {
586 return false;
587 }
588 start_for_window(win, move |r| {
589 write_report(&r);
590 done(&r);
591 });
592 true
593}
594
595#[derive(Debug, Clone)]
600pub struct LaunchSpec {
601 pub program: PathBuf,
602 pub args: Vec<String>,
603 pub env: Vec<(String, String)>,
604 pub report_timeout: Duration,
605 pub report_dir: Option<PathBuf>,
607}
608
609impl LaunchSpec {
610 pub fn new(program: impl Into<PathBuf>) -> Self {
611 Self {
612 program: program.into(),
613 args: vec![],
614 env: vec![],
615 report_timeout: Duration::from_secs(30),
616 report_dir: None,
617 }
618 }
619}
620
621#[derive(Debug)]
623pub struct OffscreenLaunch {
624 child: Option<Child>,
625 report_path: PathBuf,
626 pub report: SessionReport,
627}
628
629impl OffscreenLaunch {
630 pub fn pid(&self) -> u32 {
631 self.report.pid
632 }
633 pub fn end(mut self) -> std::io::Result<()> {
636 self.shutdown()
637 }
638 fn shutdown(&mut self) -> std::io::Result<()> {
639 let _ = std::fs::remove_file(&self.report_path);
640 match self.child.take() {
641 Some(mut c) => {
642 let k = c.kill();
643 let w = c.wait();
644 k.and(w.map(|_| ()))
645 }
646 None => Ok(()),
647 }
648 }
649}
650
651impl Drop for OffscreenLaunch {
652 fn drop(&mut self) {
653 let _ = self.shutdown();
654 }
655}
656
657fn kill(child: &mut Child) {
658 let _ = child.kill();
659 let _ = child.wait();
660}
661
662pub fn launch_offscreen(spec: &LaunchSpec) -> Result<OffscreenLaunch, OffscreenError> {
666 let dir = spec.report_dir.clone().unwrap_or_else(std::env::temp_dir);
667 let nonce = std::time::SystemTime::now()
668 .duration_since(std::time::UNIX_EPOCH)
669 .map_or(0, |d| d.as_nanos());
670 let report_path = dir.join(format!(
671 "rightkit-offscreen-{}-{nonce}.json",
672 std::process::id()
673 ));
674 let _ = std::fs::remove_file(&report_path);
675 let mut cmd = Command::new(&spec.program);
676 cmd.args(&spec.args)
677 .envs(spec.env.iter().map(|(k, v)| (k, v)))
678 .env(ENV_OFFSCREEN, "1")
679 .env(ENV_REPORT, &report_path)
680 .stdin(Stdio::null())
681 .stdout(Stdio::null())
682 .stderr(Stdio::null());
683 #[cfg(windows)]
684 {
685 use std::os::windows::process::CommandExt;
686 cmd.creation_flags(0x0800_0000); }
688 let mut child = cmd
689 .spawn()
690 .map_err(|e| OffscreenError::Launch(e.to_string()))?;
691 let start = Instant::now();
692 let body = loop {
693 if let Ok(b) = std::fs::read_to_string(&report_path) {
694 break b;
695 }
696 if let Ok(Some(st)) = child.try_wait() {
697 let _ = std::fs::remove_file(&report_path);
698 return Err(OffscreenError::ChildExited(st.to_string()));
699 }
700 if start.elapsed() > spec.report_timeout {
701 kill(&mut child);
702 let _ = std::fs::remove_file(&report_path);
703 return Err(OffscreenError::ReportTimeout);
704 }
705 std::thread::sleep(Duration::from_millis(50));
706 };
707 let fail = |child: &mut Child, e: OffscreenError| {
708 kill(child);
709 let _ = std::fs::remove_file(&report_path);
710 Err(e)
711 };
712 if let Some(i) = body.find("\"error\":\"") {
713 let msg = body[i + 9..].trim_end_matches(['}', '"', '\n']).to_string();
714 return fail(&mut child, OffscreenError::AppReportedError(msg));
715 }
716 let report = match SessionReport::from_json(&body) {
717 Ok(r) => r,
718 Err(e) => return fail(&mut child, e),
719 };
720 if report.pid != child.id() {
721 let e = OffscreenError::IdentityMismatch {
722 child_pid: child.id(),
723 reported_pid: report.pid,
724 };
725 return fail(&mut child, e);
726 }
727 #[cfg(target_os = "macos")]
728 if !crate::mac::windows(report.pid as i32)
729 .iter()
730 .any(|w| u64::from(w.id) == report.window_id)
731 {
732 let e = OffscreenError::WindowNotFound {
733 pid: report.pid,
734 window_id: report.window_id,
735 };
736 return fail(&mut child, e);
737 }
738 Ok(OffscreenLaunch {
739 child: Some(child),
740 report_path,
741 report,
742 })
743}
744
745pub fn launch_offscreen_gated(
748 gate: &crate::admission::EffectGate,
749 spec: &LaunchSpec,
750) -> crate::admission::Effect<OffscreenLaunch> {
751 use crate::admission::{CancelToken, EffectKind, EffectRequest, ExecError};
752 let params = format!(
753 "{{\"program\":\"{}\",\"args\":{},\"env\":{}}}",
754 esc(&spec.program.to_string_lossy()),
755 spec.args.len(),
756 spec.env.len()
757 );
758 let req = EffectRequest::new(
759 EffectKind::Process,
760 "launch_offscreen",
761 spec.program.to_string_lossy().to_string(),
762 )
763 .with_params(params);
764 gate.admit(req, &CancelToken::new(), |_| {
765 launch_offscreen(spec).map_err(|e| ExecError::from(e.to_string()))
766 })
767}
768
769#[cfg(test)]
770mod tests {
771 use super::*;
772
773 fn d(x: f64, y: f64, w: f64, h: f64) -> Rect {
774 Rect::new(x, y, w, h)
775 }
776
777 fn clear_of_all(displays: &[Rect], f: &Rect) {
778 for dd in displays {
779 assert!(!dd.intersects(f), "{f:?} intersects {dd:?}");
780 }
781 assert!(!display_union(displays).unwrap().intersects(f));
782 }
783
784 #[test]
785 fn single_display() {
786 let ds = [d(0.0, 0.0, 1920.0, 1080.0)];
787 let f = offscreen_frame(&ds, (800.0, 600.0)).unwrap();
788 assert_eq!(f, d(2920.0, 0.0, 800.0, 600.0));
789 clear_of_all(&ds, &f);
790 }
791
792 #[test]
793 fn negative_origins_left_and_below() {
794 let ds = [
796 d(0.0, 0.0, 1440.0, 900.0),
797 d(-1920.0, -300.0, 1920.0, 1080.0),
798 ];
799 let f = offscreen_frame(&ds, (1000.0, 700.0)).unwrap();
800 assert_eq!(f.x, 1440.0 + OFFSCREEN_GAP);
801 assert_eq!(f.y, -300.0);
802 clear_of_all(&ds, &f);
803 }
804
805 #[test]
806 fn all_negative_layout_uses_union_right_edge() {
807 let ds = [
808 d(-3000.0, -500.0, 1000.0, 800.0),
809 d(-2000.0, -400.0, 1000.0, 800.0),
810 ];
811 let f = offscreen_frame(&ds, (400.0, 300.0)).unwrap();
812 assert_eq!(f.x, -1000.0 + OFFSCREEN_GAP);
813 assert_eq!(f.y, -500.0);
814 clear_of_all(&ds, &f);
815 }
816
817 #[test]
818 fn rightmost_display_is_not_the_primary() {
819 let ds = [d(0.0, 0.0, 1000.0, 800.0), d(1000.0, 0.0, 3840.0, 2160.0)];
821 let f = offscreen_frame(&ds, (500.0, 500.0)).unwrap();
822 assert_eq!(f.x, 4840.0 + OFFSCREEN_GAP);
823 clear_of_all(&ds, &f);
824 }
825
826 #[test]
827 fn stacked_and_gapped_displays() {
828 let ds = [
829 d(0.0, 0.0, 1920.0, 1080.0),
830 d(100.0, 1080.0, 1920.0, 1080.0),
831 d(5000.0, -200.0, 800.0, 600.0),
832 ];
833 let f = offscreen_frame(&ds, (640.0, 480.0)).unwrap();
834 assert_eq!(f.x, 5800.0 + OFFSCREEN_GAP);
835 clear_of_all(&ds, &f);
836 }
837
838 #[test]
839 fn rejects_bad_input() {
840 assert_eq!(
841 offscreen_frame(&[], (10.0, 10.0)),
842 Err(OffscreenError::NoDisplays)
843 );
844 let ds = [d(0.0, 0.0, 10.0, 10.0)];
845 assert_eq!(
846 offscreen_frame(&ds, (0.0, 10.0)),
847 Err(OffscreenError::InvalidSize)
848 );
849 assert_eq!(
850 offscreen_frame(&ds, (f64::NAN, 10.0)),
851 Err(OffscreenError::InvalidSize)
852 );
853 assert_eq!(
854 offscreen_frame(&[d(f64::INFINITY, 0.0, 1.0, 1.0)], (10.0, 10.0)),
855 Err(OffscreenError::NoDisplays)
856 );
857 }
858
859 #[test]
860 fn touching_edges_do_not_intersect() {
861 assert!(!d(0.0, 0.0, 10.0, 10.0).intersects(&d(10.0, 0.0, 10.0, 10.0)));
862 assert!(d(0.0, 0.0, 10.0, 10.0).intersects(&d(9.0, 9.0, 10.0, 10.0)));
863 }
864
865 struct Fake {
866 displays: Vec<Rect>,
867 frame: Rect,
868 visible: bool,
869 mini: bool,
870 scale: f64,
871 clamp_to: Option<(f64, f64)>,
873 hidden_calls: u32,
874 shown: bool,
875 hidden_precheck: bool,
876 }
877 impl Fake {
878 fn new() -> Self {
879 Self {
880 displays: vec![d(0.0, 0.0, 1920.0, 1080.0), d(-1280.0, 0.0, 1280.0, 800.0)],
881 frame: d(100.0, 100.0, 800.0, 600.0),
882 visible: false,
883 mini: false,
884 scale: 2.0,
885 clamp_to: None,
886 hidden_calls: 0,
887 shown: false,
888 hidden_precheck: false,
889 }
890 }
891 }
892 impl WindowOps for Fake {
893 fn displays(&self) -> Result<Vec<Rect>, OffscreenError> {
894 Ok(self.displays.clone())
895 }
896 fn frame(&self) -> Result<Rect, OffscreenError> {
897 Ok(self.frame)
898 }
899 fn backing_scale(&self) -> Result<f64, OffscreenError> {
900 Ok(self.scale)
901 }
902 fn is_visible(&self) -> Result<bool, OffscreenError> {
903 Ok(self.visible)
904 }
905 fn is_miniaturized(&self) -> Result<bool, OffscreenError> {
906 Ok(self.mini)
907 }
908 fn precheck(&self) -> Result<(), OffscreenError> {
909 if self.hidden_precheck {
910 Err(OffscreenError::WindowHidden)
911 } else {
912 Ok(())
913 }
914 }
915 fn set_origin(&mut self, x: f64, y: f64) -> Result<(), OffscreenError> {
916 let (x, y) = self.clamp_to.unwrap_or((x, y));
917 self.frame.x = x;
918 self.frame.y = y;
919 Ok(())
920 }
921 fn show(&mut self) -> Result<(), OffscreenError> {
922 self.visible = true;
923 self.shown = true;
924 Ok(())
925 }
926 fn hide_for_restore(&mut self) -> Result<(), OffscreenError> {
927 self.hidden_calls += 1;
928 self.visible = false;
929 Ok(())
930 }
931 }
932
933 #[test]
934 fn place_then_restore_round_trip() {
935 let mut w = Fake::new();
936 let p = place(&mut w).unwrap();
937 assert_eq!(p.original, d(100.0, 100.0, 800.0, 600.0));
938 assert_eq!(p.placed.x, 1920.0 + OFFSCREEN_GAP);
939 assert_eq!(p.backing_scale, 2.0);
940 assert!(w.shown);
941 let st = snapshot(&w, SpiStatus::Responded).unwrap();
942 assert!(qualify(&st, Platform::Mac).passed());
943 restore(&mut w, &p).unwrap();
944 assert_eq!(w.frame, p.original);
945 assert_eq!(w.hidden_calls, 1);
946 assert!(
947 !w.visible,
948 "restore hides first so it never flashes on a display"
949 );
950 }
951
952 #[test]
953 fn place_detects_clamped_window() {
954 let mut w = Fake::new();
955 w.clamp_to = Some((50.0, 50.0));
956 match place(&mut w) {
957 Err(OffscreenError::PlacementNotApplied { actual, .. }) => assert_eq!(actual.x, 50.0),
958 other => panic!("{other:?}"),
959 }
960 }
961
962 #[test]
963 fn restore_detects_failure() {
964 let mut w = Fake::new();
965 let p = place(&mut w).unwrap();
966 w.clamp_to = Some((0.0, 0.0));
967 assert!(matches!(
968 restore(&mut w, &p),
969 Err(OffscreenError::RestoreFailed { .. })
970 ));
971 }
972
973 #[test]
974 fn precheck_refusal_moves_nothing() {
975 let mut w = Fake::new();
976 w.hidden_precheck = true;
977 assert_eq!(place(&mut w), Err(OffscreenError::WindowHidden));
978 assert_eq!(w.frame, d(100.0, 100.0, 800.0, 600.0));
979 }
980
981 fn good_state() -> WindowState {
982 WindowState {
983 frame: d(3000.0, 0.0, 800.0, 600.0),
984 displays: vec![d(0.0, 0.0, 1920.0, 1080.0)],
985 visible: true,
986 miniaturized: false,
987 backing_scale: 2.0,
988 spi: SpiStatus::Responded,
989 }
990 }
991
992 #[test]
993 fn qualification_passes_and_each_check_fails_typed() {
994 assert!(qualify(&good_state(), Platform::Mac).passed());
995
996 let mut s = good_state();
997 s.frame = d(1900.0, 0.0, 800.0, 600.0);
998 assert!(matches!(
999 qualify(&s, Platform::Mac).into_result(),
1000 Err(OffscreenError::WindowOnDisplay { .. })
1001 ));
1002
1003 let mut s = good_state();
1005 s.displays = vec![d(0.0, 0.0, 1000.0, 1000.0), d(5000.0, 0.0, 1000.0, 1000.0)];
1006 s.frame = d(2000.0, 0.0, 800.0, 600.0);
1007 assert!(matches!(
1008 qualify(&s, Platform::Mac).into_result(),
1009 Err(OffscreenError::WindowOnDisplay { .. })
1010 ));
1011
1012 let mut s = good_state();
1013 s.visible = false;
1014 assert_eq!(
1015 qualify(&s, Platform::Windows).into_result(),
1016 Err(OffscreenError::WindowHidden)
1017 );
1018
1019 let mut s = good_state();
1020 s.miniaturized = true;
1021 assert_eq!(
1022 qualify(&s, Platform::Windows).into_result(),
1023 Err(OffscreenError::WindowMiniaturized)
1024 );
1025
1026 let mut s = good_state();
1027 s.backing_scale = 0.0;
1028 assert_eq!(
1029 qualify(&s, Platform::Mac).into_result(),
1030 Err(OffscreenError::InvalidBackingScale)
1031 );
1032
1033 let mut s = good_state();
1034 s.displays.clear();
1035 assert_eq!(
1036 qualify(&s, Platform::Mac).into_result(),
1037 Err(OffscreenError::NoDisplays)
1038 );
1039 }
1040
1041 #[test]
1042 fn qualification_spi_is_macos_only() {
1043 let mut s = good_state();
1044 for (spi, err) in [
1045 (
1046 SpiStatus::NotCompiled,
1047 OffscreenError::OcclusionSpiNotCompiled,
1048 ),
1049 (
1050 SpiStatus::Unavailable,
1051 OffscreenError::OcclusionSpiUnavailable,
1052 ),
1053 ] {
1054 s.spi = spi;
1055 assert_eq!(qualify(&s, Platform::Mac).into_result(), Err(err));
1056 assert!(qualify(&s, Platform::Windows).passed());
1057 }
1058 s.spi = SpiStatus::NotApplicable;
1059 assert!(qualify(&s, Platform::Windows).passed());
1060 assert!(!qualify(&s, Platform::Mac).passed());
1061 }
1062
1063 #[test]
1064 fn report_json_round_trip() {
1065 let r = SessionReport {
1066 pid: 4242,
1067 window_id: 987,
1068 placed: d(3920.0, -100.5, 800.0, 600.0),
1069 original: d(-100.0, 50.0, 800.0, 600.0),
1070 backing_scale: 2.0,
1071 qualified: true,
1072 };
1073 assert_eq!(SessionReport::from_json(&r.to_json()).unwrap(), r);
1074 assert!(matches!(
1075 SessionReport::from_json("{}"),
1076 Err(OffscreenError::BadReport(_))
1077 ));
1078 }
1079
1080 #[test]
1081 fn no_active_session_is_typed() {
1082 assert_eq!(
1084 offscreen_qualification().unwrap_err(),
1085 OffscreenError::NoActiveSession
1086 );
1087 assert_eq!(end_active().unwrap_err(), OffscreenError::NoActiveSession);
1088 }
1089
1090 #[test]
1091 fn launch_reports_child_exit_and_missing_binary() {
1092 let mut spec = LaunchSpec::new("/usr/bin/true");
1093 spec.report_timeout = Duration::from_secs(5);
1094 #[cfg(unix)]
1095 assert!(matches!(
1096 launch_offscreen(&spec),
1097 Err(OffscreenError::ChildExited(_))
1098 ));
1099 let spec = LaunchSpec::new("/nonexistent/rightkit-offscreen-app");
1100 assert!(matches!(
1101 launch_offscreen(&spec),
1102 Err(OffscreenError::Launch(_))
1103 ));
1104 }
1105}