use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
#[cfg(not(target_arch = "wasm32"))]
use std::sync::{Arc, Mutex, OnceLock};
#[cfg(not(target_arch = "wasm32"))]
type BackListener = Arc<dyn Fn() + Send + Sync + 'static>;
#[cfg(target_arch = "wasm32")]
type BackListener = std::rc::Rc<dyn Fn() + 'static>;
static BACK_REQUESTS: AtomicUsize = AtomicUsize::new(0);
static BACK_INTERCEPTION: AtomicBool = AtomicBool::new(false);
static NEXT_LISTENER_ID: AtomicU64 = AtomicU64::new(1);
static EXIT_REQUESTED: AtomicBool = AtomicBool::new(false);
pub fn push_back_request() {
BACK_REQUESTS.fetch_add(1, Ordering::SeqCst);
if let Some(listener) = latest_back_listener() {
listener();
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn observe_back_requests(listener: impl Fn() + Send + Sync + 'static) -> BackRequestObserver {
let id = NEXT_LISTENER_ID.fetch_add(1, Ordering::Relaxed);
if let Ok(mut listeners) = back_listeners().lock() {
listeners.push((id, Arc::new(listener)));
}
BackRequestObserver { id }
}
#[cfg(target_arch = "wasm32")]
pub fn observe_back_requests(listener: impl Fn() + 'static) -> BackRequestObserver {
let id = NEXT_LISTENER_ID.fetch_add(1, Ordering::Relaxed);
BACK_LISTENERS.with(|listeners| {
listeners
.borrow_mut()
.push((id, std::rc::Rc::new(listener)))
});
BackRequestObserver { id }
}
#[cfg(not(target_arch = "wasm32"))]
fn back_listeners() -> &'static Mutex<Vec<(u64, BackListener)>> {
static LISTENERS: OnceLock<Mutex<Vec<(u64, BackListener)>>> = OnceLock::new();
LISTENERS.get_or_init(|| Mutex::new(Vec::new()))
}
#[cfg(target_arch = "wasm32")]
thread_local! {
static BACK_LISTENERS: std::cell::RefCell<Vec<(u64, BackListener)>> = const { std::cell::RefCell::new(Vec::new()) };
}
#[cfg(not(target_arch = "wasm32"))]
fn latest_back_listener() -> Option<BackListener> {
back_listeners()
.lock()
.ok()
.and_then(|listeners| listeners.last().map(|(_, listener)| Arc::clone(listener)))
}
#[cfg(target_arch = "wasm32")]
fn latest_back_listener() -> Option<BackListener> {
BACK_LISTENERS.with(|listeners| {
listeners
.borrow()
.last()
.map(|(_, listener)| std::rc::Rc::clone(listener))
})
}
pub struct BackRequestObserver {
id: u64,
}
impl Drop for BackRequestObserver {
fn drop(&mut self) {
#[cfg(not(target_arch = "wasm32"))]
if let Ok(mut listeners) = back_listeners().lock() {
listeners.retain(|(id, _)| *id != self.id);
}
#[cfg(target_arch = "wasm32")]
BACK_LISTENERS.with(|listeners| listeners.borrow_mut().retain(|(id, _)| *id != self.id));
}
}
pub fn take_back_requests() -> usize {
BACK_REQUESTS.swap(0, Ordering::SeqCst)
}
pub fn set_back_interception(enabled: bool) {
BACK_INTERCEPTION.store(enabled, Ordering::SeqCst);
}
pub fn back_interception_enabled() -> bool {
BACK_INTERCEPTION.load(Ordering::SeqCst)
}
pub fn request_exit() {
EXIT_REQUESTED.store(true, Ordering::SeqCst);
if let Some(listener) = latest_back_listener() {
listener();
}
}
pub fn exit_requested() -> bool {
EXIT_REQUESTED.load(Ordering::SeqCst)
}
pub fn take_exit_request() -> bool {
exit_requested() && EXIT_REQUESTED.swap(false, Ordering::SeqCst)
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
fn navigation_lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
#[test]
fn requests_accumulate_and_drain() {
let _guard = navigation_lock();
let _ = take_back_requests();
push_back_request();
push_back_request();
assert_eq!(take_back_requests(), 2);
assert_eq!(take_back_requests(), 0);
}
#[test]
fn a_registered_listener_hears_every_request() {
let _guard = navigation_lock();
let heard = Arc::new(AtomicUsize::new(0));
let counter = Arc::clone(&heard);
let _observer = observe_back_requests(move || {
counter.fetch_add(1, Ordering::SeqCst);
});
let before = heard.load(Ordering::SeqCst);
push_back_request();
push_back_request();
assert_eq!(heard.load(Ordering::SeqCst), before + 2);
let _ = take_back_requests();
let before = heard.load(Ordering::SeqCst);
request_exit();
assert_eq!(
heard.load(Ordering::SeqCst),
before + 1,
"an exit request has to wake an idle app the way a back request does"
);
let _ = take_exit_request();
}
#[test]
fn the_latest_back_observer_wins_until_it_is_dropped() {
let _guard = navigation_lock();
let first = Arc::new(AtomicUsize::new(0));
let second = Arc::new(AtomicUsize::new(0));
let first_seen = Arc::clone(&first);
let first_observer = observe_back_requests(move || {
first_seen.fetch_add(1, Ordering::SeqCst);
});
let second_seen = Arc::clone(&second);
let second_observer = observe_back_requests(move || {
second_seen.fetch_add(1, Ordering::SeqCst);
});
push_back_request();
assert_eq!(first.load(Ordering::SeqCst), 0);
assert_eq!(second.load(Ordering::SeqCst), 1);
drop(second_observer);
push_back_request();
assert_eq!(first.load(Ordering::SeqCst), 1);
drop(first_observer);
let _ = take_back_requests();
}
#[test]
fn an_exit_request_is_taken_once() {
let _guard = navigation_lock();
let _ = take_exit_request();
assert!(!take_exit_request());
request_exit();
request_exit();
assert!(take_exit_request());
assert!(
!take_exit_request(),
"a drained request came back; the platform would close twice"
);
}
#[test]
fn a_backend_can_look_at_the_request_without_consuming_it() {
let _guard = navigation_lock();
let _ = take_exit_request();
assert!(!exit_requested());
request_exit();
assert!(exit_requested());
assert!(
exit_requested(),
"looking at the request consumed it, which is the bug"
);
assert!(take_exit_request());
assert!(!exit_requested());
}
#[test]
fn interception_defaults_off_and_toggles() {
let _guard = navigation_lock();
set_back_interception(false);
assert!(!back_interception_enabled());
set_back_interception(true);
assert!(back_interception_enabled());
set_back_interception(false);
}
}