use crate::{
Action, AnyView, AnyWindowHandle, App, AppCell, AppContext, BackgroundExecutor, Bounds,
ClipboardItem, Context, Entity, ForegroundExecutor, Global, InputEvent, Keystroke, Modifiers,
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Platform, Point, Render,
Result, Size, Task, TextSystem, Window, WindowBounds, WindowHandle, WindowOptions,
app::GpuiMode, current_platform,
};
use anyhow::anyhow;
use image::RgbaImage;
use std::{future::Future, rc::Rc, sync::Arc, time::Duration};
#[derive(Clone)]
pub struct VisualTestAppContext {
pub app: Rc<AppCell>,
pub background_executor: BackgroundExecutor,
pub foreground_executor: ForegroundExecutor,
platform: Rc<dyn Platform>,
text_system: Arc<TextSystem>,
}
impl VisualTestAppContext {
pub fn new() -> Self {
let liveness = Arc::new(());
let liveness_weak = Arc::downgrade(&liveness);
let platform = current_platform(false, liveness_weak);
let background_executor = platform.background_executor();
let foreground_executor = platform.foreground_executor();
let text_system = Arc::new(TextSystem::new(platform.text_system()));
let asset_source = Arc::new(());
let http_client = http_client::FakeHttpClient::with_404_response();
let mut app = App::new_app(platform.clone(), liveness, asset_source, http_client);
app.borrow_mut().mode = GpuiMode::test();
Self {
app,
background_executor,
foreground_executor,
platform,
text_system,
}
}
pub fn open_offscreen_window<V: Render + 'static>(
&mut self,
size: Size<Pixels>,
build_root: impl FnOnce(&mut Window, &mut App) -> Entity<V>,
) -> Result<WindowHandle<V>> {
use crate::{point, px};
let bounds = Bounds {
origin: point(px(-10000.0), px(-10000.0)),
size,
};
let mut cx = self.app.borrow_mut();
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
focus: false,
show: true,
..Default::default()
},
build_root,
)
}
pub fn open_offscreen_window_default<V: Render + 'static>(
&mut self,
build_root: impl FnOnce(&mut Window, &mut App) -> Entity<V>,
) -> Result<WindowHandle<V>> {
use crate::{px, size};
self.open_offscreen_window(size(px(1280.0), px(800.0)), build_root)
}
pub fn is_screen_capture_supported(&self) -> bool {
self.platform.is_screen_capture_supported()
}
pub fn text_system(&self) -> &Arc<TextSystem> {
&self.text_system
}
pub fn executor(&self) -> BackgroundExecutor {
self.background_executor.clone()
}
pub fn foreground_executor(&self) -> ForegroundExecutor {
self.foreground_executor.clone()
}
pub fn run_until_parked(&self) {
self.background_executor.run_until_parked();
}
pub fn update<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
let mut app = self.app.borrow_mut();
f(&mut app)
}
pub fn read<R>(&self, f: impl FnOnce(&App) -> R) -> R {
let app = self.app.borrow();
f(&app)
}
pub fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
where
F: FnOnce(AnyView, &mut Window, &mut App) -> T,
{
let mut lock = self.app.borrow_mut();
lock.update_window(window, f)
}
pub fn spawn<F, R>(&self, f: F) -> Task<R>
where
F: Future<Output = R> + 'static,
R: 'static,
{
self.foreground_executor.spawn(f)
}
pub fn has_global<G: Global>(&self) -> bool {
let app = self.app.borrow();
app.has_global::<G>()
}
pub fn read_global<G: Global, R>(&self, f: impl FnOnce(&G, &App) -> R) -> R {
let app = self.app.borrow();
f(app.global::<G>(), &app)
}
pub fn set_global<G: Global>(&mut self, global: G) {
let mut app = self.app.borrow_mut();
app.set_global(global);
}
pub fn update_global<G: Global, R>(&mut self, f: impl FnOnce(&mut G, &mut App) -> R) -> R {
let mut lock = self.app.borrow_mut();
lock.update(|cx| {
let mut global = cx.lease_global::<G>();
let result = f(&mut global, cx);
cx.end_global_lease(global);
result
})
}
pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
for keystroke_text in keystrokes.split_whitespace() {
let keystroke = Keystroke::parse(keystroke_text)
.unwrap_or_else(|_| panic!("Invalid keystroke: {}", keystroke_text));
self.dispatch_keystroke(window, keystroke);
}
self.run_until_parked();
}
pub fn dispatch_keystroke(&mut self, window: AnyWindowHandle, keystroke: Keystroke) {
self.update_window(window, |_, window, cx| {
window.dispatch_keystroke(keystroke, cx);
})
.ok();
}
pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) {
for char in input.chars() {
let key = char.to_string();
let keystroke = Keystroke {
modifiers: Modifiers::default(),
key: key.clone(),
key_char: Some(key),
};
self.dispatch_keystroke(window, keystroke);
}
self.run_until_parked();
}
pub fn simulate_mouse_move(
&mut self,
window: AnyWindowHandle,
position: Point<Pixels>,
button: impl Into<Option<MouseButton>>,
modifiers: Modifiers,
) {
self.simulate_event(
window,
MouseMoveEvent {
position,
modifiers,
pressed_button: button.into(),
},
);
}
pub fn simulate_mouse_down(
&mut self,
window: AnyWindowHandle,
position: Point<Pixels>,
button: MouseButton,
modifiers: Modifiers,
) {
self.simulate_event(
window,
MouseDownEvent {
position,
modifiers,
button,
click_count: 1,
first_mouse: false,
},
);
}
pub fn simulate_mouse_up(
&mut self,
window: AnyWindowHandle,
position: Point<Pixels>,
button: MouseButton,
modifiers: Modifiers,
) {
self.simulate_event(
window,
MouseUpEvent {
position,
modifiers,
button,
click_count: 1,
},
);
}
pub fn simulate_click(
&mut self,
window: AnyWindowHandle,
position: Point<Pixels>,
modifiers: Modifiers,
) {
self.simulate_mouse_down(window, position, MouseButton::Left, modifiers);
self.simulate_mouse_up(window, position, MouseButton::Left, modifiers);
}
pub fn simulate_event<E: InputEvent>(&mut self, window: AnyWindowHandle, event: E) {
self.update_window(window, |_, window, cx| {
window.dispatch_event(event.to_platform_input(), cx);
})
.ok();
self.run_until_parked();
}
pub fn dispatch_action(&mut self, window: AnyWindowHandle, action: impl Action) {
self.update_window(window, |_, window, cx| {
window.dispatch_action(action.boxed_clone(), cx);
})
.ok();
self.run_until_parked();
}
pub fn write_to_clipboard(&self, item: ClipboardItem) {
self.platform.write_to_clipboard(item);
}
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
self.platform.read_from_clipboard()
}
pub async fn wait_for<T: 'static>(
&mut self,
entity: &Entity<T>,
predicate: impl Fn(&T) -> bool,
timeout: Duration,
) -> Result<()> {
let start = std::time::Instant::now();
loop {
{
let app = self.app.borrow();
if predicate(entity.read(&app)) {
return Ok(());
}
}
if start.elapsed() > timeout {
return Err(anyhow!("Timed out waiting for condition"));
}
self.run_until_parked();
self.background_executor
.timer(Duration::from_millis(10))
.await;
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn capture_screenshot(&mut self, window: AnyWindowHandle) -> Result<RgbaImage> {
self.update_window(window, |_, window, _cx| window.render_to_image())?
}
pub async fn wait_for_animations(&self) {
self.background_executor
.timer(Duration::from_millis(32))
.await;
self.run_until_parked();
}
}
impl Default for VisualTestAppContext {
fn default() -> Self {
Self::new()
}
}
impl AppContext for VisualTestAppContext {
type Result<T> = T;
fn new<T: 'static>(
&mut self,
build_entity: impl FnOnce(&mut Context<T>) -> T,
) -> Self::Result<Entity<T>> {
let mut app = self.app.borrow_mut();
app.new(build_entity)
}
fn reserve_entity<T: 'static>(&mut self) -> Self::Result<crate::Reservation<T>> {
let mut app = self.app.borrow_mut();
app.reserve_entity()
}
fn insert_entity<T: 'static>(
&mut self,
reservation: crate::Reservation<T>,
build_entity: impl FnOnce(&mut Context<T>) -> T,
) -> Self::Result<Entity<T>> {
let mut app = self.app.borrow_mut();
app.insert_entity(reservation, build_entity)
}
fn update_entity<T: 'static, R>(
&mut self,
handle: &Entity<T>,
update: impl FnOnce(&mut T, &mut Context<T>) -> R,
) -> Self::Result<R> {
let mut app = self.app.borrow_mut();
app.update_entity(handle, update)
}
fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> Self::Result<crate::GpuiBorrow<'a, T>>
where
T: 'static,
{
panic!("Cannot use as_mut with a visual test app context. Try calling update() first")
}
fn read_entity<T, R>(
&self,
handle: &Entity<T>,
read: impl FnOnce(&T, &App) -> R,
) -> Self::Result<R>
where
T: 'static,
{
let app = self.app.borrow();
app.read_entity(handle, read)
}
fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
where
F: FnOnce(AnyView, &mut Window, &mut App) -> T,
{
let mut lock = self.app.borrow_mut();
lock.update_window(window, f)
}
fn read_window<T, R>(
&self,
window: &WindowHandle<T>,
read: impl FnOnce(Entity<T>, &App) -> R,
) -> Result<R>
where
T: 'static,
{
let app = self.app.borrow();
app.read_window(window, read)
}
fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
where
R: Send + 'static,
{
self.background_executor.spawn(future)
}
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result<R>
where
G: Global,
{
let app = self.app.borrow();
callback(app.global::<G>(), &app)
}
}