use std::{
cell::{Cell, RefCell},
rc::Rc,
};
use cranpose_core::{CompositionLocal, MutableState, compositionLocalOf, try_mutableStateOf};
struct ModalEntry {
id: u64,
on_back: Rc<dyn Fn()>,
}
thread_local! {
static MODALS: RefCell<Vec<ModalEntry>> = const { RefCell::new(Vec::new()) };
static NEXT_ID: Cell<u64> = const { Cell::new(1) };
static DEPTH_COUNT: Cell<usize> = const { Cell::new(0) };
static DEPTH: RefCell<Option<MutableState<usize>>> = const { RefCell::new(None) };
}
fn depth_state() -> Option<MutableState<usize>> {
DEPTH.with(|cell| {
let mut cell = cell.borrow_mut();
if cell.is_none() {
*cell = try_mutableStateOf(DEPTH_COUNT.with(Cell::get));
}
*cell
})
}
fn publish_depth() {
let depth = MODALS.with(|modals| modals.borrow().len());
DEPTH_COUNT.with(|count| count.set(depth));
if let Some(state) = DEPTH.with(|cell| *cell.borrow())
&& state.get() != depth
{
state.set(depth);
}
}
pub struct ModalRegistration {
id: u64,
depth: usize,
}
impl Drop for ModalRegistration {
fn drop(&mut self) {
MODALS.with(|modals| modals.borrow_mut().retain(|entry| entry.id != self.id));
publish_depth();
if crate::render_state::has_current_app_context() {
crate::text_field_focus::clear_focus_for_closed_modal(self.depth);
}
}
}
pub fn register_modal(on_back: Rc<dyn Fn()>) -> ModalRegistration {
let id = NEXT_ID.with(|next| {
let id = next.get();
next.set(id + 1);
id
});
let depth = MODALS.with(|modals| {
let mut modals = modals.borrow_mut();
modals.push(ModalEntry { id, on_back });
modals.len()
});
publish_depth();
ModalRegistration { id, depth }
}
pub fn modal_depth() -> usize {
match depth_state() {
Some(state) => state.get(),
None => DEPTH_COUNT.with(Cell::get),
}
}
pub(crate) fn current_modal_depth() -> usize {
DEPTH_COUNT.with(Cell::get)
}
pub fn local_modal_depth() -> CompositionLocal<usize> {
thread_local! {
static LOCAL: RefCell<Option<CompositionLocal<usize>>> = const { RefCell::new(None) };
}
LOCAL.with(|cell| {
cell.borrow_mut()
.get_or_insert_with(|| compositionLocalOf(|| 0usize))
.clone()
})
}
pub fn dispatch_modal_back() -> bool {
let innermost = MODALS.with(|modals| {
modals
.borrow()
.last()
.map(|entry| Rc::clone(&entry.on_back))
});
match innermost {
Some(on_back) => {
on_back();
true
}
None => false,
}
}
pub fn clear_modals() {
MODALS.with(|modals| modals.borrow_mut().clear());
publish_depth();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_innermost_modal_takes_the_back_request() {
clear_modals();
let outer = Rc::new(Cell::new(0u32));
let inner = Rc::new(Cell::new(0u32));
let outer_counter = Rc::clone(&outer);
let inner_counter = Rc::clone(&inner);
let _outer = register_modal(Rc::new(move || outer_counter.set(outer_counter.get() + 1)));
let inner_registration =
register_modal(Rc::new(move || inner_counter.set(inner_counter.get() + 1)));
assert!(dispatch_modal_back());
assert_eq!(inner.get(), 1);
assert_eq!(outer.get(), 0);
drop(inner_registration);
assert!(dispatch_modal_back());
assert_eq!(outer.get(), 1);
clear_modals();
}
#[test]
fn a_back_request_with_no_modal_open_is_not_taken() {
clear_modals();
assert!(!dispatch_modal_back());
}
#[test]
fn the_depth_counts_what_is_open_and_falls_back_to_zero() {
clear_modals();
assert_eq!(modal_depth(), 0);
let outer = register_modal(Rc::new(|| {}));
assert_eq!(modal_depth(), 1);
let inner = register_modal(Rc::new(|| {}));
assert_eq!(modal_depth(), 2);
drop(inner);
assert_eq!(modal_depth(), 1);
drop(outer);
assert_eq!(modal_depth(), 0);
}
#[test]
fn registrations_leave_the_stack_when_dropped() {
clear_modals();
{
let _first = register_modal(Rc::new(|| {}));
let _second = register_modal(Rc::new(|| {}));
assert_eq!(MODALS.with(|modals| modals.borrow().len()), 2);
}
assert_eq!(MODALS.with(|modals| modals.borrow().len()), 0);
assert!(!dispatch_modal_back());
}
}