use std::cell::RefCell;
use std::mem::ManuallyDrop;
use std::rc::Rc;
use reactive_core::{RwSignal, signal};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct DismissId(u64);
struct Entry {
id: DismissId,
dismiss: Rc<dyn Fn()>,
}
thread_local! {
static STACK: ManuallyDrop<RefCell<Vec<Entry>>> = ManuallyDrop::new(RefCell::new(Vec::new()));
static NEXT_ID: ManuallyDrop<RefCell<u64>> = ManuallyDrop::new(RefCell::new(0));
static DEPTH: ManuallyDrop<RwSignal<usize>> = ManuallyDrop::new(signal(0));
}
fn publish_depth() {
let depth = STACK.with(|s| s.borrow().len());
DEPTH.with(|d| d.set(depth));
}
pub fn register_dismiss(dismiss: Rc<dyn Fn()>) -> DismissId {
let id = NEXT_ID.with(|n| {
let mut n = n.borrow_mut();
*n += 1;
DismissId(*n)
});
STACK.with(|s| s.borrow_mut().push(Entry { id, dismiss }));
publish_depth();
id
}
pub fn unregister_dismiss(id: DismissId) {
STACK.with(|s| s.borrow_mut().retain(|e| e.id != id));
publish_depth();
}
pub fn dismiss_top() -> bool {
let Some(entry) = STACK.with(|s| s.borrow_mut().pop()) else {
return false;
};
publish_depth();
(entry.dismiss)();
true
}
pub fn use_dismiss_depth() -> usize {
DEPTH.with(|d| d.get())
}
pub fn dismiss_depth() -> usize {
STACK.with(|s| s.borrow().len())
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use super::*;
fn reset() {
STACK.with(|s| s.borrow_mut().clear());
}
#[test]
fn dismisses_the_most_recently_opened_first() {
reset();
let log = Rc::new(RefCell::new(Vec::new()));
for name in ["dialog", "drawer"] {
let log = log.clone();
register_dismiss(Rc::new(move || log.borrow_mut().push(name)));
}
assert_eq!(dismiss_depth(), 2);
assert!(dismiss_top());
assert_eq!(*log.borrow(), vec!["drawer"], "the last opened goes first");
assert!(dismiss_top());
assert_eq!(*log.borrow(), vec!["drawer", "dialog"]);
assert!(!dismiss_top(), "an empty stack reports nothing dismissed");
assert_eq!(dismiss_depth(), 0);
}
#[test]
fn withdrawing_out_of_order_skips_that_entry() {
reset();
let hit = Rc::new(Cell::new(0));
let first = {
let hit = hit.clone();
register_dismiss(Rc::new(move || hit.set(hit.get() + 1)))
};
let log = Rc::new(RefCell::new(Vec::new()));
{
let log = log.clone();
register_dismiss(Rc::new(move || log.borrow_mut().push("top")));
}
unregister_dismiss(first);
assert_eq!(dismiss_depth(), 1);
assert!(dismiss_top());
assert_eq!(*log.borrow(), vec!["top"]);
assert_eq!(hit.get(), 0, "the withdrawn entry is never invoked");
assert!(!dismiss_top());
}
#[test]
fn escape_dismisses_only_when_nothing_holds_focus() {
use platform_core::{Event, Key, ModifiersState, NamedKey};
reset();
crate::focus::clear();
let closed = Rc::new(Cell::new(false));
{
let closed = closed.clone();
register_dismiss(Rc::new(move || closed.set(true)));
}
let esc = Event::KeyPressed {
key: Key::Named(NamedKey::Escape),
modifiers: ModifiersState::default(),
};
let id = crate::focus::next_id();
crate::focus::register_as(id, crate::focus::FocusKind::Widget);
crate::focus::request(id);
assert_eq!(crate::dispatch_overlays(&esc), crate::EventResult::Ignored);
assert!(!closed.get(), "the focused field consumes the first Escape");
crate::focus::clear();
assert_eq!(crate::dispatch_overlays(&esc), crate::EventResult::Handled);
assert!(closed.get());
crate::focus::unregister(id);
}
#[test]
fn a_handler_that_reenters_the_stack_is_safe() {
reset();
let id = Rc::new(Cell::new(None::<DismissId>));
let inner = id.clone();
let handle = register_dismiss(Rc::new(move || {
if let Some(i) = inner.get() {
unregister_dismiss(i);
}
}));
id.set(Some(handle));
assert!(dismiss_top());
assert_eq!(dismiss_depth(), 0);
}
}