use std::collections::HashMap;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct DialogId(pub u64);
impl std::fmt::Display for DialogId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Dialog({})", self.0)
}
}
#[derive(Clone, Debug)]
pub enum DialogKind {
Alert {
title: String,
message: String,
},
Confirm {
title: String,
message: String,
},
Prompt {
title: String,
message: String,
default_text: Option<String>,
},
FileOpen {
title: String,
filters: Vec<(String, String)>,
multiple: bool,
},
FileSave {
title: String,
default_name: Option<String>,
filters: Vec<(String, String)>,
},
}
impl DialogKind {
pub fn kind_label(&self) -> &'static str {
match self {
DialogKind::Alert { .. } => "Alert",
DialogKind::Confirm { .. } => "Confirm",
DialogKind::Prompt { .. } => "Prompt",
DialogKind::FileOpen { .. } => "FileOpen",
DialogKind::FileSave { .. } => "FileSave",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DialogResponse {
Dismissed,
Confirmed,
Cancelled,
Text(String),
FilePaths(Vec<String>),
SavePath(String),
}
#[derive(Clone, Debug)]
pub struct DialogRequest {
pub id: DialogId,
pub kind: DialogKind,
}
pub struct DialogQueue {
next_id: u64,
pending: std::collections::VecDeque<DialogRequest>,
responses: HashMap<DialogId, DialogResponse>,
}
impl DialogQueue {
pub fn new() -> Self {
Self {
next_id: 1,
pending: std::collections::VecDeque::new(),
responses: HashMap::new(),
}
}
pub fn request(&mut self, kind: DialogKind) -> DialogId {
let id = DialogId(self.next_id);
self.next_id += 1;
self.pending.push_back(DialogRequest { id, kind });
id
}
pub fn pop_pending(&mut self) -> Option<DialogRequest> {
self.pending.pop_front()
}
pub fn respond(&mut self, id: DialogId, response: DialogResponse) {
self.responses.insert(id, response);
}
pub fn pop_response(&mut self, id: DialogId) -> Option<DialogResponse> {
self.responses.remove(&id)
}
pub fn peek_response(&self, id: DialogId) -> Option<&DialogResponse> {
self.responses.get(&id)
}
pub fn is_pending_empty(&self) -> bool {
self.pending.is_empty()
}
pub fn pending_count(&self) -> usize {
self.pending.len()
}
pub fn is_responses_empty(&self) -> bool {
self.responses.is_empty()
}
}
impl Default for DialogQueue {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_returns_unique_ids() {
let mut q = DialogQueue::new();
let id1 = q.request(DialogKind::Alert {
title: "a".into(),
message: "m".into(),
});
let id2 = q.request(DialogKind::Alert {
title: "b".into(),
message: "n".into(),
});
assert_ne!(id1, id2);
}
#[test]
fn pop_pending_returns_fifo_order() {
let mut q = DialogQueue::new();
let id1 = q.request(DialogKind::Alert {
title: "first".into(),
message: "".into(),
});
let id2 = q.request(DialogKind::Alert {
title: "second".into(),
message: "".into(),
});
let first = q.pop_pending().unwrap();
assert_eq!(first.id, id1);
let second = q.pop_pending().unwrap();
assert_eq!(second.id, id2);
assert!(q.pop_pending().is_none());
}
#[test]
fn respond_and_pop_response() {
let mut q = DialogQueue::new();
let id = q.request(DialogKind::Confirm {
title: "Exit?".into(),
message: "Sure?".into(),
});
q.respond(id, DialogResponse::Confirmed);
assert_eq!(q.pop_response(id), Some(DialogResponse::Confirmed));
assert_eq!(q.pop_response(id), None);
}
#[test]
fn peek_response_does_not_consume() {
let mut q = DialogQueue::new();
let id = q.request(DialogKind::Alert {
title: "t".into(),
message: "m".into(),
});
q.respond(id, DialogResponse::Dismissed);
assert!(q.peek_response(id).is_some());
assert!(q.peek_response(id).is_some()); q.pop_response(id);
assert!(q.peek_response(id).is_none());
}
#[test]
fn prompt_dialog_text_response() {
let mut q = DialogQueue::new();
let id = q.request(DialogKind::Prompt {
title: "Name".into(),
message: "Enter your name:".into(),
default_text: Some("World".into()),
});
q.respond(id, DialogResponse::Text("Alice".into()));
assert_eq!(
q.pop_response(id),
Some(DialogResponse::Text("Alice".into()))
);
}
#[test]
fn file_open_dialog_paths_response() {
let mut q = DialogQueue::new();
let id = q.request(DialogKind::FileOpen {
title: "Open".into(),
filters: vec![("Rust".into(), "*.rs".into())],
multiple: false,
});
q.respond(id, DialogResponse::FilePaths(vec!["/tmp/foo.rs".into()]));
if let Some(DialogResponse::FilePaths(paths)) = q.pop_response(id) {
assert_eq!(paths, vec!["/tmp/foo.rs"]);
} else {
panic!("expected FilePaths response");
}
}
#[test]
fn pending_count_tracks_enqueue() {
let mut q = DialogQueue::new();
assert_eq!(q.pending_count(), 0);
q.request(DialogKind::Alert {
title: "t".into(),
message: "m".into(),
});
assert_eq!(q.pending_count(), 1);
q.pop_pending();
assert_eq!(q.pending_count(), 0);
}
#[test]
fn kind_label_returns_correct_string() {
assert_eq!(
DialogKind::Alert {
title: "".into(),
message: "".into()
}
.kind_label(),
"Alert"
);
assert_eq!(
DialogKind::Confirm {
title: "".into(),
message: "".into()
}
.kind_label(),
"Confirm"
);
assert_eq!(
DialogKind::FileOpen {
title: "".into(),
filters: vec![],
multiple: false,
}
.kind_label(),
"FileOpen"
);
}
#[test]
fn dialog_id_display() {
let id = DialogId(7);
assert_eq!(format!("{id}"), "Dialog(7)");
}
}