/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ //! A renderer for use in tests, which doesn't actually render a GUI but allows programmatic //! interaction. //! //! The [`ui!`](super::ui) macro supports labeling any element with a string identifier, which can //! be used to access the element in this UI. //! //! The [`Interact`] hook must be created to interact with the test UI, before the UI is run and on //! the same thread as the UI. //! //! See how this UI is used in [`crate::test`]. use super::model::{self, Application, Element}; use std::cell::RefCell; use std::collections::HashMap; use std::sync::{ atomic::{AtomicBool, AtomicU8, Ordering::Relaxed}, mpsc, Arc, Condvar, Mutex, }; thread_local! { static INTERACT: RefCell>> = Default::default(); } /// A test UI which allows access to the UI elements. #[derive(Default)] pub struct UI { interface: Mutex>, } impl UI { pub fn run_loop(&self, app: Application) { let (tx, rx) = mpsc::channel(); let interface = UIInterface { work: tx }; let elements = id_elements(&app); INTERACT.with(cc! { (interface) move |r| { if let Some(state) = &*r.borrow() { state.set_interface(interface); } }}); *self.interface.lock().unwrap() = Some(interface.clone()); // Close the UI when the root windows are closed. // Use a bitfield rather than a count in case the `close` event is fired multiple times. assert!(app.windows.len() <= 8); let mut windows = 0u8; for i in 0..app.windows.len() { windows |= 1 << i; } let windows = Arc::new(AtomicU8::new(windows)); for (index, window) in app.windows.iter().enumerate() { if let Some(c) = &window.element_type.close { c.subscribe(cc! { (interface, windows) move |&()| { let old = windows .fetch_update(Relaxed, Relaxed, |x| Some(x & !(1u8 << index))) .unwrap(); if old == 1u8 << index { interface.work.send(Command::Finish).unwrap(); } }}); } else { // No close event, so we must assume a closed state (and assume that _some_ window // will have a close event registered so we don't drop the interface now). windows .fetch_update(Relaxed, Relaxed, |x| Some(x & !(1u8 << index))) .unwrap(); } } while let Ok(f) = rx.recv() { match f { Command::Invoke(f) => f(), Command::Interact(f) => f(&elements), Command::Finish => break, } } *self.interface.lock().unwrap() = None; INTERACT.with(|r| { if let Some(state) = &*r.borrow() { state.clear_interface(); } }); } pub fn invoke(&self, f: model::InvokeFn) { let guard = self.interface.lock().unwrap(); if let Some(interface) = &*guard { let _ = interface.work.send(Command::Invoke(f)); } } } /// Test interaction interface. #[derive(Clone)] pub struct Interact { state: Arc, } /// Spawned test interaction. pub struct SpawnedInteract { interact: Interact, handle: ::std::mem::ManuallyDrop<::std::thread::JoinHandle<()>>, ignore_panic: bool, } impl SpawnedInteract { pub fn ignore_panic(&mut self) { self.ignore_panic = true; } } impl Drop for SpawnedInteract { fn drop(&mut self) { self.interact.cancel(); if unsafe { ::std::mem::ManuallyDrop::take(&mut self.handle) } .join() .is_err() { if !std::thread::panicking() && !self.ignore_panic { // Make sure this thread panics so we see the panic from the interact thread. panic!("interact thread panicked"); } } } } impl Interact { /// Spawn a thread which runs interactions according with the test UI. /// /// The returned `SpawnedInteract` should be dropped after the test UI is run. /// /// # Panic Safety /// If a panic occurs in the interact thread, it will not be re-thrown (we assume that the /// panic handler will still show the thread's panic message when the test completes). pub fn spawn(f: F) -> SpawnedInteract { let interact = Self::hook(); let handle = ::std::thread::spawn(cc! { (interact) move || { interact.wait_for_ready(); f(&interact); }}); SpawnedInteract { interact, handle: ::std::mem::ManuallyDrop::new(handle), ignore_panic: false, } } /// Create an interaction hook for the test UI. /// /// This should be done before running the UI, and must be done on the same thread that /// later runs it. fn hook() -> Interact { let v = Interact { state: Default::default(), }; { let state = v.state.clone(); INTERACT.with(move |r| *r.borrow_mut() = Some(state)); } v } /// Wait for the render thread to be ready for interaction. fn wait_for_ready(&self) { let mut guard = self.state.interface.lock().unwrap(); while guard.is_none() && !self.state.cancel.load(Relaxed) { guard = self.state.waiting_for_interface.wait(guard).unwrap(); } } /// Cancel an Interact (which causes `wait_for_ready` to always return). fn cancel(&self) { let _guard = self.state.interface.lock().unwrap(); self.state.cancel.store(true, Relaxed); self.state.waiting_for_interface.notify_all(); } /// Run the given function on the element with the given type and identity. /// /// Panics if either the id is missing or the type is incorrect. pub fn element<'a, 'b, T: 'b, F, R>(&self, id: &'a str, f: F) -> R where &'b T: TryFrom<&'b model::ElementType>, F: FnOnce(&model::ElementStyle, &T) -> R + Send + 'a, R: Send + 'a, { self.interact(id, move |element: &IdElement| match element { IdElement::Generic(e) => Some(f(&e.style, (&e.element_type).try_into().ok()?)), IdElement::Window(_) => None, }) .expect("incorrect element type") } /// Run the given function on the window with the given identity. /// /// Panics if the id is missing or the type is incorrect. pub fn window<'a, F, R>(&self, id: &'a str, f: F) -> R where F: FnOnce(&model::ElementStyle, &model::Window) -> R + Send + 'a, R: Send + 'a, { self.interact(id, move |element| match element { IdElement::Window(e) => Some(f(&e.style, &e.element_type)), IdElement::Generic(_) => None, }) .expect("incorrect element type") } fn interact<'a, 'b, F, R>(&self, id: &'a str, f: F) -> R where F: FnOnce(&IdElement<'b>) -> R + Send + 'a, R: Send + 'a, { let (send, recv) = std::sync::mpsc::sync_channel(0); { let f: Box) + Send + 'a> = Box::new(move |elements| { let _ = send.send(elements.get(id).map(f)); }); // # Safety // The function is run while `'a` is still valid (we wait here for it to complete). let f: Box = unsafe { std::mem::transmute(f) }; let guard = self.state.interface.lock().unwrap(); let interface = guard.as_ref().expect("renderer is not running"); let _ = interface.work.send(Command::Interact(f)); } recv.recv().unwrap().expect("failed to get element") } } #[derive(Clone)] struct UIInterface { work: mpsc::Sender, } enum Command { Invoke(Box), Interact(Box), Finish, } enum IdElement<'a> { Generic(&'a Element), Window(&'a model::TypedElement), } type IdElements<'a> = HashMap>; #[derive(Default)] struct State { interface: Mutex>, waiting_for_interface: Condvar, cancel: AtomicBool, } impl State { /// Set the interface for the interaction client to use. pub fn set_interface(&self, interface: UIInterface) { let mut guard = self.interface.lock().unwrap(); *guard = Some(interface); self.waiting_for_interface.notify_all(); } /// Clear the UI interface. pub fn clear_interface(&self) { *self.interface.lock().unwrap() = None; } } fn id_elements<'a>(app: &'a Application) -> IdElements<'a> { let mut elements: IdElements<'a> = Default::default(); let mut windows_to_visit: Vec<_> = app.windows.iter().collect(); let mut to_visit: Vec<&'a Element> = Vec::new(); while let Some(window) = windows_to_visit.pop() { if let Some(id) = &window.style.id { elements.insert(id.to_owned(), IdElement::Window(window)); } windows_to_visit.extend(&window.element_type.children); if let Some(content) = &window.element_type.content { to_visit.push(content); } } while let Some(el) = to_visit.pop() { if let Some(id) = &el.style.id { elements.insert(id.to_owned(), IdElement::Generic(el)); } use model::ElementType::*; match &el.element_type { Button(model::Button { content: Some(content), .. }) | Scroll(model::Scroll { content: Some(content), }) => { to_visit.push(content); } VBox(model::VBox { items, .. }) | HBox(model::HBox { items, .. }) => { for item in items { to_visit.push(item) } } _ => (), } } elements }