|
1 | 1 | mod config;
|
2 | 2 | mod window;
|
3 | 3 |
|
4 |
| -use afrim::frontend::Frontend; |
| 4 | +use afrim::frontend::{Command, Frontend}; |
| 5 | +use anyhow::{anyhow, Result}; |
5 | 6 | use rstk::*;
|
| 7 | +use std::sync::{ |
| 8 | + mpsc::{Receiver, Sender}, |
| 9 | + OnceLock, |
| 10 | +}; |
| 11 | +use std::thread; |
6 | 12 | use window::{rstk_ext::init_rstk_ext, toolkit::ToolKit, tooltip::ToolTip};
|
7 | 13 |
|
8 | 14 | pub use config::Config;
|
9 | 15 |
|
10 |
| -#[derive(Clone)] |
11 | 16 | pub struct Wish {
|
12 |
| - window: rstk::TkTopLevel, |
| 17 | + window: &'static rstk::TkTopLevel, |
13 | 18 | tooltip: ToolTip,
|
14 | 19 | toolkit: ToolKit,
|
| 20 | + tx: Option<Sender<Command>>, |
| 21 | + rx: Option<Receiver<Command>>, |
15 | 22 | }
|
16 | 23 |
|
17 | 24 | impl Wish {
|
18 |
| - pub fn init(config: config::Config) -> Self { |
19 |
| - let wish = if cfg!(debug_assertions) { |
20 |
| - rstk::trace_with("wish").unwrap() |
21 |
| - } else { |
22 |
| - rstk::start_wish().unwrap() |
23 |
| - }; |
| 25 | + fn init() -> &'static rstk::TkTopLevel { |
| 26 | + static WISH: OnceLock<rstk::TkTopLevel> = OnceLock::new(); |
| 27 | + WISH.get_or_init(|| { |
| 28 | + let wish = if cfg!(debug_assertions) { |
| 29 | + rstk::trace_with("wish").unwrap() |
| 30 | + } else { |
| 31 | + rstk::start_wish().unwrap() |
| 32 | + }; |
| 33 | + |
| 34 | + // The default behavior is to close the window. |
| 35 | + // But since this window, represent the main window, |
| 36 | + // we don't want an unexpected behavior. |
| 37 | + // It's better for us to manage the close. |
| 38 | + // |
| 39 | + // Note that, this close button is on the title bar. |
| 40 | + wish.on_close(Self::kill); |
24 | 41 |
|
25 |
| - init_rstk_ext(); |
| 42 | + init_rstk_ext(); |
| 43 | + |
| 44 | + wish |
| 45 | + }) |
| 46 | + } |
26 | 47 |
|
| 48 | + pub fn from_config(config: config::Config) -> Self { |
| 49 | + let wish = Self::init(); |
27 | 50 | let tooltip = ToolTip::new(config.theme.to_owned().unwrap_or_default());
|
28 | 51 | let toolkit = ToolKit::new(config.to_owned());
|
29 | 52 |
|
30 | 53 | Wish {
|
31 | 54 | window: wish,
|
32 | 55 | tooltip,
|
33 | 56 | toolkit,
|
| 57 | + tx: None, |
| 58 | + rx: None, |
34 | 59 | }
|
35 | 60 | }
|
36 | 61 |
|
37 |
| - pub fn raise_error(&self, message: &str, detail: &str) { |
| 62 | + pub fn raise_error<T: std::fmt::Debug>(message: &str, detail: T) { |
38 | 63 | rstk::message_box()
|
39 |
| - .parent(&self.window) |
| 64 | + .parent(Self::init()) |
40 | 65 | .icon(IconImage::Error)
|
41 | 66 | .title("Unexpected Error")
|
42 | 67 | .message(message)
|
43 |
| - .detail(detail) |
| 68 | + .detail(&format!("{detail:?}")) |
44 | 69 | .show();
|
45 |
| - rstk::end_wish(); |
| 70 | + Self::kill(); |
46 | 71 | }
|
47 | 72 |
|
48 |
| - pub fn build(&mut self) { |
49 |
| - self.tooltip.build(rstk::make_toplevel(&self.window)); |
| 73 | + fn build(&mut self) { |
| 74 | + self.tooltip.build(rstk::make_toplevel(self.window)); |
50 | 75 | self.toolkit.build(self.window.to_owned());
|
51 | 76 | }
|
52 | 77 |
|
53 |
| - pub fn listen(&self) { |
54 |
| - rstk::mainloop(); |
55 |
| - } |
56 |
| - |
57 |
| - pub fn destroy(&self) { |
| 78 | + /// End the process (wish and rust). |
| 79 | + /// |
| 80 | + /// Note that a `process::exit` is called internally. |
| 81 | + pub fn kill() { |
58 | 82 | rstk::end_wish();
|
59 | 83 | }
|
60 | 84 | }
|
61 | 85 |
|
62 | 86 | impl Frontend for Wish {
|
63 |
| - fn update_screen(&mut self, screen: (u64, u64)) { |
64 |
| - self.tooltip.update_screen(screen); |
65 |
| - } |
66 |
| - |
67 |
| - fn update_position(&mut self, position: (f64, f64)) { |
68 |
| - self.tooltip.update_position(position); |
69 |
| - } |
70 |
| - |
71 |
| - fn set_input(&mut self, text: &str) { |
72 |
| - self.tooltip.set_input_text(text); |
73 |
| - } |
74 |
| - |
75 |
| - fn set_page_size(&mut self, size: usize) { |
76 |
| - self.tooltip.set_page_size(size); |
77 |
| - } |
| 87 | + fn init(&mut self, tx: Sender<Command>, rx: Receiver<Command>) -> Result<()> { |
| 88 | + self.tx = Some(tx); |
| 89 | + self.rx = Some(rx); |
| 90 | + self.build(); |
78 | 91 |
|
79 |
| - fn add_predicate(&mut self, code: &str, remaining_code: &str, text: &str) { |
80 |
| - self.tooltip.add_predicate(code, remaining_code, text); |
| 92 | + Ok(()) |
81 | 93 | }
|
| 94 | + fn listen(&mut self) -> Result<()> { |
| 95 | + if self.tx.as_ref().and(self.rx.as_ref()).is_none() { |
| 96 | + return Err(anyhow!("you should config the channel first!")); |
| 97 | + } |
82 | 98 |
|
83 |
| - fn clear_predicates(&mut self) { |
84 |
| - self.tooltip.clear(); |
85 |
| - } |
| 99 | + // We shouldn't forget to listen for GUI events. |
| 100 | + thread::spawn(rstk::mainloop); |
86 | 101 |
|
87 |
| - fn previous_predicate(&mut self) { |
88 |
| - self.tooltip.select_previous_predicate(); |
89 |
| - } |
| 102 | + let tx = self.tx.as_ref().unwrap(); |
90 | 103 |
|
91 |
| - fn next_predicate(&mut self) { |
92 |
| - self.tooltip.select_next_predicate(); |
93 |
| - } |
94 |
| - |
95 |
| - fn get_selected_predicate(&self) -> Option<&(String, String, String)> { |
96 |
| - self.tooltip.get_selected_predicate() |
97 |
| - } |
| 104 | + loop { |
| 105 | + let command = self.rx.as_ref().unwrap().recv()?; |
| 106 | + match command { |
| 107 | + Command::ScreenSize(screen) => self.tooltip.update_screen(screen), |
| 108 | + Command::Position(position) => self.tooltip.update_position(position), |
| 109 | + Command::InputText(input) => self.tooltip.set_input_text(input), |
| 110 | + Command::PageSize(size) => self.tooltip.set_page_size(size), |
| 111 | + // TODO: implement the pause/resume. |
| 112 | + Command::State(_state) => {} |
| 113 | + Command::Predicate(predicate) => self.tooltip.add_predicate(predicate), |
| 114 | + Command::Update => self.tooltip.update(), |
| 115 | + Command::Clear => self.tooltip.clear(), |
| 116 | + Command::SelectPreviousPredicate => self.tooltip.select_previous_predicate(), |
| 117 | + Command::SelectNextPredicate => self.tooltip.select_next_predicate(), |
| 118 | + Command::SelectedPredicate => { |
| 119 | + if let Some(predicate) = self.tooltip.get_selected_predicate() { |
| 120 | + tx.send(Command::Predicate(predicate.to_owned()))?; |
| 121 | + } else { |
| 122 | + tx.send(Command::NoPredicate)?; |
| 123 | + } |
| 124 | + } |
| 125 | + // TODO: complete the implementation |
| 126 | + // to send GUI commands such as pause/resume. |
| 127 | + Command::NOP => tx.send(Command::NOP)?, |
| 128 | + Command::End => { |
| 129 | + tx.send(Command::End)?; |
| 130 | + self.window.destroy(); |
98 | 131 |
|
99 |
| - fn display(&self) { |
100 |
| - self.tooltip.update(); |
| 132 | + return Ok(()); |
| 133 | + } |
| 134 | + _ => (), |
| 135 | + } |
| 136 | + } |
101 | 137 | }
|
102 | 138 | }
|
103 | 139 |
|
104 | 140 | #[cfg(test)]
|
105 | 141 | mod tests {
|
106 | 142 | use crate::{Config, Wish};
|
107 |
| - use afrim::frontend::Frontend; |
| 143 | + use afrim::frontend::{Command, Frontend, Predicate}; |
108 | 144 | use std::path::Path;
|
| 145 | + use std::sync::mpsc; |
109 | 146 | use std::thread;
|
110 | 147 | use std::time::Duration;
|
111 | 148 |
|
112 | 149 | #[test]
|
113 | 150 | fn test_api() {
|
114 | 151 | let config = Config::from_file(Path::new("data/full_sample.toml")).unwrap();
|
115 |
| - let mut afrim_wish = Wish::init(config); |
116 |
| - afrim_wish.build(); |
| 152 | + let mut afrim_wish = Wish::from_config(config); |
| 153 | + assert!(afrim_wish.listen().is_err()); |
| 154 | + let (tx1, rx1) = mpsc::channel(); |
| 155 | + let (tx2, rx2) = mpsc::channel(); |
| 156 | + |
| 157 | + let afrim_wish_thread = thread::spawn(move || { |
| 158 | + afrim_wish.init(tx2, rx1).unwrap(); |
| 159 | + afrim_wish.listen().unwrap(); |
| 160 | + }); |
| 161 | + |
| 162 | + tx1.send(Command::NOP).unwrap(); |
| 163 | + assert_eq!(rx2.recv().unwrap(), Command::NOP); |
117 | 164 |
|
118 | 165 | // Test without data.
|
119 |
| - afrim_wish.clear_predicates(); |
120 |
| - afrim_wish.next_predicate(); |
121 |
| - afrim_wish.previous_predicate(); |
122 |
| - assert!(afrim_wish.get_selected_predicate().is_none()); |
123 |
| - afrim_wish.display(); |
| 166 | + tx1.send(Command::ScreenSize((480, 320))).unwrap(); |
| 167 | + tx1.send(Command::Clear).unwrap(); |
| 168 | + tx1.send(Command::SelectNextPredicate).unwrap(); |
| 169 | + tx1.send(Command::SelectPreviousPredicate).unwrap(); |
| 170 | + tx1.send(Command::SelectedPredicate).unwrap(); |
| 171 | + assert_eq!(rx2.recv().unwrap(), Command::NoPredicate); |
| 172 | + tx1.send(Command::Update).unwrap(); |
124 | 173 |
|
125 | 174 | // Test the adding of predicates.
|
126 |
| - afrim_wish.set_page_size(3); |
127 |
| - afrim_wish.set_input("Test started!"); |
128 |
| - afrim_wish.add_predicate("test", "123", "ok"); |
129 |
| - afrim_wish.add_predicate("test1", "23", "ok"); |
130 |
| - afrim_wish.add_predicate("test12", "1", "ok"); |
131 |
| - afrim_wish.add_predicate("test123", "", "ok"); |
132 |
| - afrim_wish.add_predicate("test1234", "", ""); |
133 |
| - afrim_wish.display(); |
| 175 | + tx1.send(Command::PageSize(3)).unwrap(); |
| 176 | + tx1.send(Command::InputText("Test started!".to_owned())) |
| 177 | + .unwrap(); |
| 178 | + tx1.send(Command::Predicate(Predicate { |
| 179 | + code: "test".to_owned(), |
| 180 | + remaining_code: "123".to_owned(), |
| 181 | + texts: vec!["ok".to_owned()], |
| 182 | + can_commit: false, |
| 183 | + })) |
| 184 | + .unwrap(); |
| 185 | + tx1.send(Command::Predicate(Predicate { |
| 186 | + code: "test1".to_owned(), |
| 187 | + remaining_code: "23".to_owned(), |
| 188 | + texts: vec!["ok".to_owned()], |
| 189 | + can_commit: false, |
| 190 | + })) |
| 191 | + .unwrap(); |
| 192 | + tx1.send(Command::Predicate(Predicate { |
| 193 | + code: "test12".to_owned(), |
| 194 | + remaining_code: "3".to_owned(), |
| 195 | + texts: vec!["ok".to_owned()], |
| 196 | + can_commit: false, |
| 197 | + })) |
| 198 | + .unwrap(); |
| 199 | + tx1.send(Command::Predicate(Predicate { |
| 200 | + code: "test123".to_owned(), |
| 201 | + remaining_code: "".to_owned(), |
| 202 | + texts: vec!["ok".to_owned()], |
| 203 | + can_commit: false, |
| 204 | + })) |
| 205 | + .unwrap(); |
| 206 | + tx1.send(Command::Predicate(Predicate { |
| 207 | + code: "test1234".to_owned(), |
| 208 | + remaining_code: "".to_owned(), |
| 209 | + texts: vec!["".to_owned()], |
| 210 | + can_commit: false, |
| 211 | + })) |
| 212 | + .unwrap(); |
| 213 | + tx1.send(Command::Update).unwrap(); |
134 | 214 |
|
135 | 215 | // Test the geometry.
|
136 | 216 | (0..100).for_each(|i| {
|
137 | 217 | if i % 10 != 0 {
|
138 | 218 | return;
|
139 | 219 | };
|
140 | 220 | let i = i as f64;
|
141 |
| - afrim_wish.update_position((i, i)); |
| 221 | + tx1.send(Command::Position((i, i))).unwrap(); |
142 | 222 | thread::sleep(Duration::from_millis(100));
|
143 | 223 | });
|
144 | 224 |
|
145 | 225 | // Test the navigation.
|
146 |
| - afrim_wish.previous_predicate(); |
| 226 | + tx1.send(Command::SelectPreviousPredicate).unwrap(); |
| 227 | + tx1.send(Command::SelectedPredicate).unwrap(); |
147 | 228 | assert_eq!(
|
148 |
| - afrim_wish.get_selected_predicate(), |
149 |
| - Some(&("test1234".to_owned(), "".to_owned(), "".to_owned())) |
| 229 | + rx2.recv().unwrap(), |
| 230 | + Command::Predicate(Predicate { |
| 231 | + code: "test123".to_owned(), |
| 232 | + remaining_code: "".to_owned(), |
| 233 | + texts: vec!["ok".to_owned()], |
| 234 | + can_commit: false, |
| 235 | + }) |
150 | 236 | );
|
151 |
| - afrim_wish.next_predicate(); |
| 237 | + tx1.send(Command::SelectNextPredicate).unwrap(); |
| 238 | + tx1.send(Command::SelectedPredicate).unwrap(); |
152 | 239 | assert_eq!(
|
153 |
| - afrim_wish.get_selected_predicate(), |
154 |
| - Some(&("test".to_owned(), "123".to_owned(), "ok".to_owned())) |
| 240 | + rx2.recv().unwrap(), |
| 241 | + Command::Predicate(Predicate { |
| 242 | + code: "test".to_owned(), |
| 243 | + remaining_code: "123".to_owned(), |
| 244 | + texts: vec!["ok".to_owned()], |
| 245 | + can_commit: false, |
| 246 | + }) |
155 | 247 | );
|
156 |
| - afrim_wish.display(); |
157 |
| - afrim_wish.destroy(); |
| 248 | + tx1.send(Command::Update).unwrap(); |
| 249 | + |
| 250 | + // We end the communication. |
| 251 | + tx1.send(Command::End).unwrap(); |
| 252 | + assert_eq!(rx2.recv().unwrap(), Command::End); |
| 253 | + assert!(rx2.recv().is_err()); |
| 254 | + |
| 255 | + // We wait the afrim to end properly. |
| 256 | + afrim_wish_thread.join().unwrap(); |
158 | 257 | }
|
159 | 258 | }
|
0 commit comments