BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / dialogs.rs

dialogs.rs

dialogs.rs 4.8 KiB Download Raw
1//! One place starts file dialogs and collects the result.
2//!
3//! GTK dialogs block on the UI thread, so `poll` runs them there. A backend
4//! that returns before the choice is ready reports [`DialogPoll::Pending`]
5//! and the editor keeps asking.
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.set_title("Nieuwe projectmap").pick_folder(),
123 DialogPurpose::LoadProject => dialog
124 .set_title("Project laden")
125 .add_filter("Paperdoll-project", &["ppd"])
126 .pick_file(),
127 DialogPurpose::Body(_) => dialog
128 .set_title("Lichaamsmodel")
129 .add_filter("Model", &["glb", "gltf", "fbx", "obj"])
130 .pick_file(),
131 DialogPurpose::EquipmentModel(_) => dialog
132 .set_title("Rigide equipment toevoegen")
133 .add_filter("Wavefront OBJ", &["obj"])
134 .pick_file(),
135 DialogPurpose::EquipmentShiny(_, _) => dialog
136 .set_title("Shinymap")
137 .add_filter("PNG", &["png"])
138 .pick_file(),
139 DialogPurpose::ImportEquipment => dialog
140 .set_title("Equipment-preset importeren")
141 .add_filter("Paperdoll equipment", &["ppe"])
142 .pick_file(),
143 }
144}
145
146pub fn load_candidate(path: &Path) -> PathBuf {
147 if path.is_dir() {
148 path.join(crate::project::MANIFEST_NAME)
149 } else {
150 path.to_path_buf()
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn scripted_dialogs_return_the_queued_path_from_poll() {
160 let mut dialogs = ScriptedDialogs::default();
161 dialogs.push(Some(PathBuf::from("/tmp/project")));
162 assert!(matches!(dialogs.poll(), DialogPoll::Idle));
163 dialogs.start(DialogPurpose::NewProject);
164 match dialogs.poll() {
165 DialogPoll::Ready { purpose, path } => {
166 assert_eq!(purpose, DialogPurpose::NewProject);
167 assert_eq!(path, Some(PathBuf::from("/tmp/project")));
168 }
169 other => panic!("expected a ready dialog, got {other:?}"),
170 }
171 }
172}