repame_shell/portal/lifecycle.rs
1//! App lifecycle: foreground or background, as the single fact the gate reads.
2//!
3//! The platform reports real visibility transitions (Android
4//! `resumed`/`suspended`, a desktop window hidden or occluded, the web's
5//! `visibilitychange`), and a game that is not on screen must stop playing and
6//! go quiet. Every game in the portal family carries the same pair of
7//! functions for this, plus the same static flag behind them; this is that
8//! pair, once.
9//!
10//! [`watch`] is idempotent, and it reads the state the app is already in before
11//! it subscribes, because the platform only reports *changes*: a game that
12//! boots straight into the background (an Android reopen, a hidden tab) would
13//! otherwise read as foreground until the player came back and left again.
14
15use std::sync::Once;
16use std::sync::atomic::{AtomicBool, Ordering};
17
18use repose_platform::AppLifecycle;
19
20/// Foreground until the platform says otherwise, and on every platform that
21/// never reports a lifecycle at all.
22static FOREGROUND: AtomicBool = AtomicBool::new(true);
23
24static WATCH: Once = Once::new();
25
26/// Subscribe to the app's lifecycle. Idempotent: calling it twice installs one
27/// listener, so a game may call it at boot and anything else may call it again.
28pub fn watch() {
29 WATCH.call_once(|| {
30 if let Some(state) = repose_platform::current_lifecycle() {
31 FOREGROUND.store(state == AppLifecycle::Foreground, Ordering::Relaxed);
32 }
33 // The listeners outlive this call and are never removed (the platform
34 // has no unsubscribe for them), so everything they touch is a static.
35 repose_platform::add_lifecycle_listener(Box::new(|state| {
36 FOREGROUND.store(state == AppLifecycle::Foreground, Ordering::Relaxed);
37 }));
38 });
39}
40
41/// Whether the app is in the foreground.
42///
43/// `true` before [`watch`] runs and wherever no platform lifecycle exists, so a
44/// game that never watches runs as it always did.
45pub fn foreground() -> bool {
46 FOREGROUND.load(Ordering::Relaxed)
47}
48
49/// Whether the app is in the background: the value the gate takes for
50/// [`PortalView::backgrounded`](super::PortalView::backgrounded).
51pub fn backgrounded() -> bool {
52 !foreground()
53}