use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OverlayKind {
Help,
Confirm,
Alert,
Custom(String),
}
impl std::fmt::Display for OverlayKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Help => write!(f, "Help"),
Self::Confirm => write!(f, "Confirm"),
Self::Alert => write!(f, "Alert"),
Self::Custom(name) => write!(f, "{name}"),
}
}
}
#[derive(Debug, Clone)]
pub struct OverlayRequest {
pub kind: OverlayKind,
pub title: String,
pub body: Option<String>,
pub actions: Vec<String>,
}
impl OverlayRequest {
#[must_use]
pub fn new(kind: OverlayKind, title: impl Into<String>) -> Self {
Self {
kind,
title: title.into(),
body: None,
actions: Vec::new(),
}
}
#[must_use]
pub fn with_body(mut self, body: impl Into<String>) -> Self {
self.body = Some(body.into());
self
}
#[must_use]
pub fn with_actions(mut self, actions: Vec<String>) -> Self {
self.actions = actions;
self
}
}
pub struct OverlayManager {
stack: Vec<OverlayRequest>,
}
impl OverlayManager {
#[must_use]
pub const fn new() -> Self {
Self { stack: Vec::new() }
}
pub fn push(&mut self, request: OverlayRequest) {
self.stack.push(request);
}
pub fn dismiss(&mut self) -> Option<OverlayRequest> {
self.stack.pop()
}
pub fn dismiss_all(&mut self) {
self.stack.clear();
}
#[must_use]
pub fn top(&self) -> Option<&OverlayRequest> {
self.stack.last()
}
#[must_use]
pub const fn has_active(&self) -> bool {
!self.stack.is_empty()
}
#[must_use]
pub const fn depth(&self) -> usize {
self.stack.len()
}
}
impl Default for OverlayManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn overlay_manager_empty() {
let mgr = OverlayManager::new();
assert!(!mgr.has_active());
assert_eq!(mgr.depth(), 0);
assert!(mgr.top().is_none());
}
#[test]
fn overlay_push_and_dismiss() {
let mut mgr = OverlayManager::new();
mgr.push(OverlayRequest::new(OverlayKind::Help, "Help"));
assert!(mgr.has_active());
assert_eq!(mgr.depth(), 1);
assert_eq!(mgr.top().unwrap().title, "Help");
let dismissed = mgr.dismiss().unwrap();
assert_eq!(dismissed.title, "Help");
assert!(!mgr.has_active());
}
#[test]
fn overlay_stack_ordering() {
let mut mgr = OverlayManager::new();
mgr.push(OverlayRequest::new(OverlayKind::Help, "Help"));
mgr.push(OverlayRequest::new(OverlayKind::Confirm, "Confirm"));
mgr.push(OverlayRequest::new(OverlayKind::Alert, "Alert"));
assert_eq!(mgr.depth(), 3);
assert_eq!(mgr.top().unwrap().title, "Alert");
mgr.dismiss();
assert_eq!(mgr.top().unwrap().title, "Confirm");
mgr.dismiss();
assert_eq!(mgr.top().unwrap().title, "Help");
}
#[test]
fn overlay_dismiss_all() {
let mut mgr = OverlayManager::new();
mgr.push(OverlayRequest::new(OverlayKind::Help, "H"));
mgr.push(OverlayRequest::new(OverlayKind::Alert, "A"));
mgr.dismiss_all();
assert!(!mgr.has_active());
assert_eq!(mgr.depth(), 0);
}
#[test]
fn overlay_request_builder() {
let req = OverlayRequest::new(OverlayKind::Confirm, "Delete?")
.with_body("This action cannot be undone.")
.with_actions(vec!["Cancel".to_string(), "Delete".to_string()]);
assert_eq!(req.kind, OverlayKind::Confirm);
assert_eq!(req.body.as_deref(), Some("This action cannot be undone."));
assert_eq!(req.actions.len(), 2);
}
#[test]
fn overlay_kind_display() {
assert_eq!(OverlayKind::Help.to_string(), "Help");
assert_eq!(OverlayKind::Custom("Foo".into()).to_string(), "Foo");
}
#[test]
fn overlay_kind_serde_roundtrip() {
for kind in [
OverlayKind::Help,
OverlayKind::Confirm,
OverlayKind::Alert,
OverlayKind::Custom("test".into()),
] {
let json = serde_json::to_string(&kind).unwrap();
let decoded: OverlayKind = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, kind);
}
}
}