BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / dialogs.rs

dialogs.rs

dialogs.rs 5.0 KiB Download Raw
1//! One place starts file dialogs and collects the result.
2//!
3//! The native picker runs on a dedicated request thread; `poll` never waits.
4//! [`DialogPoll::Pending`] keeps the editor repainting until an answer arrives.
5//! Scripted backends exercise the same request/result boundary in tests.
6
7use std::path::{Path, PathBuf};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum DialogPurpose {
11 NewProject,
12 LoadProject,
13 Body(crate::session::Sex),
14 EquipmentModel(usize),
15 EquipmentShiny(usize, usize),
16 ImportEquipment,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum DialogPoll {
21 Idle,
22 Pending,
23 Ready {
24 purpose: DialogPurpose,
25 path: Option<PathBuf>,
26 },
27}
28
29pub trait Dialogs {
30 fn start(&mut self, purpose: DialogPurpose);
31 fn poll(&mut self) -> DialogPoll;
32}
33
34/// Remembers one answer per request. Tests use this instead of a window.
35#[derive(Debug, Default)]
36pub struct ScriptedDialogs {
37 queued: Option<DialogPurpose>,
38 answers: Vec<Option<PathBuf>>,
39}
40
41impl ScriptedDialogs {
42 pub fn push(&mut self, path: Option<PathBuf>) {
43 self.answers.push(path);
44 }
45}
46
47impl Dialogs for ScriptedDialogs {
48 fn start(&mut self, purpose: DialogPurpose) {
49 self.queued = Some(purpose);
50 }
51
52 fn poll(&mut self) -> DialogPoll {
53 let Some(purpose) = self.queued.take() else {
54 return DialogPoll::Idle;
55 };
56 let path = if self.answers.is_empty() {
57 None
58 } else {
59 self.answers.remove(0)
60 };
61 DialogPoll::Ready { purpose, path }
62 }
63}
64
65pub struct SystemDialogs {
66 queued: Option<DialogPurpose>,
67 pending: Option<std::sync::mpsc::Receiver<(DialogPurpose, Option<PathBuf>)>>,
68}
69
70impl SystemDialogs {
71 pub fn new() -> Self {
72 Self {
73 queued: None,
74 pending: None,
75 }
76 }
77}
78
79impl Default for SystemDialogs {
80 fn default() -> Self {
81 Self::new()
82 }
83}
84
85impl Dialogs for SystemDialogs {
86 fn start(&mut self, purpose: DialogPurpose) {
87 if self.pending.is_none() && self.queued.is_none() {
88 self.queued = Some(purpose);
89 }
90 }
91
92 fn poll(&mut self) -> DialogPoll {
93 if let Some(purpose) = self.queued.take() {
94 let (tx, rx) = std::sync::mpsc::channel();
95 std::thread::spawn(move || {
96 let path = native_pick(&purpose);
97 let _ = tx.send((purpose, path));
98 });
99 self.pending = Some(rx);
100 }
101 if let Some(rx) = &self.pending {
102 match rx.try_recv() {
103 Ok((purpose, path)) => {
104 self.pending = None;
105 DialogPoll::Ready { purpose, path }
106 }
107 Err(std::sync::mpsc::TryRecvError::Empty) => DialogPoll::Pending,
108 Err(_) => {
109 self.pending = None;
110 DialogPoll::Idle
111 }
112 }
113 } else {
114 DialogPoll::Idle
115 }
116 }
117}
118
119fn native_pick(purpose: &DialogPurpose) -> Option<PathBuf> {
120 let dialog = rfd::FileDialog::new();
121 match purpose {
122 DialogPurpose::NewProject => dialog
123 .set_title("Map voor het nieuwe project")
124 .pick_folder(),
125 DialogPurpose::LoadProject => dialog
126 .set_title("Project laden")
127 .add_filter("Paperdoll-project", &["ppd"])
128 .pick_file(),
129 DialogPurpose::Body(_) => dialog
130 .set_title("Lichaamsmodel")
131 .add_filter("Model", &["glb", "gltf", "fbx", "obj"])
132 .pick_file(),
133 DialogPurpose::EquipmentModel(_) => dialog
134 .set_title("Rigide equipment toevoegen")
135 .add_filter("Rigide model", &["obj", "fbx"])
136 .add_filter("Wavefront OBJ", &["obj"])
137 .add_filter("FBX", &["fbx"])
138 .pick_file(),
139 DialogPurpose::EquipmentShiny(_, _) => dialog
140 .set_title("Shinymap")
141 .add_filter("PNG", &["png"])
142 .pick_file(),
143 DialogPurpose::ImportEquipment => dialog
144 .set_title("Equipment-preset importeren")
145 .add_filter("Paperdoll equipment", &["ppe"])
146 .pick_file(),
147 }
148}
149
150pub fn load_candidate(path: &Path) -> PathBuf {
151 if path.is_dir() {
152 path.join(crate::project::MANIFEST_NAME)
153 } else {
154 path.to_path_buf()
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn scripted_dialogs_return_the_queued_path_from_poll() {
164 let mut dialogs = ScriptedDialogs::default();
165 dialogs.push(Some(PathBuf::from("/tmp/project")));
166 assert!(matches!(dialogs.poll(), DialogPoll::Idle));
167 dialogs.start(DialogPurpose::NewProject);
168 match dialogs.poll() {
169 DialogPoll::Ready { purpose, path } => {
170 assert_eq!(purpose, DialogPurpose::NewProject);
171 assert_eq!(path, Some(PathBuf::from("/tmp/project")));
172 }
173 other => panic!("expected a ready dialog, got {other:?}"),
174 }
175 }
176}