/* 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<Option<Arc<State>>> = Default::default();
}
/// A test UI which allows access to the UI elements.
#[derive(Default)]
pub struct UI {
interface: Mutex<Option<UIInterface>>,
}
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<State>,
}
/// 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: FnOnce(&
Self) + Send +
'static>(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<
dyn FnOnce(&IdElements<
'b>) + 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<
dyn FnOnce(&IdElements) + Send +
'static> =
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<Command>,
}
enum Command {
Invoke(
Box<
dyn FnOnce() + Send +
'static>),
Interact(
Box<
dyn FnOnce(&IdElements) + Send +
'static>),
Finish,
}
enum IdElement<
'a> {
Generic(&
'a Element),
Window(&
'a model::TypedElement<model::Window>),
}
type IdElements<
'a> = HashMap<String, IdElement<'a>>;
#[derive(Default)]
struct State {
interface: Mutex<Option<UIInterface>>,
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
}