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(Debug, Clone, Copy, PartialEq, Eq, Default)]
79pub enum IdleTrigger {
80 #[default]
83 HiddenOrMinimised,
84 Minimised,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum IdleSource {
95 Hide,
97 Minimise,
99}
100
101pub fn source_starts_idle(trigger: IdleTrigger, source: IdleSource) -> bool {
103 !matches!(
104 (trigger, source),
105 (IdleTrigger::Minimised, IdleSource::Hide)
106 )
107}
108
109#[derive(Clone)]
111pub struct SuspendAfterProvider(Arc<dyn Fn() -> Option<Duration> + Send + Sync>);
112
113impl std::fmt::Debug for SuspendAfterProvider {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 f.write_str("SuspendAfterProvider(..)")
116 }
117}
118
119impl PartialEq for SuspendAfterProvider {
122 fn eq(&self, other: &Self) -> bool {
123 Arc::ptr_eq(&self.0, &other.0)
124 }
125}
126
127impl Eq for SuspendAfterProvider {}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct IdleOptions {
138 pub suspend_after: Duration,
141 pub start_on_minimize: bool,
144 pub controller_hide: ControllerHide,
145 pub events: IdleEvents,
146 pub trigger: IdleTrigger,
148 suspend_after_fn: Option<SuspendAfterProvider>,
150}
151
152impl Default for IdleOptions {
153 fn default() -> Self {
154 Self {
155 suspend_after: DEFAULT_SUSPEND_AFTER,
156 start_on_minimize: false,
157 controller_hide: ControllerHide::Immediate,
158 events: IdleEvents::Always,
159 trigger: IdleTrigger::HiddenOrMinimised,
160 suspend_after_fn: None,
161 }
162 }
163}
164
165impl IdleOptions {
166 pub fn suspend_after(mut self, suspend_after: Duration) -> Self {
168 self.suspend_after = suspend_after;
169 self
170 }
171
172 pub fn start_on_minimize(mut self, start_on_minimize: bool) -> Self {
174 self.start_on_minimize = start_on_minimize;
175 self
176 }
177
178 pub fn controller_hide(mut self, controller_hide: ControllerHide) -> Self {
180 self.controller_hide = controller_hide;
181 self
182 }
183
184 pub fn trigger(mut self, trigger: IdleTrigger) -> Self {
186 self.trigger = trigger;
187 self
188 }
189
190 pub fn on_minimise(mut self, on_minimise: bool) -> Self {
193 self.trigger = if on_minimise {
194 IdleTrigger::Minimised
195 } else {
196 IdleTrigger::HiddenOrMinimised
197 };
198 self
199 }
200
201 pub fn minimise_enabled(&self) -> bool {
203 self.start_on_minimize || self.trigger == IdleTrigger::Minimised
204 }
205
206 pub fn events(mut self, events: IdleEvents) -> Self {
208 self.events = events;
209 self
210 }
211
212 pub fn suspend_after_with(
224 mut self,
225 provider: impl Fn() -> Option<Duration> + Send + Sync + 'static,
226 ) -> Self {
227 self.suspend_after_fn = Some(SuspendAfterProvider(Arc::new(provider)));
228 self
229 }
230
231 pub fn hide_delay(&self, host_default: Duration, per_hide: Option<Duration>) -> Duration {
235 effective_delay(host_default, per_hide.or_else(|| self.provided_delay()))
236 }
237
238 fn provided_delay(&self) -> Option<Duration> {
241 let provider = self.suspend_after_fn.as_ref()?;
242 catch_unwind(AssertUnwindSafe(|| (*provider.0)()))
243 .ok()
244 .flatten()
245 }
246}
247
248pub fn effective_delay(host_default: Duration, per_hide: Option<Duration>) -> Duration {
250 per_hide.unwrap_or(host_default).min(MAX_SUSPEND_AFTER)
251}
252
253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub struct HidePlan {
256 pub now: bool,
258 pub on_timer: bool,
260}
261
262pub fn hide_plan(mode: ControllerHide, controller_already_hidden: bool) -> HidePlan {
264 HidePlan {
265 now: mode == ControllerHide::Immediate && !controller_already_hidden,
266 on_timer: mode == ControllerHide::OnTimer && !controller_already_hidden,
267 }
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
272pub enum MinimizeStep {
273 Start,
274 End,
275 None,
276}
277
278#[derive(Debug, Default)]
281pub struct MinimizeTracker {
282 active: AtomicBool,
283}
284
285impl MinimizeTracker {
286 pub fn step(&self, minimized: bool) -> MinimizeStep {
287 let was = self.active.swap(minimized, Ordering::SeqCst);
288 match (was, minimized) {
289 (false, true) => MinimizeStep::Start,
290 (true, false) => MinimizeStep::End,
291 _ => MinimizeStep::None,
292 }
293 }
294
295 pub fn clear(&self) {
297 self.active.store(false, Ordering::SeqCst);
298 }
299}
300
301#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
303#[serde(rename_all = "lowercase")]
304pub enum IdlePhase {
305 Suspend,
306 Resume,
307}
308
309#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
312pub struct IdleFailure {
313 pub phase: IdlePhase,
314 pub window: String,
316 pub error: String,
318}
319
320pub type IdleErrorCallback = Arc<dyn Fn(&IdleFailure) + Send + Sync>;
322
323#[derive(Clone)]
325pub(crate) struct Reporter {
326 window: String,
327 sink: Option<IdleErrorCallback>,
328}
329
330impl Reporter {
331 #[cfg_attr(not(windows), allow(dead_code))]
332 pub(crate) fn fail(&self, phase: IdlePhase, error: impl Display) {
333 if let Some(sink) = &self.sink {
334 sink(&IdleFailure {
335 phase,
336 window: self.window.clone(),
337 error: error.to_string(),
338 });
339 }
340 }
341}
342
343#[derive(Debug, Clone, Copy, PartialEq, Eq)]
344pub enum IdleAction {
345 HideControllerAndStartTimer,
346 ShowController,
347 SuspendNow,
348 ResumeAndShowController,
349 None,
350}
351
352#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
353pub struct IdleState {
354 hidden: bool,
355 suspended: bool,
356 generation: u64,
357}
358
359impl IdleState {
360 pub fn on_hidden(&mut self) -> (u64, IdleAction) {
361 if self.suspended {
362 self.hidden = true;
363 return (self.generation, IdleAction::None);
364 }
365 self.hidden = true;
366 self.generation = self.generation.wrapping_add(1);
367 (self.generation, IdleAction::HideControllerAndStartTimer)
368 }
369
370 pub fn on_visible(&mut self) -> IdleAction {
371 let was_hidden = self.hidden;
372 self.hidden = false;
373 self.generation = self.generation.wrapping_add(1);
374 if std::mem::take(&mut self.suspended) {
375 IdleAction::ResumeAndShowController
376 } else if was_hidden {
377 IdleAction::ShowController
378 } else {
379 IdleAction::None
380 }
381 }
382
383 pub fn on_timer(&mut self, generation: u64) -> IdleAction {
384 if self.hidden && !self.suspended && self.generation == generation {
385 self.suspended = true;
386 IdleAction::SuspendNow
387 } else {
388 IdleAction::None
389 }
390 }
391
392 pub fn on_suspend_failed(&mut self) {
395 self.suspended = false;
396 }
397
398 pub fn snapshot(&self) -> (bool, bool) {
400 (self.hidden, self.suspended)
401 }
402}
403
404#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
406#[serde(rename_all = "lowercase")]
407pub enum IdleTransitionKind {
408 Suspended,
409 Resumed,
410}
411
412#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
415pub struct IdleTransition {
416 pub kind: IdleTransitionKind,
417 pub window: String,
419 pub elapsed: Duration,
423}
424
425pub type IdleTransitionCallback = Arc<dyn Fn(&IdleTransition) + Send + Sync>;
427
428pub(crate) trait Controller {
431 fn set_visible(&mut self, visible: bool) -> Result<(), String>;
432 fn try_suspend(&mut self) -> Result<(), String>;
434 fn resume(&mut self) -> Result<(), String>;
435}
436
437#[derive(Debug, Default)]
439pub(crate) struct Applied {
440 pub(crate) hidden: bool,
442 pub(crate) suspended: bool,
444 suspended_at: Option<Instant>,
445 resumed_for: Option<Duration>,
447}
448
449#[derive(Debug, Clone, PartialEq, Eq)]
450pub(crate) struct StepFailure {
451 pub(crate) phase: IdlePhase,
452 pub(crate) error: String,
453}
454
455#[derive(Debug, Clone, Copy, PartialEq, Eq)]
457pub(crate) enum ResumeStep {
458 Nothing,
460 Shown,
462 Resumed(Duration),
464}
465
466impl Applied {
467 pub(crate) fn hide(&mut self, c: &mut dyn Controller) -> Result<(), StepFailure> {
469 if self.hidden {
470 return Ok(());
471 }
472 c.set_visible(false).map_err(|error| StepFailure {
473 phase: IdlePhase::Suspend,
474 error: format!("SetIsVisible(false): {error}"),
475 })?;
476 self.hidden = true;
477 Ok(())
478 }
479
480 pub(crate) fn hide_and_suspend(
483 &mut self,
484 c: &mut dyn Controller,
485 now: Instant,
486 ) -> Result<(), StepFailure> {
487 self.hide(c)?;
488 if self.suspended {
489 return Ok(());
490 }
491 c.try_suspend().map_err(|error| StepFailure {
492 phase: IdlePhase::Suspend,
493 error: format!("TrySuspend: {error}"),
494 })?;
495 self.suspended = true;
496 self.suspended_at = Some(now);
497 Ok(())
498 }
499
500 pub(crate) fn resume_and_show(
503 &mut self,
504 c: &mut dyn Controller,
505 now: Instant,
506 ) -> Result<ResumeStep, StepFailure> {
507 let mut resumed = None;
508 if self.suspended {
509 c.resume().map_err(|error| StepFailure {
510 phase: IdlePhase::Resume,
511 error: format!("Resume: {error}"),
512 })?;
513 self.suspended = false;
514 let for_ = self
515 .suspended_at
516 .take()
517 .map(|at| now.saturating_duration_since(at))
518 .unwrap_or_default();
519 self.resumed_for = Some(for_);
520 }
521 if self.hidden {
522 c.set_visible(true).map_err(|error| StepFailure {
523 phase: IdlePhase::Resume,
524 error: format!("SetIsVisible(true): {error}"),
525 })?;
526 self.hidden = false;
527 }
528 if let Some(for_) = self.resumed_for.take() {
529 resumed = Some(for_);
530 }
531 Ok(match resumed {
532 Some(for_) => ResumeStep::Resumed(for_),
533 None => ResumeStep::Nothing,
534 })
535 }
536}
537
538impl Applied {
539 fn classify(&self, before_hidden: bool, step: ResumeStep) -> ResumeStep {
541 match step {
542 ResumeStep::Nothing if before_hidden => ResumeStep::Shown,
543 other => other,
544 }
545 }
546}
547
548pub struct IdleHost {
550 pub suspend_after: Duration,
551 pub options: IdleOptions,
552 pub minimize: MinimizeTracker,
553 pub state: Mutex<IdleState>,
554 pub counters: Mutex<(u32, u32)>,
556 pub on_error: Option<IdleErrorCallback>,
558 pub on_transition: Option<IdleTransitionCallback>,
560 pub(crate) applied: Mutex<Applied>,
561}
562
563impl IdleHost {
564 pub fn new(suspend_after: Duration) -> Self {
565 Self {
566 suspend_after,
567 options: IdleOptions {
568 suspend_after,
569 ..IdleOptions::default()
570 },
571 minimize: MinimizeTracker::default(),
572 state: Mutex::new(IdleState::default()),
573 counters: Mutex::new((0, 0)),
574 on_error: None,
575 on_transition: None,
576 applied: Mutex::new(Applied::default()),
577 }
578 }
579
580 pub fn with_options(mut self, options: IdleOptions) -> Self {
582 self.suspend_after = options.suspend_after.min(MAX_SUSPEND_AFTER);
583 self.options = IdleOptions {
584 suspend_after: self.suspend_after,
585 ..options
586 };
587 self
588 }
589
590 pub fn with_on_error(mut self, on_error: Option<IdleErrorCallback>) -> Self {
592 self.on_error = on_error;
593 self
594 }
595
596 pub fn with_on_transition(mut self, on_transition: Option<IdleTransitionCallback>) -> Self {
598 self.on_transition = on_transition;
599 self
600 }
601
602 pub fn report_failure(&self, phase: IdlePhase, window: &str, error: &str) {
604 self.reporter(window).fail(phase, error);
605 }
606
607 pub(crate) fn reporter(&self, window: &str) -> Reporter {
608 Reporter {
609 window: window.to_owned(),
610 sink: self.on_error.clone(),
611 }
612 }
613
614 pub(crate) fn transition(&self, kind: IdleTransitionKind, window: &str, elapsed: Duration) {
616 if let Some(sink) = &self.on_transition {
617 sink(&IdleTransition {
618 kind,
619 window: window.to_owned(),
620 elapsed,
621 });
622 }
623 }
624}
625
626#[derive(Debug, Clone, Serialize)]
627#[serde(rename_all = "camelCase")]
628pub struct IdleStatus {
629 pub enabled: bool,
630 pub hidden: bool,
631 pub suspended: bool,
632 pub suspend_after_ms: u64,
633 pub suspends: u32,
634 pub resumes: u32,
635 pub native_suspend: bool,
637}
638
639pub fn status(app: &AppHandle<Wry>) -> IdleStatus {
640 let state = app.state::<ShellState>();
641 match &state.idle {
642 Some(h) => {
643 let (hidden, suspended) = h.state.lock().unwrap_or_else(|e| e.into_inner()).snapshot();
644 let (suspends, resumes) = *h.counters.lock().unwrap_or_else(|e| e.into_inner());
645 IdleStatus {
646 enabled: true,
647 hidden,
648 suspended,
649 suspend_after_ms: h.suspend_after.as_millis() as u64,
650 suspends,
651 resumes,
652 native_suspend: cfg!(windows),
653 }
654 }
655 None => IdleStatus {
656 enabled: false,
657 hidden: false,
658 suspended: false,
659 suspend_after_ms: 0,
660 suspends: 0,
661 resumes: 0,
662 native_suspend: cfg!(windows),
663 },
664 }
665}
666
667fn emit(app: &AppHandle<Wry>, host: &IdleHost, state: &str) {
668 if host.options.events.emits(cfg!(windows)) {
669 let _ = app.emit(EVENT_IDLE, serde_json::json!({"state": state}));
670 }
671}
672
673pub fn note_hidden(app: &AppHandle<Wry>, controller_already_hidden: bool) {
676 note_hidden_after(app, controller_already_hidden, None);
677}
678
679pub fn note_hidden_after(
681 app: &AppHandle<Wry>,
682 controller_already_hidden: bool,
683 suspend_after: Option<Duration>,
684) {
685 let state = app.state::<ShellState>();
686 let Some(host) = &state.idle else { return };
687 if !source_starts_idle(host.options.trigger, IdleSource::Hide) {
688 return;
689 }
690 start_hide(app, controller_already_hidden, suspend_after);
691}
692
693fn start_hide(
695 app: &AppHandle<Wry>,
696 controller_already_hidden: bool,
697 suspend_after: Option<Duration>,
698) {
699 let state = app.state::<ShellState>();
700 let Some(host) = &state.idle else { return };
701 let (generation, action) = host
702 .state
703 .lock()
704 .unwrap_or_else(|e| e.into_inner())
705 .on_hidden();
706 if action != IdleAction::HideControllerAndStartTimer {
707 return;
708 }
709 emit(app, host, "hidden");
710 let hidden_at = Instant::now();
711 let plan = hide_plan(host.options.controller_hide, controller_already_hidden);
712 if plan.now {
713 hide_controller(app);
714 }
715 let delay = host.options.hide_delay(host.suspend_after, suspend_after);
718 let app = app.clone();
719 let _ = std::thread::Builder::new()
720 .name("rk-shell-idle".into())
721 .spawn(move || {
722 std::thread::sleep(delay);
723 suspend_controller(&app, generation, hidden_at);
724 });
725}
726
727fn hide_controller(app: &AppHandle<Wry>) {
730 let state = app.state::<ShellState>();
731 let Some(host) = &state.idle else { return };
732 let reporter = host.reporter(&state.main_label);
733 let app = app.clone();
734 let run = platform::with_controller(&app.clone(), move |c| {
735 let state = app.state::<ShellState>();
736 let Some(host) = &state.idle else { return };
737 let mut applied = host.applied.lock().unwrap_or_else(|e| e.into_inner());
738 let still_hidden = host
739 .state
740 .lock()
741 .unwrap_or_else(|e| e.into_inner())
742 .snapshot()
743 .0;
744 if !still_hidden {
745 return;
746 }
747 if let Err(f) = applied.hide(c) {
748 host.reporter(&state.main_label).fail(f.phase, f.error);
749 }
750 });
751 if let Err(e) = run {
752 reporter.fail(IdlePhase::Suspend, e);
753 }
754}
755
756fn suspend_controller(app: &AppHandle<Wry>, generation: u64, hidden_at: Instant) {
759 let state = app.state::<ShellState>();
760 let Some(host) = &state.idle else { return };
761 let reporter = host.reporter(&state.main_label);
762 let handle = app.clone();
763 let run = platform::with_controller(app, move |c| {
764 let state = handle.state::<ShellState>();
765 let Some(host) = &state.idle else { return };
766 let mut applied = host.applied.lock().unwrap_or_else(|e| e.into_inner());
767 let action = host
768 .state
769 .lock()
770 .unwrap_or_else(|e| e.into_inner())
771 .on_timer(generation);
772 if action != IdleAction::SuspendNow {
773 return;
774 }
775 let now = Instant::now();
776 match applied.hide_and_suspend(c, now) {
777 Ok(()) => {
778 host.counters.lock().unwrap_or_else(|e| e.into_inner()).0 += 1;
779 emit(&handle, host, "suspended");
780 host.transition(
781 IdleTransitionKind::Suspended,
782 &state.main_label,
783 now.saturating_duration_since(hidden_at),
784 );
785 }
786 Err(f) => {
787 host.state
788 .lock()
789 .unwrap_or_else(|e| e.into_inner())
790 .on_suspend_failed();
791 host.reporter(&state.main_label).fail(f.phase, f.error);
792 }
793 }
794 });
795 if let Err(e) = run {
796 reporter.fail(IdlePhase::Suspend, e);
797 }
798}
799
800#[derive(Debug, Clone, Copy, PartialEq, Eq)]
802pub enum MinimizeAction {
803 Hide {
805 delay: Duration,
806 },
807 Visible,
808 None,
809}
810
811pub fn minimize_action(
814 host_default: Duration,
815 step: MinimizeStep,
816 per_hide: Option<Duration>,
817) -> MinimizeAction {
818 match step {
819 MinimizeStep::Start => MinimizeAction::Hide {
820 delay: effective_delay(host_default, per_hide),
821 },
822 MinimizeStep::End => MinimizeAction::Visible,
823 MinimizeStep::None => MinimizeAction::None,
824 }
825}
826
827pub fn note_minimized(app: &AppHandle<Wry>, minimized: bool) {
832 note_minimized_after(app, minimized, None);
833}
834
835pub fn note_minimized_after(
838 app: &AppHandle<Wry>,
839 minimized: bool,
840 suspend_after: Option<Duration>,
841) {
842 let state = app.state::<ShellState>();
843 let Some(host) = &state.idle else { return };
844 if !host.options.minimise_enabled() {
845 return;
846 }
847 let step = host.minimize.step(minimized);
848 let per_hide = match step {
851 MinimizeStep::Start => Some(host.options.hide_delay(host.suspend_after, suspend_after)),
852 MinimizeStep::End | MinimizeStep::None => suspend_after,
853 };
854 match minimize_action(host.suspend_after, step, per_hide) {
855 MinimizeAction::Hide { delay } => start_hide(app, false, Some(delay)),
856 MinimizeAction::Visible => note_visible(app),
857 MinimizeAction::None => {}
858 }
859}
860
861pub fn note_visible(app: &AppHandle<Wry>) {
865 let state = app.state::<ShellState>();
866 let Some(host) = &state.idle else { return };
867 host.minimize.clear();
868 let action = host
869 .state
870 .lock()
871 .unwrap_or_else(|e| e.into_inner())
872 .on_visible();
873 let reporter = host.reporter(&state.main_label);
874 let handle = app.clone();
875 let run = platform::with_controller(app, move |c| {
876 let state = handle.state::<ShellState>();
877 let Some(host) = &state.idle else { return };
878 let mut applied = host.applied.lock().unwrap_or_else(|e| e.into_inner());
879 let before_hidden = applied.hidden;
880 let now = Instant::now();
881 match applied.resume_and_show(c, now) {
882 Ok(step) => match applied.classify(before_hidden, step) {
883 ResumeStep::Resumed(for_) => {
884 host.counters.lock().unwrap_or_else(|e| e.into_inner()).1 += 1;
885 emit(&handle, host, "resumed");
886 host.transition(IdleTransitionKind::Resumed, &state.main_label, for_);
887 }
888 ResumeStep::Shown => emit(&handle, host, "visible"),
889 ResumeStep::Nothing => {
890 if matches!(
891 action,
892 IdleAction::ShowController | IdleAction::ResumeAndShowController
893 ) {
894 emit(&handle, host, "visible");
895 }
896 }
897 },
898 Err(f) => host.reporter(&state.main_label).fail(f.phase, f.error),
899 }
900 });
901 if let Err(e) = run {
902 reporter.fail(IdlePhase::Resume, e);
903 }
904}
905
906#[cfg(windows)]
907mod platform {
908 use crate::winapi::webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2_3;
909 use crate::winapi::windows::core::Interface;
910 use tauri::{AppHandle, Manager, Wry};
911
912 use super::Controller;
913 use crate::ShellState;
914
915 struct Wv2<'a> {
917 visible: &'a dyn Fn(bool) -> Result<(), String>,
918 suspend: &'a dyn Fn() -> Result<(), String>,
919 resume: &'a dyn Fn() -> Result<(), String>,
920 }
921
922 impl Controller for Wv2<'_> {
923 fn set_visible(&mut self, visible: bool) -> Result<(), String> {
924 (self.visible)(visible)
925 }
926 fn try_suspend(&mut self) -> Result<(), String> {
927 (self.suspend)()
928 }
929 fn resume(&mut self) -> Result<(), String> {
930 (self.resume)()
931 }
932 }
933
934 pub fn with_controller(
937 app: &AppHandle<Wry>,
938 f: impl FnOnce(&mut dyn Controller) + Send + 'static,
939 ) -> Result<(), String> {
940 let label = app.state::<ShellState>().main_label.clone();
941 let w = app
942 .get_webview_window(&label)
943 .ok_or_else(|| "main window not found".to_string())?;
944 let report = app
945 .state::<ShellState>()
946 .idle
947 .as_ref()
948 .map(|h| h.reporter(&label));
949 w.with_webview(move |webview| {
950 let controller = webview.controller();
951 let visible = |v: bool| -> Result<(), String> {
952 unsafe { controller.SetIsVisible(v) }.map_err(|e| e.to_string())
954 };
955 let core3 = || -> Result<ICoreWebView2_3, String> {
956 let core = unsafe { controller.CoreWebView2() }.map_err(|e| e.to_string())?;
958 core.cast::<ICoreWebView2_3>().map_err(|e| e.to_string())
959 };
960 let suspend = || -> Result<(), String> {
961 let core3 = core3()?;
962 let completion = report.clone();
963 let handler = crate::winapi::webview2_com::TrySuspendCompletedHandler::create(
964 Box::new(move |error, _suspended| {
965 if let (Err(e), Some(r)) = (error, &completion) {
966 r.fail(super::IdlePhase::Suspend, e);
967 }
968 Ok(())
969 }),
970 );
971 unsafe { core3.TrySuspend(&handler) }.map_err(|e| e.to_string())
973 };
974 let resume = || -> Result<(), String> {
975 let core3 = core3()?;
976 unsafe { core3.Resume() }.map_err(|e| e.to_string())
978 };
979 let mut c = Wv2 {
980 visible: &visible,
981 suspend: &suspend,
982 resume: &resume,
983 };
984 f(&mut c);
985 })
986 .map_err(|e| e.to_string())
987 }
988}
989
990#[cfg(not(windows))]
991mod platform {
992 use tauri::{AppHandle, Wry};
993
994 use super::Controller;
995
996 struct Noop;
999
1000 impl Controller for Noop {
1001 fn set_visible(&mut self, _: bool) -> Result<(), String> {
1002 Ok(())
1003 }
1004 fn try_suspend(&mut self) -> Result<(), String> {
1005 Ok(())
1006 }
1007 fn resume(&mut self) -> Result<(), String> {
1008 Ok(())
1009 }
1010 }
1011
1012 pub fn with_controller(
1013 _: &AppHandle<Wry>,
1014 f: impl FnOnce(&mut dyn Controller) + Send + 'static,
1015 ) -> Result<(), String> {
1016 f(&mut Noop);
1017 Ok(())
1018 }
1019}
1020
1021#[cfg(test)]
1022mod tests {
1023 use super::*;
1024 use std::collections::HashSet;
1025 use std::sync::atomic::AtomicUsize;
1026
1027 #[derive(Default)]
1029 struct Fake {
1030 calls: Vec<&'static str>,
1031 failing: HashSet<&'static str>,
1032 }
1033
1034 impl Fake {
1035 fn failing(names: &[&'static str]) -> Self {
1036 Self {
1037 failing: names.iter().copied().collect(),
1038 ..Self::default()
1039 }
1040 }
1041 fn call(&mut self, name: &'static str) -> Result<(), String> {
1042 self.calls.push(name);
1043 if self.failing.contains(name) {
1044 Err(format!("{name} failed"))
1045 } else {
1046 Ok(())
1047 }
1048 }
1049 }
1050
1051 impl Controller for Fake {
1052 fn set_visible(&mut self, visible: bool) -> Result<(), String> {
1053 self.call(if visible { "show" } else { "hide" })
1054 }
1055 fn try_suspend(&mut self) -> Result<(), String> {
1056 self.call("suspend")
1057 }
1058 fn resume(&mut self) -> Result<(), String> {
1059 self.call("resume")
1060 }
1061 }
1062
1063 #[test]
1064 fn suspend_is_hide_then_suspend() {
1065 let (mut a, mut c) = (Applied::default(), Fake::default());
1066 a.hide_and_suspend(&mut c, Instant::now()).unwrap();
1067 assert_eq!(c.calls, ["hide", "suspend"]);
1068 assert!(a.hidden && a.suspended);
1069 }
1070
1071 #[test]
1072 fn a_failed_hide_means_no_suspend_and_a_running_webview() {
1073 let (mut a, mut c) = (Applied::default(), Fake::failing(&["hide"]));
1074 let f = a.hide_and_suspend(&mut c, Instant::now()).unwrap_err();
1075 assert_eq!(f.phase, IdlePhase::Suspend);
1076 assert_eq!(c.calls, ["hide"], "TrySuspend must not run");
1077 assert!(!a.hidden && !a.suspended);
1078 }
1079
1080 #[test]
1081 fn an_immediate_hide_is_not_repeated_by_the_suspend() {
1082 let (mut a, mut c) = (Applied::default(), Fake::default());
1083 a.hide(&mut c).unwrap();
1084 a.hide_and_suspend(&mut c, Instant::now()).unwrap();
1085 assert_eq!(c.calls, ["hide", "suspend"]);
1086 }
1087
1088 #[test]
1089 fn a_failed_immediate_hide_is_retried_by_the_suspend() {
1090 let (mut a, mut c) = (Applied::default(), Fake::failing(&["hide"]));
1091 assert!(a.hide(&mut c).is_err());
1092 c.failing.clear();
1093 a.hide_and_suspend(&mut c, Instant::now()).unwrap();
1094 assert_eq!(c.calls, ["hide", "hide", "suspend"]);
1095 }
1096
1097 #[test]
1098 fn a_failed_try_suspend_leaves_the_controller_hidden_not_suspended() {
1099 let (mut a, mut c) = (Applied::default(), Fake::failing(&["suspend"]));
1100 assert!(a.hide_and_suspend(&mut c, Instant::now()).is_err());
1101 assert!(a.hidden && !a.suspended);
1102 c.calls.clear();
1104 assert_eq!(
1105 a.resume_and_show(&mut c, Instant::now()).unwrap(),
1106 ResumeStep::Nothing
1107 );
1108 assert_eq!(c.calls, ["show"]);
1109 }
1110
1111 #[test]
1112 fn resume_is_resume_then_show() {
1113 let (mut a, mut c) = (Applied::default(), Fake::default());
1114 let t0 = Instant::now();
1115 a.hide_and_suspend(&mut c, t0).unwrap();
1116 c.calls.clear();
1117 let step = a
1118 .resume_and_show(&mut c, t0 + Duration::from_secs(5))
1119 .unwrap();
1120 assert_eq!(c.calls, ["resume", "show"]);
1121 assert_eq!(step, ResumeStep::Resumed(Duration::from_secs(5)));
1122 assert!(!a.hidden && !a.suspended);
1123 }
1124
1125 #[test]
1126 fn a_failed_resume_means_no_show_and_an_error_then_retries() {
1127 let (mut a, mut c) = (Applied::default(), Fake::default());
1128 a.hide_and_suspend(&mut c, Instant::now()).unwrap();
1129 c.calls.clear();
1130 c.failing.insert("resume");
1131 let f = a.resume_and_show(&mut c, Instant::now()).unwrap_err();
1132 assert_eq!(f.phase, IdlePhase::Resume);
1133 assert_eq!(c.calls, ["resume"], "no show after a failed resume");
1134 assert!(a.hidden && a.suspended, "still pending");
1135 c.failing.clear();
1136 c.calls.clear();
1137 assert!(matches!(
1138 a.resume_and_show(&mut c, Instant::now()),
1139 Ok(ResumeStep::Resumed(_))
1140 ));
1141 assert_eq!(c.calls, ["resume", "show"]);
1142 }
1143
1144 #[test]
1145 fn a_failed_show_is_surfaced_and_retried_on_the_next_request_without_resuming_again() {
1146 let (mut a, mut c) = (Applied::default(), Fake::default());
1147 a.hide_and_suspend(&mut c, Instant::now()).unwrap();
1148 c.calls.clear();
1149 c.failing.insert("show");
1150 let f = a.resume_and_show(&mut c, Instant::now()).unwrap_err();
1151 assert_eq!(f.phase, IdlePhase::Resume);
1152 assert_eq!(c.calls, ["resume", "show"]);
1153 assert!(a.hidden && !a.suspended);
1154 c.failing.clear();
1155 c.calls.clear();
1156 let step = a.resume_and_show(&mut c, Instant::now()).unwrap();
1157 assert_eq!(c.calls, ["show"]);
1158 assert!(
1159 matches!(step, ResumeStep::Resumed(_)),
1160 "resumed hook still fires once"
1161 );
1162 c.calls.clear();
1164 assert_eq!(
1165 a.resume_and_show(&mut c, Instant::now()).unwrap(),
1166 ResumeStep::Nothing
1167 );
1168 assert!(c.calls.is_empty());
1169 }
1170
1171 #[test]
1172 fn resume_with_nothing_suspended_makes_no_call() {
1173 let (mut a, mut c) = (Applied::default(), Fake::default());
1174 assert_eq!(
1175 a.resume_and_show(&mut c, Instant::now()).unwrap(),
1176 ResumeStep::Nothing
1177 );
1178 assert!(c.calls.is_empty());
1179 }
1180
1181 #[test]
1182 fn a_hide_alone_is_undone_by_a_show_alone() {
1183 let (mut a, mut c) = (Applied::default(), Fake::default());
1184 a.hide(&mut c).unwrap();
1185 c.calls.clear();
1186 let step = a.resume_and_show(&mut c, Instant::now()).unwrap();
1187 assert_eq!(c.calls, ["show"]);
1188 assert_eq!(a.classify(true, step), ResumeStep::Shown);
1189 }
1190
1191 #[test]
1192 fn a_failed_suspend_is_forgotten_by_the_state_machine() {
1193 let mut s = IdleState::default();
1194 let (g, _) = s.on_hidden();
1195 assert_eq!(s.on_timer(g), IdleAction::SuspendNow);
1196 s.on_suspend_failed();
1197 assert_eq!(s.snapshot(), (true, false));
1198 let (_, action) = s.on_hidden();
1199 assert_eq!(action, IdleAction::HideControllerAndStartTimer);
1200 }
1201
1202 #[test]
1203 fn the_transition_hook_fires_once_per_call_and_is_silent_when_unset() {
1204 let count = Arc::new(AtomicUsize::new(0));
1205 let seen = Arc::new(Mutex::new(Vec::new()));
1206 let (c2, s2) = (count.clone(), seen.clone());
1207 let host = IdleHost::new(Duration::from_secs(1)).with_on_transition(Some(Arc::new(
1208 move |t: &IdleTransition| {
1209 c2.fetch_add(1, Ordering::SeqCst);
1210 s2.lock().unwrap().push(t.clone());
1211 },
1212 )));
1213 host.transition(
1214 IdleTransitionKind::Suspended,
1215 "main",
1216 Duration::from_secs(30),
1217 );
1218 host.transition(IdleTransitionKind::Resumed, "main", Duration::from_secs(4));
1219 assert_eq!(count.load(Ordering::SeqCst), 2);
1220 let seen = seen.lock().unwrap();
1221 assert_eq!(seen[0].kind, IdleTransitionKind::Suspended);
1222 assert_eq!(seen[0].window, "main");
1223 assert_eq!(seen[0].elapsed, Duration::from_secs(30));
1224 assert_eq!(seen[1].kind, IdleTransitionKind::Resumed);
1225 IdleHost::new(Duration::from_secs(1)).transition(
1227 IdleTransitionKind::Suspended,
1228 "main",
1229 Duration::ZERO,
1230 );
1231 }
1232
1233 struct Cycle {
1235 tracker: MinimizeTracker,
1236 state: IdleState,
1237 applied: Applied,
1238 c: Fake,
1239 }
1240
1241 impl Cycle {
1242 fn new() -> Self {
1243 Self {
1244 tracker: MinimizeTracker::default(),
1245 state: IdleState::default(),
1246 applied: Applied::default(),
1247 c: Fake::default(),
1248 }
1249 }
1250 fn minimised(&mut self, minimised: bool, timer: bool) {
1252 match self.tracker.step(minimised) {
1253 MinimizeStep::Start => {
1254 let (g, _) = self.state.on_hidden();
1255 self.applied.hide(&mut self.c).unwrap();
1256 if timer && self.state.on_timer(g) == IdleAction::SuspendNow {
1257 self.applied
1258 .hide_and_suspend(&mut self.c, Instant::now())
1259 .unwrap();
1260 }
1261 }
1262 MinimizeStep::End => {
1263 self.tracker.clear();
1264 self.state.on_visible();
1265 self.applied
1266 .resume_and_show(&mut self.c, Instant::now())
1267 .unwrap();
1268 }
1269 MinimizeStep::None => {}
1270 }
1271 }
1272 }
1273
1274 #[test]
1275 fn minimise_only_trigger_ignores_hides_and_always_accepts_a_minimise() {
1276 for trigger in [IdleTrigger::HiddenOrMinimised, IdleTrigger::Minimised] {
1277 assert!(source_starts_idle(trigger, IdleSource::Minimise));
1278 }
1279 assert!(source_starts_idle(
1280 IdleTrigger::HiddenOrMinimised,
1281 IdleSource::Hide
1282 ));
1283 assert!(!source_starts_idle(
1284 IdleTrigger::Minimised,
1285 IdleSource::Hide
1286 ));
1287 let o = IdleOptions::default().on_minimise(true);
1288 assert_eq!(o.trigger, IdleTrigger::Minimised);
1289 assert!(o.minimise_enabled() && !o.start_on_minimize);
1290 assert!(!IdleOptions::default().minimise_enabled());
1291 assert_eq!(o.on_minimise(false).trigger, IdleTrigger::HiddenOrMinimised);
1292 }
1293
1294 #[test]
1295 fn minimise_then_restore_suspends_and_resumes_in_order() {
1296 let mut m = Cycle::new();
1297 m.minimised(true, true);
1298 assert_eq!(m.c.calls, ["hide", "suspend"]);
1299 m.c.calls.clear();
1300 m.minimised(false, false);
1301 assert_eq!(m.c.calls, ["resume", "show"]);
1302 assert_eq!(m.state.snapshot(), (false, false));
1303 }
1304
1305 #[test]
1306 fn a_double_minimise_changes_nothing_the_second_time() {
1307 let mut m = Cycle::new();
1308 m.minimised(true, true);
1309 m.c.calls.clear();
1310 m.minimised(true, true);
1311 assert!(m.c.calls.is_empty());
1312 m.minimised(false, false);
1313 assert_eq!(m.c.calls, ["resume", "show"]);
1314 }
1315
1316 #[test]
1317 fn a_restore_before_the_timer_only_shows_and_never_resumes() {
1318 let mut m = Cycle::new();
1319 m.minimised(true, false);
1320 m.minimised(false, false);
1321 assert_eq!(m.c.calls, ["hide", "show"]);
1322 }
1323
1324 #[test]
1325 fn a_restore_with_no_minimise_makes_no_controller_call() {
1326 let mut m = Cycle::new();
1327 m.minimised(false, false);
1328 assert!(m.c.calls.is_empty());
1329 }
1330
1331 #[test]
1332 fn the_minimise_trigger_honours_a_per_hide_delay() {
1333 let host = Duration::from_secs(30);
1334 assert_eq!(
1335 minimize_action(host, MinimizeStep::Start, None),
1336 MinimizeAction::Hide { delay: host }
1337 );
1338 assert_eq!(
1339 minimize_action(host, MinimizeStep::Start, Some(Duration::from_secs(2))),
1340 MinimizeAction::Hide {
1341 delay: Duration::from_secs(2)
1342 }
1343 );
1344 assert_eq!(
1345 minimize_action(
1346 host,
1347 MinimizeStep::Start,
1348 Some(Duration::from_secs(10 * 24 * 3600))
1349 ),
1350 MinimizeAction::Hide {
1351 delay: MAX_SUSPEND_AFTER
1352 }
1353 );
1354 assert_eq!(
1355 minimize_action(host, MinimizeStep::End, Some(Duration::ZERO)),
1356 MinimizeAction::Visible
1357 );
1358 assert_eq!(
1359 minimize_action(host, MinimizeStep::None, None),
1360 MinimizeAction::None
1361 );
1362 }
1363}