1use std::fmt::Display;
26use std::panic::{catch_unwind, AssertUnwindSafe};
27use std::sync::atomic::{AtomicBool, Ordering};
28use std::sync::{Arc, Mutex};
29use std::time::{Duration, Instant};
30
31use serde::Serialize;
32use tauri::{AppHandle, Emitter, Manager, Wry};
33
34use crate::{ShellState, EVENT_IDLE};
35
36pub const DEFAULT_SUSPEND_AFTER: Duration = Duration::from_secs(30);
38
39pub const MAX_SUSPEND_AFTER: Duration = Duration::from_secs(24 * 60 * 60);
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44pub enum ControllerHide {
45 #[default]
48 Immediate,
49 OnTimer,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
57pub enum IdleEvents {
58 #[default]
61 Always,
62 NativeSuspendOnly,
65}
66
67impl IdleEvents {
68 pub fn emits(self, native_suspend: bool) -> bool {
70 match self {
71 IdleEvents::Always => true,
72 IdleEvents::NativeSuspendOnly => native_suspend,
73 }
74 }
75}
76
77#[derive(Clone)]
79pub struct SuspendAfterProvider(Arc<dyn Fn() -> Option<Duration> + Send + Sync>);
80
81impl std::fmt::Debug for SuspendAfterProvider {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 f.write_str("SuspendAfterProvider(..)")
84 }
85}
86
87impl PartialEq for SuspendAfterProvider {
90 fn eq(&self, other: &Self) -> bool {
91 Arc::ptr_eq(&self.0, &other.0)
92 }
93}
94
95impl Eq for SuspendAfterProvider {}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct IdleOptions {
106 pub suspend_after: Duration,
109 pub start_on_minimize: bool,
112 pub controller_hide: ControllerHide,
113 pub events: IdleEvents,
114 suspend_after_fn: Option<SuspendAfterProvider>,
116}
117
118impl Default for IdleOptions {
119 fn default() -> Self {
120 Self {
121 suspend_after: DEFAULT_SUSPEND_AFTER,
122 start_on_minimize: false,
123 controller_hide: ControllerHide::Immediate,
124 events: IdleEvents::Always,
125 suspend_after_fn: None,
126 }
127 }
128}
129
130impl IdleOptions {
131 pub fn suspend_after(mut self, suspend_after: Duration) -> Self {
133 self.suspend_after = suspend_after;
134 self
135 }
136
137 pub fn start_on_minimize(mut self, start_on_minimize: bool) -> Self {
139 self.start_on_minimize = start_on_minimize;
140 self
141 }
142
143 pub fn controller_hide(mut self, controller_hide: ControllerHide) -> Self {
145 self.controller_hide = controller_hide;
146 self
147 }
148
149 pub fn events(mut self, events: IdleEvents) -> Self {
151 self.events = events;
152 self
153 }
154
155 pub fn suspend_after_with(
167 mut self,
168 provider: impl Fn() -> Option<Duration> + Send + Sync + 'static,
169 ) -> Self {
170 self.suspend_after_fn = Some(SuspendAfterProvider(Arc::new(provider)));
171 self
172 }
173
174 pub fn hide_delay(&self, host_default: Duration, per_hide: Option<Duration>) -> Duration {
178 effective_delay(host_default, per_hide.or_else(|| self.provided_delay()))
179 }
180
181 fn provided_delay(&self) -> Option<Duration> {
184 let provider = self.suspend_after_fn.as_ref()?;
185 catch_unwind(AssertUnwindSafe(|| (*provider.0)()))
186 .ok()
187 .flatten()
188 }
189}
190
191pub fn effective_delay(host_default: Duration, per_hide: Option<Duration>) -> Duration {
193 per_hide.unwrap_or(host_default).min(MAX_SUSPEND_AFTER)
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198pub struct HidePlan {
199 pub now: bool,
201 pub on_timer: bool,
203}
204
205pub fn hide_plan(mode: ControllerHide, controller_already_hidden: bool) -> HidePlan {
207 HidePlan {
208 now: mode == ControllerHide::Immediate && !controller_already_hidden,
209 on_timer: mode == ControllerHide::OnTimer && !controller_already_hidden,
210 }
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215pub enum MinimizeStep {
216 Start,
217 End,
218 None,
219}
220
221#[derive(Debug, Default)]
224pub struct MinimizeTracker {
225 active: AtomicBool,
226}
227
228impl MinimizeTracker {
229 pub fn step(&self, minimized: bool) -> MinimizeStep {
230 let was = self.active.swap(minimized, Ordering::SeqCst);
231 match (was, minimized) {
232 (false, true) => MinimizeStep::Start,
233 (true, false) => MinimizeStep::End,
234 _ => MinimizeStep::None,
235 }
236 }
237
238 pub fn clear(&self) {
240 self.active.store(false, Ordering::SeqCst);
241 }
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
246#[serde(rename_all = "lowercase")]
247pub enum IdlePhase {
248 Suspend,
249 Resume,
250}
251
252#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
255pub struct IdleFailure {
256 pub phase: IdlePhase,
257 pub window: String,
259 pub error: String,
261}
262
263pub type IdleErrorCallback = Arc<dyn Fn(&IdleFailure) + Send + Sync>;
265
266#[derive(Clone)]
268pub(crate) struct Reporter {
269 window: String,
270 sink: Option<IdleErrorCallback>,
271}
272
273impl Reporter {
274 #[cfg_attr(not(windows), allow(dead_code))]
275 pub(crate) fn fail(&self, phase: IdlePhase, error: impl Display) {
276 if let Some(sink) = &self.sink {
277 sink(&IdleFailure {
278 phase,
279 window: self.window.clone(),
280 error: error.to_string(),
281 });
282 }
283 }
284}
285
286#[derive(Debug, Clone, Copy, PartialEq, Eq)]
287pub enum IdleAction {
288 HideControllerAndStartTimer,
289 ShowController,
290 SuspendNow,
291 ResumeAndShowController,
292 None,
293}
294
295#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
296pub struct IdleState {
297 hidden: bool,
298 suspended: bool,
299 generation: u64,
300}
301
302impl IdleState {
303 pub fn on_hidden(&mut self) -> (u64, IdleAction) {
304 if self.suspended {
305 self.hidden = true;
306 return (self.generation, IdleAction::None);
307 }
308 self.hidden = true;
309 self.generation = self.generation.wrapping_add(1);
310 (self.generation, IdleAction::HideControllerAndStartTimer)
311 }
312
313 pub fn on_visible(&mut self) -> IdleAction {
314 let was_hidden = self.hidden;
315 self.hidden = false;
316 self.generation = self.generation.wrapping_add(1);
317 if std::mem::take(&mut self.suspended) {
318 IdleAction::ResumeAndShowController
319 } else if was_hidden {
320 IdleAction::ShowController
321 } else {
322 IdleAction::None
323 }
324 }
325
326 pub fn on_timer(&mut self, generation: u64) -> IdleAction {
327 if self.hidden && !self.suspended && self.generation == generation {
328 self.suspended = true;
329 IdleAction::SuspendNow
330 } else {
331 IdleAction::None
332 }
333 }
334
335 pub fn on_suspend_failed(&mut self) {
338 self.suspended = false;
339 }
340
341 pub fn snapshot(&self) -> (bool, bool) {
343 (self.hidden, self.suspended)
344 }
345}
346
347#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
349#[serde(rename_all = "lowercase")]
350pub enum IdleTransitionKind {
351 Suspended,
352 Resumed,
353}
354
355#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
358pub struct IdleTransition {
359 pub kind: IdleTransitionKind,
360 pub window: String,
362 pub elapsed: Duration,
366}
367
368pub type IdleTransitionCallback = Arc<dyn Fn(&IdleTransition) + Send + Sync>;
370
371pub(crate) trait Controller {
374 fn set_visible(&mut self, visible: bool) -> Result<(), String>;
375 fn try_suspend(&mut self) -> Result<(), String>;
377 fn resume(&mut self) -> Result<(), String>;
378}
379
380#[derive(Debug, Default)]
382pub(crate) struct Applied {
383 pub(crate) hidden: bool,
385 pub(crate) suspended: bool,
387 suspended_at: Option<Instant>,
388 resumed_for: Option<Duration>,
390}
391
392#[derive(Debug, Clone, PartialEq, Eq)]
393pub(crate) struct StepFailure {
394 pub(crate) phase: IdlePhase,
395 pub(crate) error: String,
396}
397
398#[derive(Debug, Clone, Copy, PartialEq, Eq)]
400pub(crate) enum ResumeStep {
401 Nothing,
403 Shown,
405 Resumed(Duration),
407}
408
409impl Applied {
410 pub(crate) fn hide(&mut self, c: &mut dyn Controller) -> Result<(), StepFailure> {
412 if self.hidden {
413 return Ok(());
414 }
415 c.set_visible(false).map_err(|error| StepFailure {
416 phase: IdlePhase::Suspend,
417 error: format!("SetIsVisible(false): {error}"),
418 })?;
419 self.hidden = true;
420 Ok(())
421 }
422
423 pub(crate) fn hide_and_suspend(
426 &mut self,
427 c: &mut dyn Controller,
428 now: Instant,
429 ) -> Result<(), StepFailure> {
430 self.hide(c)?;
431 if self.suspended {
432 return Ok(());
433 }
434 c.try_suspend().map_err(|error| StepFailure {
435 phase: IdlePhase::Suspend,
436 error: format!("TrySuspend: {error}"),
437 })?;
438 self.suspended = true;
439 self.suspended_at = Some(now);
440 Ok(())
441 }
442
443 pub(crate) fn resume_and_show(
446 &mut self,
447 c: &mut dyn Controller,
448 now: Instant,
449 ) -> Result<ResumeStep, StepFailure> {
450 let mut resumed = None;
451 if self.suspended {
452 c.resume().map_err(|error| StepFailure {
453 phase: IdlePhase::Resume,
454 error: format!("Resume: {error}"),
455 })?;
456 self.suspended = false;
457 let for_ = self
458 .suspended_at
459 .take()
460 .map(|at| now.saturating_duration_since(at))
461 .unwrap_or_default();
462 self.resumed_for = Some(for_);
463 }
464 if self.hidden {
465 c.set_visible(true).map_err(|error| StepFailure {
466 phase: IdlePhase::Resume,
467 error: format!("SetIsVisible(true): {error}"),
468 })?;
469 self.hidden = false;
470 }
471 if let Some(for_) = self.resumed_for.take() {
472 resumed = Some(for_);
473 }
474 Ok(match resumed {
475 Some(for_) => ResumeStep::Resumed(for_),
476 None => ResumeStep::Nothing,
477 })
478 }
479}
480
481impl Applied {
482 fn classify(&self, before_hidden: bool, step: ResumeStep) -> ResumeStep {
484 match step {
485 ResumeStep::Nothing if before_hidden => ResumeStep::Shown,
486 other => other,
487 }
488 }
489}
490
491pub struct IdleHost {
493 pub suspend_after: Duration,
494 pub options: IdleOptions,
495 pub minimize: MinimizeTracker,
496 pub state: Mutex<IdleState>,
497 pub counters: Mutex<(u32, u32)>,
499 pub on_error: Option<IdleErrorCallback>,
501 pub on_transition: Option<IdleTransitionCallback>,
503 pub(crate) applied: Mutex<Applied>,
504}
505
506impl IdleHost {
507 pub fn new(suspend_after: Duration) -> Self {
508 Self {
509 suspend_after,
510 options: IdleOptions {
511 suspend_after,
512 ..IdleOptions::default()
513 },
514 minimize: MinimizeTracker::default(),
515 state: Mutex::new(IdleState::default()),
516 counters: Mutex::new((0, 0)),
517 on_error: None,
518 on_transition: None,
519 applied: Mutex::new(Applied::default()),
520 }
521 }
522
523 pub fn with_options(mut self, options: IdleOptions) -> Self {
525 self.suspend_after = options.suspend_after.min(MAX_SUSPEND_AFTER);
526 self.options = IdleOptions {
527 suspend_after: self.suspend_after,
528 ..options
529 };
530 self
531 }
532
533 pub fn with_on_error(mut self, on_error: Option<IdleErrorCallback>) -> Self {
535 self.on_error = on_error;
536 self
537 }
538
539 pub fn with_on_transition(mut self, on_transition: Option<IdleTransitionCallback>) -> Self {
541 self.on_transition = on_transition;
542 self
543 }
544
545 pub fn report_failure(&self, phase: IdlePhase, window: &str, error: &str) {
547 self.reporter(window).fail(phase, error);
548 }
549
550 pub(crate) fn reporter(&self, window: &str) -> Reporter {
551 Reporter {
552 window: window.to_owned(),
553 sink: self.on_error.clone(),
554 }
555 }
556
557 pub(crate) fn transition(&self, kind: IdleTransitionKind, window: &str, elapsed: Duration) {
559 if let Some(sink) = &self.on_transition {
560 sink(&IdleTransition {
561 kind,
562 window: window.to_owned(),
563 elapsed,
564 });
565 }
566 }
567}
568
569#[derive(Debug, Clone, Serialize)]
570#[serde(rename_all = "camelCase")]
571pub struct IdleStatus {
572 pub enabled: bool,
573 pub hidden: bool,
574 pub suspended: bool,
575 pub suspend_after_ms: u64,
576 pub suspends: u32,
577 pub resumes: u32,
578 pub native_suspend: bool,
580}
581
582pub fn status(app: &AppHandle<Wry>) -> IdleStatus {
583 let state = app.state::<ShellState>();
584 match &state.idle {
585 Some(h) => {
586 let (hidden, suspended) = h.state.lock().unwrap_or_else(|e| e.into_inner()).snapshot();
587 let (suspends, resumes) = *h.counters.lock().unwrap_or_else(|e| e.into_inner());
588 IdleStatus {
589 enabled: true,
590 hidden,
591 suspended,
592 suspend_after_ms: h.suspend_after.as_millis() as u64,
593 suspends,
594 resumes,
595 native_suspend: cfg!(windows),
596 }
597 }
598 None => IdleStatus {
599 enabled: false,
600 hidden: false,
601 suspended: false,
602 suspend_after_ms: 0,
603 suspends: 0,
604 resumes: 0,
605 native_suspend: cfg!(windows),
606 },
607 }
608}
609
610fn emit(app: &AppHandle<Wry>, host: &IdleHost, state: &str) {
611 if host.options.events.emits(cfg!(windows)) {
612 let _ = app.emit(EVENT_IDLE, serde_json::json!({"state": state}));
613 }
614}
615
616pub fn note_hidden(app: &AppHandle<Wry>, controller_already_hidden: bool) {
619 note_hidden_after(app, controller_already_hidden, None);
620}
621
622pub fn note_hidden_after(
624 app: &AppHandle<Wry>,
625 controller_already_hidden: bool,
626 suspend_after: Option<Duration>,
627) {
628 let state = app.state::<ShellState>();
629 let Some(host) = &state.idle else { return };
630 let (generation, action) = host
631 .state
632 .lock()
633 .unwrap_or_else(|e| e.into_inner())
634 .on_hidden();
635 if action != IdleAction::HideControllerAndStartTimer {
636 return;
637 }
638 emit(app, host, "hidden");
639 let hidden_at = Instant::now();
640 let plan = hide_plan(host.options.controller_hide, controller_already_hidden);
641 if plan.now {
642 hide_controller(app);
643 }
644 let delay = host.options.hide_delay(host.suspend_after, suspend_after);
647 let app = app.clone();
648 let _ = std::thread::Builder::new()
649 .name("rk-shell-idle".into())
650 .spawn(move || {
651 std::thread::sleep(delay);
652 suspend_controller(&app, generation, hidden_at);
653 });
654}
655
656fn hide_controller(app: &AppHandle<Wry>) {
659 let state = app.state::<ShellState>();
660 let Some(host) = &state.idle else { return };
661 let reporter = host.reporter(&state.main_label);
662 let app = app.clone();
663 let run = platform::with_controller(&app.clone(), move |c| {
664 let state = app.state::<ShellState>();
665 let Some(host) = &state.idle else { return };
666 let mut applied = host.applied.lock().unwrap_or_else(|e| e.into_inner());
667 let still_hidden = host
668 .state
669 .lock()
670 .unwrap_or_else(|e| e.into_inner())
671 .snapshot()
672 .0;
673 if !still_hidden {
674 return;
675 }
676 if let Err(f) = applied.hide(c) {
677 host.reporter(&state.main_label).fail(f.phase, f.error);
678 }
679 });
680 if let Err(e) = run {
681 reporter.fail(IdlePhase::Suspend, e);
682 }
683}
684
685fn suspend_controller(app: &AppHandle<Wry>, generation: u64, hidden_at: Instant) {
688 let state = app.state::<ShellState>();
689 let Some(host) = &state.idle else { return };
690 let reporter = host.reporter(&state.main_label);
691 let handle = app.clone();
692 let run = platform::with_controller(app, move |c| {
693 let state = handle.state::<ShellState>();
694 let Some(host) = &state.idle else { return };
695 let mut applied = host.applied.lock().unwrap_or_else(|e| e.into_inner());
696 let action = host
697 .state
698 .lock()
699 .unwrap_or_else(|e| e.into_inner())
700 .on_timer(generation);
701 if action != IdleAction::SuspendNow {
702 return;
703 }
704 let now = Instant::now();
705 match applied.hide_and_suspend(c, now) {
706 Ok(()) => {
707 host.counters.lock().unwrap_or_else(|e| e.into_inner()).0 += 1;
708 emit(&handle, host, "suspended");
709 host.transition(
710 IdleTransitionKind::Suspended,
711 &state.main_label,
712 now.saturating_duration_since(hidden_at),
713 );
714 }
715 Err(f) => {
716 host.state
717 .lock()
718 .unwrap_or_else(|e| e.into_inner())
719 .on_suspend_failed();
720 host.reporter(&state.main_label).fail(f.phase, f.error);
721 }
722 }
723 });
724 if let Err(e) = run {
725 reporter.fail(IdlePhase::Suspend, e);
726 }
727}
728
729#[derive(Debug, Clone, Copy, PartialEq, Eq)]
731pub enum MinimizeAction {
732 Hide {
734 delay: Duration,
735 },
736 Visible,
737 None,
738}
739
740pub fn minimize_action(
743 host_default: Duration,
744 step: MinimizeStep,
745 per_hide: Option<Duration>,
746) -> MinimizeAction {
747 match step {
748 MinimizeStep::Start => MinimizeAction::Hide {
749 delay: effective_delay(host_default, per_hide),
750 },
751 MinimizeStep::End => MinimizeAction::Visible,
752 MinimizeStep::None => MinimizeAction::None,
753 }
754}
755
756pub fn note_minimized(app: &AppHandle<Wry>, minimized: bool) {
759 note_minimized_after(app, minimized, None);
760}
761
762pub fn note_minimized_after(
765 app: &AppHandle<Wry>,
766 minimized: bool,
767 suspend_after: Option<Duration>,
768) {
769 let state = app.state::<ShellState>();
770 let Some(host) = &state.idle else { return };
771 if !host.options.start_on_minimize {
772 return;
773 }
774 let step = host.minimize.step(minimized);
775 let per_hide = match step {
778 MinimizeStep::Start => Some(host.options.hide_delay(host.suspend_after, suspend_after)),
779 MinimizeStep::End | MinimizeStep::None => suspend_after,
780 };
781 match minimize_action(host.suspend_after, step, per_hide) {
782 MinimizeAction::Hide { delay } => note_hidden_after(app, false, Some(delay)),
783 MinimizeAction::Visible => note_visible(app),
784 MinimizeAction::None => {}
785 }
786}
787
788pub fn note_visible(app: &AppHandle<Wry>) {
792 let state = app.state::<ShellState>();
793 let Some(host) = &state.idle else { return };
794 host.minimize.clear();
795 let action = host
796 .state
797 .lock()
798 .unwrap_or_else(|e| e.into_inner())
799 .on_visible();
800 let reporter = host.reporter(&state.main_label);
801 let handle = app.clone();
802 let run = platform::with_controller(app, move |c| {
803 let state = handle.state::<ShellState>();
804 let Some(host) = &state.idle else { return };
805 let mut applied = host.applied.lock().unwrap_or_else(|e| e.into_inner());
806 let before_hidden = applied.hidden;
807 let now = Instant::now();
808 match applied.resume_and_show(c, now) {
809 Ok(step) => match applied.classify(before_hidden, step) {
810 ResumeStep::Resumed(for_) => {
811 host.counters.lock().unwrap_or_else(|e| e.into_inner()).1 += 1;
812 emit(&handle, host, "resumed");
813 host.transition(IdleTransitionKind::Resumed, &state.main_label, for_);
814 }
815 ResumeStep::Shown => emit(&handle, host, "visible"),
816 ResumeStep::Nothing => {
817 if matches!(
818 action,
819 IdleAction::ShowController | IdleAction::ResumeAndShowController
820 ) {
821 emit(&handle, host, "visible");
822 }
823 }
824 },
825 Err(f) => host.reporter(&state.main_label).fail(f.phase, f.error),
826 }
827 });
828 if let Err(e) = run {
829 reporter.fail(IdlePhase::Resume, e);
830 }
831}
832
833#[cfg(windows)]
834mod platform {
835 use crate::winapi::webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2_3;
836 use crate::winapi::windows::core::Interface;
837 use tauri::{AppHandle, Manager, Wry};
838
839 use super::Controller;
840 use crate::ShellState;
841
842 struct Wv2<'a> {
844 visible: &'a dyn Fn(bool) -> Result<(), String>,
845 suspend: &'a dyn Fn() -> Result<(), String>,
846 resume: &'a dyn Fn() -> Result<(), String>,
847 }
848
849 impl Controller for Wv2<'_> {
850 fn set_visible(&mut self, visible: bool) -> Result<(), String> {
851 (self.visible)(visible)
852 }
853 fn try_suspend(&mut self) -> Result<(), String> {
854 (self.suspend)()
855 }
856 fn resume(&mut self) -> Result<(), String> {
857 (self.resume)()
858 }
859 }
860
861 pub fn with_controller(
864 app: &AppHandle<Wry>,
865 f: impl FnOnce(&mut dyn Controller) + Send + 'static,
866 ) -> Result<(), String> {
867 let label = app.state::<ShellState>().main_label.clone();
868 let w = app
869 .get_webview_window(&label)
870 .ok_or_else(|| "main window not found".to_string())?;
871 let report = app
872 .state::<ShellState>()
873 .idle
874 .as_ref()
875 .map(|h| h.reporter(&label));
876 w.with_webview(move |webview| {
877 let controller = webview.controller();
878 let visible = |v: bool| -> Result<(), String> {
879 unsafe { controller.SetIsVisible(v) }.map_err(|e| e.to_string())
881 };
882 let core3 = || -> Result<ICoreWebView2_3, String> {
883 let core = unsafe { controller.CoreWebView2() }.map_err(|e| e.to_string())?;
885 core.cast::<ICoreWebView2_3>().map_err(|e| e.to_string())
886 };
887 let suspend = || -> Result<(), String> {
888 let core3 = core3()?;
889 let completion = report.clone();
890 let handler = crate::winapi::webview2_com::TrySuspendCompletedHandler::create(
891 Box::new(move |error, _suspended| {
892 if let (Err(e), Some(r)) = (error, &completion) {
893 r.fail(super::IdlePhase::Suspend, e);
894 }
895 Ok(())
896 }),
897 );
898 unsafe { core3.TrySuspend(&handler) }.map_err(|e| e.to_string())
900 };
901 let resume = || -> Result<(), String> {
902 let core3 = core3()?;
903 unsafe { core3.Resume() }.map_err(|e| e.to_string())
905 };
906 let mut c = Wv2 {
907 visible: &visible,
908 suspend: &suspend,
909 resume: &resume,
910 };
911 f(&mut c);
912 })
913 .map_err(|e| e.to_string())
914 }
915}
916
917#[cfg(not(windows))]
918mod platform {
919 use tauri::{AppHandle, Wry};
920
921 use super::Controller;
922
923 struct Noop;
926
927 impl Controller for Noop {
928 fn set_visible(&mut self, _: bool) -> Result<(), String> {
929 Ok(())
930 }
931 fn try_suspend(&mut self) -> Result<(), String> {
932 Ok(())
933 }
934 fn resume(&mut self) -> Result<(), String> {
935 Ok(())
936 }
937 }
938
939 pub fn with_controller(
940 _: &AppHandle<Wry>,
941 f: impl FnOnce(&mut dyn Controller) + Send + 'static,
942 ) -> Result<(), String> {
943 f(&mut Noop);
944 Ok(())
945 }
946}
947
948#[cfg(test)]
949mod tests {
950 use super::*;
951 use std::collections::HashSet;
952 use std::sync::atomic::AtomicUsize;
953
954 #[derive(Default)]
956 struct Fake {
957 calls: Vec<&'static str>,
958 failing: HashSet<&'static str>,
959 }
960
961 impl Fake {
962 fn failing(names: &[&'static str]) -> Self {
963 Self {
964 failing: names.iter().copied().collect(),
965 ..Self::default()
966 }
967 }
968 fn call(&mut self, name: &'static str) -> Result<(), String> {
969 self.calls.push(name);
970 if self.failing.contains(name) {
971 Err(format!("{name} failed"))
972 } else {
973 Ok(())
974 }
975 }
976 }
977
978 impl Controller for Fake {
979 fn set_visible(&mut self, visible: bool) -> Result<(), String> {
980 self.call(if visible { "show" } else { "hide" })
981 }
982 fn try_suspend(&mut self) -> Result<(), String> {
983 self.call("suspend")
984 }
985 fn resume(&mut self) -> Result<(), String> {
986 self.call("resume")
987 }
988 }
989
990 #[test]
991 fn suspend_is_hide_then_suspend() {
992 let (mut a, mut c) = (Applied::default(), Fake::default());
993 a.hide_and_suspend(&mut c, Instant::now()).unwrap();
994 assert_eq!(c.calls, ["hide", "suspend"]);
995 assert!(a.hidden && a.suspended);
996 }
997
998 #[test]
999 fn a_failed_hide_means_no_suspend_and_a_running_webview() {
1000 let (mut a, mut c) = (Applied::default(), Fake::failing(&["hide"]));
1001 let f = a.hide_and_suspend(&mut c, Instant::now()).unwrap_err();
1002 assert_eq!(f.phase, IdlePhase::Suspend);
1003 assert_eq!(c.calls, ["hide"], "TrySuspend must not run");
1004 assert!(!a.hidden && !a.suspended);
1005 }
1006
1007 #[test]
1008 fn an_immediate_hide_is_not_repeated_by_the_suspend() {
1009 let (mut a, mut c) = (Applied::default(), Fake::default());
1010 a.hide(&mut c).unwrap();
1011 a.hide_and_suspend(&mut c, Instant::now()).unwrap();
1012 assert_eq!(c.calls, ["hide", "suspend"]);
1013 }
1014
1015 #[test]
1016 fn a_failed_immediate_hide_is_retried_by_the_suspend() {
1017 let (mut a, mut c) = (Applied::default(), Fake::failing(&["hide"]));
1018 assert!(a.hide(&mut c).is_err());
1019 c.failing.clear();
1020 a.hide_and_suspend(&mut c, Instant::now()).unwrap();
1021 assert_eq!(c.calls, ["hide", "hide", "suspend"]);
1022 }
1023
1024 #[test]
1025 fn a_failed_try_suspend_leaves_the_controller_hidden_not_suspended() {
1026 let (mut a, mut c) = (Applied::default(), Fake::failing(&["suspend"]));
1027 assert!(a.hide_and_suspend(&mut c, Instant::now()).is_err());
1028 assert!(a.hidden && !a.suspended);
1029 c.calls.clear();
1031 assert_eq!(
1032 a.resume_and_show(&mut c, Instant::now()).unwrap(),
1033 ResumeStep::Nothing
1034 );
1035 assert_eq!(c.calls, ["show"]);
1036 }
1037
1038 #[test]
1039 fn resume_is_resume_then_show() {
1040 let (mut a, mut c) = (Applied::default(), Fake::default());
1041 let t0 = Instant::now();
1042 a.hide_and_suspend(&mut c, t0).unwrap();
1043 c.calls.clear();
1044 let step = a
1045 .resume_and_show(&mut c, t0 + Duration::from_secs(5))
1046 .unwrap();
1047 assert_eq!(c.calls, ["resume", "show"]);
1048 assert_eq!(step, ResumeStep::Resumed(Duration::from_secs(5)));
1049 assert!(!a.hidden && !a.suspended);
1050 }
1051
1052 #[test]
1053 fn a_failed_resume_means_no_show_and_an_error_then_retries() {
1054 let (mut a, mut c) = (Applied::default(), Fake::default());
1055 a.hide_and_suspend(&mut c, Instant::now()).unwrap();
1056 c.calls.clear();
1057 c.failing.insert("resume");
1058 let f = a.resume_and_show(&mut c, Instant::now()).unwrap_err();
1059 assert_eq!(f.phase, IdlePhase::Resume);
1060 assert_eq!(c.calls, ["resume"], "no show after a failed resume");
1061 assert!(a.hidden && a.suspended, "still pending");
1062 c.failing.clear();
1063 c.calls.clear();
1064 assert!(matches!(
1065 a.resume_and_show(&mut c, Instant::now()),
1066 Ok(ResumeStep::Resumed(_))
1067 ));
1068 assert_eq!(c.calls, ["resume", "show"]);
1069 }
1070
1071 #[test]
1072 fn a_failed_show_is_surfaced_and_retried_on_the_next_request_without_resuming_again() {
1073 let (mut a, mut c) = (Applied::default(), Fake::default());
1074 a.hide_and_suspend(&mut c, Instant::now()).unwrap();
1075 c.calls.clear();
1076 c.failing.insert("show");
1077 let f = a.resume_and_show(&mut c, Instant::now()).unwrap_err();
1078 assert_eq!(f.phase, IdlePhase::Resume);
1079 assert_eq!(c.calls, ["resume", "show"]);
1080 assert!(a.hidden && !a.suspended);
1081 c.failing.clear();
1082 c.calls.clear();
1083 let step = a.resume_and_show(&mut c, Instant::now()).unwrap();
1084 assert_eq!(c.calls, ["show"]);
1085 assert!(
1086 matches!(step, ResumeStep::Resumed(_)),
1087 "resumed hook still fires once"
1088 );
1089 c.calls.clear();
1091 assert_eq!(
1092 a.resume_and_show(&mut c, Instant::now()).unwrap(),
1093 ResumeStep::Nothing
1094 );
1095 assert!(c.calls.is_empty());
1096 }
1097
1098 #[test]
1099 fn resume_with_nothing_suspended_makes_no_call() {
1100 let (mut a, mut c) = (Applied::default(), Fake::default());
1101 assert_eq!(
1102 a.resume_and_show(&mut c, Instant::now()).unwrap(),
1103 ResumeStep::Nothing
1104 );
1105 assert!(c.calls.is_empty());
1106 }
1107
1108 #[test]
1109 fn a_hide_alone_is_undone_by_a_show_alone() {
1110 let (mut a, mut c) = (Applied::default(), Fake::default());
1111 a.hide(&mut c).unwrap();
1112 c.calls.clear();
1113 let step = a.resume_and_show(&mut c, Instant::now()).unwrap();
1114 assert_eq!(c.calls, ["show"]);
1115 assert_eq!(a.classify(true, step), ResumeStep::Shown);
1116 }
1117
1118 #[test]
1119 fn a_failed_suspend_is_forgotten_by_the_state_machine() {
1120 let mut s = IdleState::default();
1121 let (g, _) = s.on_hidden();
1122 assert_eq!(s.on_timer(g), IdleAction::SuspendNow);
1123 s.on_suspend_failed();
1124 assert_eq!(s.snapshot(), (true, false));
1125 let (_, action) = s.on_hidden();
1126 assert_eq!(action, IdleAction::HideControllerAndStartTimer);
1127 }
1128
1129 #[test]
1130 fn the_transition_hook_fires_once_per_call_and_is_silent_when_unset() {
1131 let count = Arc::new(AtomicUsize::new(0));
1132 let seen = Arc::new(Mutex::new(Vec::new()));
1133 let (c2, s2) = (count.clone(), seen.clone());
1134 let host = IdleHost::new(Duration::from_secs(1)).with_on_transition(Some(Arc::new(
1135 move |t: &IdleTransition| {
1136 c2.fetch_add(1, Ordering::SeqCst);
1137 s2.lock().unwrap().push(t.clone());
1138 },
1139 )));
1140 host.transition(
1141 IdleTransitionKind::Suspended,
1142 "main",
1143 Duration::from_secs(30),
1144 );
1145 host.transition(IdleTransitionKind::Resumed, "main", Duration::from_secs(4));
1146 assert_eq!(count.load(Ordering::SeqCst), 2);
1147 let seen = seen.lock().unwrap();
1148 assert_eq!(seen[0].kind, IdleTransitionKind::Suspended);
1149 assert_eq!(seen[0].window, "main");
1150 assert_eq!(seen[0].elapsed, Duration::from_secs(30));
1151 assert_eq!(seen[1].kind, IdleTransitionKind::Resumed);
1152 IdleHost::new(Duration::from_secs(1)).transition(
1154 IdleTransitionKind::Suspended,
1155 "main",
1156 Duration::ZERO,
1157 );
1158 }
1159
1160 #[test]
1161 fn the_minimise_trigger_honours_a_per_hide_delay() {
1162 let host = Duration::from_secs(30);
1163 assert_eq!(
1164 minimize_action(host, MinimizeStep::Start, None),
1165 MinimizeAction::Hide { delay: host }
1166 );
1167 assert_eq!(
1168 minimize_action(host, MinimizeStep::Start, Some(Duration::from_secs(2))),
1169 MinimizeAction::Hide {
1170 delay: Duration::from_secs(2)
1171 }
1172 );
1173 assert_eq!(
1174 minimize_action(
1175 host,
1176 MinimizeStep::Start,
1177 Some(Duration::from_secs(10 * 24 * 3600))
1178 ),
1179 MinimizeAction::Hide {
1180 delay: MAX_SUSPEND_AFTER
1181 }
1182 );
1183 assert_eq!(
1184 minimize_action(host, MinimizeStep::End, Some(Duration::ZERO)),
1185 MinimizeAction::Visible
1186 );
1187 assert_eq!(
1188 minimize_action(host, MinimizeStep::None, None),
1189 MinimizeAction::None
1190 );
1191 }
1192}