use crate::{ AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor, Keymap, Platform, PlatformDisplay, PlatformTextSystem, Task, TestDisplay, TestWindow, WindowAppearance, WindowOptions, }; use anyhow::{anyhow, Result}; use collections::VecDeque; use futures::channel::oneshot; use parking_lot::Mutex; use std::{ cell::RefCell, path::PathBuf, rc::{Rc, Weak}, sync::Arc, time::Duration, }; /// TestPlatform implements the Platform trait for use in tests. pub(crate) struct TestPlatform { background_executor: BackgroundExecutor, foreground_executor: ForegroundExecutor, pub(crate) active_window: RefCell>, active_display: Rc, active_cursor: Mutex, current_clipboard_item: Mutex>, pub(crate) prompts: RefCell, pub opened_url: RefCell>, weak: Weak, } #[derive(Default)] pub(crate) struct TestPrompts { multiple_choice: VecDeque>, new_path: VecDeque<(PathBuf, oneshot::Sender>)>, } impl TestPlatform { pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Rc { Rc::new_cyclic(|weak| TestPlatform { background_executor: executor, foreground_executor, prompts: Default::default(), active_cursor: Default::default(), active_display: Rc::new(TestDisplay::new()), active_window: Default::default(), current_clipboard_item: Mutex::new(None), weak: weak.clone(), opened_url: Default::default(), }) } pub(crate) fn simulate_new_path_selection( &self, select_path: impl FnOnce(&std::path::Path) -> Option, ) { let (path, tx) = self .prompts .borrow_mut() .new_path .pop_front() .expect("no pending new path prompt"); tx.send(select_path(&path)).ok(); } pub(crate) fn simulate_prompt_answer(&self, response_ix: usize) { let tx = self .prompts .borrow_mut() .multiple_choice .pop_front() .expect("no pending multiple choice prompt"); tx.send(response_ix).ok(); } pub(crate) fn has_pending_prompt(&self) -> bool { !self.prompts.borrow().multiple_choice.is_empty() } pub(crate) fn prompt(&self) -> oneshot::Receiver { let (tx, rx) = oneshot::channel(); self.prompts.borrow_mut().multiple_choice.push_back(tx); rx } pub(crate) fn set_active_window(&self, window: Option) { let executor = self.foreground_executor().clone(); let previous_window = self.active_window.borrow_mut().take(); *self.active_window.borrow_mut() = window.clone(); executor .spawn(async move { if let Some(previous_window) = previous_window { if let Some(window) = window.as_ref() { if Arc::ptr_eq(&previous_window.0, &window.0) { return; } } previous_window.simulate_active_status_change(false); } if let Some(window) = window { window.simulate_active_status_change(true); } }) .detach(); } pub(crate) fn did_prompt_for_new_path(&self) -> bool { self.prompts.borrow().new_path.len() > 0 } } impl Platform for TestPlatform { fn background_executor(&self) -> BackgroundExecutor { self.background_executor.clone() } fn foreground_executor(&self) -> ForegroundExecutor { self.foreground_executor.clone() } fn text_system(&self) -> Arc { #[cfg(target_os = "linux")] return Arc::new(crate::platform::test::TestTextSystem {}); #[cfg(target_os = "macos")] return Arc::new(crate::platform::mac::MacTextSystem::new()); // todo!("windows") #[cfg(target_os = "windows")] unimplemented!() } fn run(&self, _on_finish_launching: Box) { unimplemented!() } fn quit(&self) {} fn restart(&self) { unimplemented!() } fn activate(&self, _ignoring_other_apps: bool) { // } fn hide(&self) { unimplemented!() } fn hide_other_apps(&self) { unimplemented!() } fn unhide_other_apps(&self) { unimplemented!() } fn displays(&self) -> Vec> { vec![self.active_display.clone()] } fn display(&self, id: DisplayId) -> Option> { self.displays().iter().find(|d| d.id() == id).cloned() } fn active_window(&self) -> Option { self.active_window .borrow() .as_ref() .map(|window| window.0.lock().handle) } fn open_window( &self, handle: AnyWindowHandle, options: WindowOptions, ) -> Box { let window = TestWindow::new( options, handle, self.weak.clone(), self.active_display.clone(), ); Box::new(window) } fn window_appearance(&self) -> WindowAppearance { WindowAppearance::Light } fn open_url(&self, url: &str) { *self.opened_url.borrow_mut() = Some(url.to_string()) } fn on_open_urls(&self, _callback: Box)>) { unimplemented!() } fn prompt_for_paths( &self, _options: crate::PathPromptOptions, ) -> oneshot::Receiver>> { unimplemented!() } fn prompt_for_new_path( &self, directory: &std::path::Path, ) -> oneshot::Receiver> { let (tx, rx) = oneshot::channel(); self.prompts .borrow_mut() .new_path .push_back((directory.to_path_buf(), tx)); rx } fn reveal_path(&self, _path: &std::path::Path) { unimplemented!() } fn on_become_active(&self, _callback: Box) {} fn on_resign_active(&self, _callback: Box) {} fn on_quit(&self, _callback: Box) {} fn on_reopen(&self, _callback: Box) { unimplemented!() } fn on_event(&self, _callback: Box bool>) { unimplemented!() } fn set_menus(&self, _menus: Vec, _keymap: &Keymap) {} fn on_app_menu_action(&self, _callback: Box) {} fn on_will_open_app_menu(&self, _callback: Box) {} fn on_validate_app_menu_command(&self, _callback: Box bool>) {} fn os_name(&self) -> &'static str { "test" } fn os_version(&self) -> Result { Err(anyhow!("os_version called on TestPlatform")) } fn app_version(&self) -> Result { Err(anyhow!("app_version called on TestPlatform")) } fn app_path(&self) -> Result { unimplemented!() } fn local_timezone(&self) -> time::UtcOffset { time::UtcOffset::UTC } fn path_for_auxiliary_executable(&self, _name: &str) -> Result { unimplemented!() } fn set_cursor_style(&self, style: crate::CursorStyle) { *self.active_cursor.lock() = style; } fn should_auto_hide_scrollbars(&self) -> bool { false } fn write_to_clipboard(&self, item: ClipboardItem) { *self.current_clipboard_item.lock() = Some(item); } fn read_from_clipboard(&self) -> Option { self.current_clipboard_item.lock().clone() } fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task> { Task::ready(Ok(())) } fn read_credentials(&self, _url: &str) -> Task)>>> { Task::ready(Ok(None)) } fn delete_credentials(&self, _url: &str) -> Task> { Task::ready(Ok(())) } fn double_click_interval(&self) -> std::time::Duration { Duration::from_millis(500) } }