//! File dialogs shown off the UI thread (Linux, #592): what asked runs again with the answer. use std::cell::RefCell; use std::rc::Rc; use std::sync::mpsc::{Sender, channel}; use serde_json::json; use vectorcraft_engine::Session; use crate::picks::{self, PickRequest}; use crate::state::Dialog; use crate::{FilePick, Services, VectorcraftApp}; const SVG: &[u8] = br#""#; /// A dialog asked for, and where its answer goes. type Asked = (PickRequest, Sender>); /// The dialogs showing. #[derive(Clone, Default)] struct Shown(Rc>>); impl Shown { fn requests(&self) -> Vec { self.0.borrow().iter().map(|(r, _)| r.clone()).collect() } /// The last dialog shown answers `paths`. fn answer(&self, paths: &[&str]) { let (_, tx) = self.0.borrow_mut().pop().expect("a dialog is showing"); tx.send(paths.iter().map(|p| p.to_string()).collect()).unwrap(); } } /// A desktop app whose dialogs show off the UI thread (none in line), reading [`SVG`] from any path /// and writing files to `written`. fn app() -> (VectorcraftApp, Shown, Rc>>) { let (shown, written) = (Shown::default(), Rc::new(RefCell::new(vec![]))); let (s, w) = (shown.clone(), written.clone()); let in_line = |_: &FilePick| -> Option { panic!("no dialog on the UI thread") }; let services = Services { pick_open: Some(Box::new(in_line)), pick_save: Some(Box::new(in_line)), pick_folder: Some(Box::new(|| panic!("no dialog on the UI thread"))), read: Some(Box::new(|_: &str| Ok(SVG.to_vec()))), write: Some(Box::new(move |path: &str, _: &[u8]| { w.borrow_mut().push(path.to_string()); Ok(()) })), start_pick: Some(Box::new(move |request: PickRequest| { let (tx, rx) = channel(); s.0.borrow_mut().push((request, tx)); Some(rx) })), ..Default::default() }; (VectorcraftApp::new(Session::new(), services), shown, written) } fn poll(app: &mut VectorcraftApp) { picks::poll(app, &egui::Context::default()); } #[test] fn file_open_opens_what_its_dialog_picks_when_it_answers() { let (mut app, shown, _) = app(); // The dialog shows; File › Open gives up for now, without a "cancelled". assert!(app.run("file.open", json!({})).is_err()); assert!(matches!(shown.requests().as_slice(), [PickRequest::Open(_)])); poll(&mut app); assert!(app.session.documents().is_empty() && app.ui.status.is_empty(), "{}", app.ui.status); // Another dialog waits for this one. assert!(app.run("file.open", json!({})).is_err()); assert_eq!(shown.requests().len(), 1); assert!(app.ui.status.contains("file dialog is open"), "{}", app.ui.status); shown.answer(&["/art/a.svg"]); poll(&mut app); assert_eq!(app.session.documents().len(), 1); assert_eq!(app.session.active().and_then(|d| d.path.clone()).as_deref(), Some("/art/a.svg")); // Cancelled: nothing happens, and dialogs show again. assert!(app.run("file.open", json!({})).is_err()); shown.answer(&[]); poll(&mut app); assert_eq!(app.session.documents().len(), 1); assert!(app.run("file.open", json!({})).is_err()); assert_eq!(shown.requests().len(), 1); } #[test] fn save_as_goes_on_with_the_path_picked() { let (mut app, shown, written) = app(); app.run("file.new", json!({"width": 100, "height": 80})).unwrap(); app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap(); assert!(app.run("file.saveAs", json!({})).is_err()); assert!(matches!(shown.requests().as_slice(), [PickRequest::Save(_)])); shown.answer(&["/art/poster.svg"]); poll(&mut app); // SVG Options asks next, and its OK writes the file there. assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some("svgOptions")); crate::dialogs::confirm(&mut app).unwrap(); assert_eq!(*written.borrow(), ["/art/poster.svg"]); } #[test] fn a_dialogs_folder_button_fills_its_field_when_the_picker_answers() { let (mut app, shown, _) = app(); let mut d = Dialog::new("exportForScreens", json!({"folder": ""})); app.ui.dialog = Some(d.clone()); picks::folder_field(&mut app, &mut d, "folder"); assert_eq!(shown.requests(), [PickRequest::Folder]); shown.answer(&["/out"]); poll(&mut app); assert_eq!(app.ui.dialog.as_ref().map(|d| d.str("folder")).as_deref(), Some("/out")); }