afrim_wish/
lib.rs

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