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)
}
static ACTIVE_BACK_HANDLERS: AtomicUsize = AtomicUsize::new(0);
#[expect(non_snake_case)]
#[track_caller]
pub fn BackHandler(enabled: bool, mut on_back: impl FnMut() + 'static) {
let requests = cranpose_core::rememberEventStream(enabled, move |sender| {
if !enabled {
return None;
}
if ACTIVE_BACK_HANDLERS.fetch_add(1, Ordering::AcqRel) == 0 {
set_back_interception(true);
}
let registration = observe_back_requests(move || {
let count = take_back_requests();
if count > 0 {
sender.send(count);
}
});
Some(BackInterception {
_registration: registration,
})
});
if enabled {
cranpose_core::CollectEvents(requests, enabled, move |count: usize| {
for _ in 0..count {
on_back();
}
});
}
}
struct BackInterception {
_registration: BackRequestObserver,
}
impl Drop for BackInterception {
fn drop(&mut self) {
if ACTIVE_BACK_HANDLERS.fetch_sub(1, Ordering::AcqRel) == 1 {
set_back_interception(false);
}
}
}
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)]
#[path = "tests/navigation_tests.rs"]
mod tests;