BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / dialogs.rs

dialogs.rs

dialogs.rs 4.4 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}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum DialogPoll {
19 Idle,
20 Pending,
21 Ready {
22 purpose: DialogPurpose,
23 path: Option<PathBuf>,
24 },
25}
26
27pub trait Dialogs {
28 fn start(&mut self, purpose: DialogPurpose);
29 fn poll(&mut self) -> DialogPoll;
30}
31
32/// Remembers one answer per request. Tests use this instead of a window.
33#[derive(Debug, Default)]
34pub struct ScriptedDialogs {
35 queued: Option<DialogPurpose>,
36 answers: Vec<Option<PathBuf>>,
37}
38
39impl ScriptedDialogs {
40 pub fn push(&mut self, path: Option<PathBuf>) {
41 self.answers.push(path);
42 }
43}
44
45impl Dialogs for ScriptedDialogs {
46 fn start(&mut self, purpose: DialogPurpose) {
47 self.queued = Some(purpose);
48 }
49
50 fn poll(&mut self) -> DialogPoll {
51 let Some(purpose) = self.queued.take() else {
52 return DialogPoll::Idle;
53 };
54 let path = if self.answers.is_empty() {
55 None
56 } else {
57 self.answers.remove(0)
58 };
59 DialogPoll::Ready { purpose, path }
60 }
61}
62
63pub struct SystemDialogs {
64 queued: Option<DialogPurpose>,
65 pending: Option<std::sync::mpsc::Receiver<(DialogPurpose, Option<PathBuf>)>>,
66}
67
68impl SystemDialogs {
69 pub fn new() -> Self {
70 Self {
71 queued: None,
72 pending: None,
73 }
74 }
75}
76
77impl Default for SystemDialogs {
78 fn default() -> Self {
79 Self::new()
80 }
81}
82
83impl Dialogs for SystemDialogs {
84 fn start(&mut self, purpose: DialogPurpose) {
85 if self.pending.is_none() && self.queued.is_none() {
86 self.queued = Some(purpose);
87 }
88 }
89
90 fn poll(&mut self) -> DialogPoll {
91 if let Some(purpose) = self.queued.take() {
92 let (tx, rx) = std::sync::mpsc::channel();
93 std::thread::spawn(move || {
94 let path = native_pick(&purpose);
95 let _ = tx.send((purpose, path));
96 });
97 self.pending = Some(rx);
98 }
99 if let Some(rx) = &self.pending {
100 match rx.try_recv() {
101 Ok((purpose, path)) => {
102 self.pending = None;
103 DialogPoll::Ready { purpose, path }
104 }
105 Err(std::sync::mpsc::TryRecvError::Empty) => DialogPoll::Pending,
106 Err(_) => {
107 self.pending = None;
108 DialogPoll::Idle
109 }
110 }
111 } else {
112 DialogPoll::Idle
113 }
114 }
115}
116
117fn native_pick(purpose: &DialogPurpose) -> Option<PathBuf> {
118 let dialog = rfd::FileDialog::new();
119 match purpose {
120 DialogPurpose::NewProject => dialog.set_title("Nieuwe projectmap").pick_folder(),
121 DialogPurpose::LoadProject => dialog
122 .set_title("Project laden")
123 .add_filter("Paperdoll-project", &["ppd"])
124 .pick_file(),
125 DialogPurpose::Body(_) => dialog
126 .set_title("Lichaamsmodel")
127 .add_filter("Model", &["glb", "gltf", "fbx", "obj"])
128 .pick_file(),
129 DialogPurpose::EquipmentModel(_) => dialog
130 .set_title("Equipmentmodel")
131 .add_filter("Model", &["glb", "gltf", "fbx", "obj"])
132 .pick_file(),
133 }
134}
135
136pub fn load_candidate(path: &Path) -> PathBuf {
137 if path.is_dir() {
138 path.join(crate::project::MANIFEST_NAME)
139 } else {
140 path.to_path_buf()
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn scripted_dialogs_return_the_queued_path_from_poll() {
150 let mut dialogs = ScriptedDialogs::default();
151 dialogs.push(Some(PathBuf::from("/tmp/project")));
152 assert!(matches!(dialogs.poll(), DialogPoll::Idle));
153 dialogs.start(DialogPurpose::NewProject);
154 match dialogs.poll() {
155 DialogPoll::Ready { purpose, path } => {
156 assert_eq!(purpose, DialogPurpose::NewProject);
157 assert_eq!(path, Some(PathBuf::from("/tmp/project")));
158 }
159 other => panic!("expected a ready dialog, got {other:?}"),
160 }
161 }
162}