use std::sync::Arc;
use std::time::Duration;
use parking_lot::{Condvar, Mutex};
use crate::App;
type AppCreator = Box<dyn FnOnce(&eframe::CreationContext<'_>) -> App>;
const DEFAULT_HEADLESS_SIZE: (f32, f32) = (1920.0, 1080.0);
pub fn run_headless_app(
app_creator: AppCreator,
force_wgpu_backend: Option<&str>,
initial_size: Option<egui::Vec2>,
) -> eframe::Result {
let size = initial_size
.unwrap_or_else(|| egui::vec2(DEFAULT_HEADLESS_SIZE.0, DEFAULT_HEADLESS_SIZE.1));
let wgpu_setup = crate::wgpu_options(force_wgpu_backend).wgpu_setup;
let repaint_signal: Arc<(Mutex<bool>, Condvar)> = Arc::new((Mutex::new(false), Condvar::new()));
let mut init_result = Ok(());
let init_result_mut = &mut init_result;
let mut harness = {
let repaint_signal = repaint_signal.clone();
egui_kittest::Harness::<App>::builder()
.with_size(size)
.wgpu_setup(wgpu_setup)
.build_eframe(move |cc| {
let repaint_signal = repaint_signal.clone();
cc.egui_ctx.set_request_repaint_callback(move |_info| {
let (lock, cvar) = &*repaint_signal;
*lock.lock() = true;
cvar.notify_all();
});
*init_result_mut = crate::customize_eframe_and_setup_renderer(cc);
app_creator(cc)
})
};
init_result.map_err(|err| eframe::Error::AppCreation(Box::new(err)))?;
re_log::info!("Headless viewer running at {}x{}.", size.x, size.y);
let idle_timeout = Duration::from_secs(1);
loop {
harness.step();
handle_pending_screenshots(&mut harness);
if has_pending_close(&harness) {
re_log::info!("Headless viewer received close request, shutting down.");
return Ok(());
}
let (lock, cvar) = &*repaint_signal;
let mut signaled = lock.lock();
if !*signaled {
cvar.wait_for(&mut signaled, idle_timeout);
}
*signaled = false;
}
}
fn has_pending_close(harness: &egui_kittest::Harness<'_, App>) -> bool {
harness
.output()
.viewport_output
.values()
.flat_map(|v| v.commands.iter())
.any(|cmd| matches!(cmd, egui::ViewportCommand::Close))
}
fn handle_pending_screenshots(harness: &mut egui_kittest::Harness<'_, App>) {
let pending: Vec<egui::UserData> = harness
.output()
.viewport_output
.values()
.flat_map(|v| v.commands.iter())
.filter_map(|cmd| match cmd {
egui::ViewportCommand::Screenshot(user_data) => Some(user_data.clone()),
_ => None,
})
.collect();
if pending.is_empty() {
return;
}
let rgba = match harness.render() {
Ok(rgba) => rgba,
Err(err) => {
re_log::error!("Failed to render headless screenshot: {err}");
return;
}
};
let size = [rgba.width() as usize, rgba.height() as usize];
let pixels = rgba.into_raw();
let color_image = Arc::new(egui::ColorImage::from_rgba_premultiplied(size, &pixels));
for user_data in pending {
harness.event(egui::Event::Screenshot {
viewport_id: egui::ViewportId::ROOT,
user_data,
image: color_image.clone(),
});
}
}