use std::{error::Error, fmt};
use super::{ChildId, FocusState};
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ModalState {
ids: Vec<ChildId>,
return_focus: Vec<FocusState>,
}
impl ModalState {
pub fn open(
&mut self,
id: impl Into<ChildId>,
focus: &mut FocusState,
) -> Result<(), ModalOpenError> {
let id = id.into();
if self.ids.last() == Some(&id) {
return Ok(());
}
if self.ids.contains(&id) {
return Err(ModalOpenError { id });
}
self.return_focus.push(focus.clone());
*focus = FocusState::intent([id.clone()]);
self.ids.push(id);
Ok(())
}
pub fn close(&mut self, focus: &mut FocusState) -> Option<ChildId> {
let id = self.ids.pop()?;
*focus = self
.return_focus
.pop()
.expect("modal ids and return-focus history stay aligned");
Some(id)
}
#[must_use]
pub fn is_open(&self, id: impl AsRef<str>) -> bool {
let id = id.as_ref();
self.ids.iter().any(|open| open.as_str() == id)
}
#[must_use]
pub fn top(&self) -> Option<&ChildId> {
self.ids.last()
}
#[must_use]
pub fn ids(&self) -> &[ChildId] {
&self.ids
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModalOpenError {
id: ChildId,
}
impl ModalOpenError {
#[must_use]
pub fn id(&self) -> &ChildId {
&self.id
}
}
impl fmt::Display for ModalOpenError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"modal `{}` is already open below the top of the stack",
self.id
)
}
}
impl Error for ModalOpenError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn open_and_close_restore_the_exact_focus_snapshot() {
let original = FocusState::intent([
ChildId::Static("pane"),
ChildId::Static("temporarily-absent"),
]);
let mut focus = original.clone();
let mut modals = ModalState::default();
modals
.open(ChildId::Static("dialog"), &mut focus)
.expect("open dialog");
assert_eq!(focus, FocusState::intent([ChildId::Static("dialog")]));
assert_eq!(modals.close(&mut focus), Some(ChildId::Static("dialog")));
assert_eq!(focus, original);
}
#[test]
fn nested_modals_restore_each_stack_edge() {
let base = FocusState::intent([ChildId::Static("base")]);
let lower = FocusState::intent([ChildId::Static("lower"), ChildId::Static("field")]);
let mut focus = base.clone();
let mut modals = ModalState::default();
modals.open("lower", &mut focus).expect("open lower");
focus = lower.clone();
modals.open("top", &mut focus).expect("open top");
assert_eq!(
modals.ids(),
[ChildId::Static("lower"), ChildId::Static("top")]
);
assert_eq!(modals.close(&mut focus), Some(ChildId::Static("top")));
assert_eq!(focus, lower);
assert_eq!(modals.close(&mut focus), Some(ChildId::Static("lower")));
assert_eq!(focus, base);
}
#[test]
fn same_top_is_idempotent_but_duplicate_nesting_is_rejected() {
let base = FocusState::intent([ChildId::Static("base")]);
let mut focus = base.clone();
let mut modals = ModalState::default();
modals.open("lower", &mut focus).expect("open lower");
focus = FocusState::intent([ChildId::Static("lower"), ChildId::Static("field")]);
modals.open("lower", &mut focus).expect("same top is valid");
assert_eq!(modals.ids(), [ChildId::Static("lower")]);
modals.open("top", &mut focus).expect("open top");
let error = modals
.open("lower", &mut focus)
.expect_err("duplicate nesting");
assert_eq!(error.id(), &ChildId::Static("lower"));
assert_eq!(
modals.ids(),
[ChildId::Static("lower"), ChildId::Static("top")]
);
let _ = modals.close(&mut focus);
let _ = modals.close(&mut focus);
assert_eq!(focus, base, "same-top open must not overwrite return focus");
}
}