use std::sync::Arc;
use winit::{
application::ApplicationHandler,
event::{
DeviceEvent, DeviceId, ElementState, KeyEvent, MouseButton, MouseScrollDelta, WindowEvent,
},
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
keyboard::PhysicalKey,
window::{CursorGrabMode, Window, WindowId},
};
use crate::audio::{Audio, Effect, LoopHandle, Voice};
use crate::capture;
use crate::clustered::Clusters;
use crate::control::{Command, Control, Request};
use crate::ecs::{Application as EcsApp, Bundle, Component, Entity, Without};
use crate::gizmos::{ActiveGrid, GizmosPlugin, GridRenderer, WireDrawList};
use crate::gpu::{FrameError, GpuContext};
use crate::hid::{GamepadState, Gamepads, HidPlugin, ps};
use crate::input::{KeyCode, Keys};
use crate::mesh::{MeshData, MeshError, load_glb};
use crate::primitives;
use crate::render3d::{
Camera, MeshHandles, MeshId, MeshInstance, MeshRenderer, Model, ModelDrawList, Render3dPlugin,
Transform,
};
use crate::scene::{self, Build, Scene, SceneCommands, SceneEntity, Spawn};
use crate::sceneobjects::SceneObject;
use crate::time::{Clock, Time};
use crate::tonemap::{self, Tonemap};
use crate::ui::{Color, CursorPosition, MouseInput, ScreenSize, Ui, UiPlugin, UiRenderer};
const BACKGROUND: Color = Color::srgb(0.05, 0.05, 0.07);
pub struct AppState {
pub window: Option<Arc<Window>>,
pub gpu: GpuContext,
pub ui_renderer: UiRenderer,
pub mesh_renderer: MeshRenderer,
pub grid_renderer: GridRenderer,
pub wire_renderer: crate::gizmos::WireRenderer,
pub tonemap: Tonemap,
pub clusters: Clusters,
pub ecs: EcsApp,
pub audio: Option<Audio>,
pads: ps::Hub,
scenes: SceneCommands,
clock: Clock,
time: Time,
control: Option<Control>,
pending_capture: Option<(std::path::PathBuf, Request)>,
keys_to_release: Vec<crate::input::KeyCode>,
click_to_release: bool,
quit: bool,
}
impl AppState {
pub fn new(window: Arc<Window>) -> Self {
let gpu = GpuContext::new(window.clone());
Self::with_gpu(gpu, Some(window))
}
pub fn offscreen(width: u32, height: u32) -> Self {
Self::with_gpu(GpuContext::offscreen(width, height), None)
}
fn with_gpu(gpu: GpuContext, window: Option<Arc<Window>>) -> Self {
let ui_renderer = UiRenderer::new(&gpu.device, &gpu.queue, gpu.format());
let clusters = Clusters::new(&gpu.device);
let mesh_renderer = MeshRenderer::new(&gpu.device, tonemap::HDR_FORMAT, &clusters);
let grid_renderer = GridRenderer::new(&gpu.device, tonemap::HDR_FORMAT);
let wire_renderer = crate::gizmos::WireRenderer::new(&gpu.device, tonemap::HDR_FORMAT);
let tonemap = Tonemap::new(&gpu.device, gpu.format());
let mut ecs = EcsApp::new();
ecs.add_plugin(UiPlugin);
ecs.add_plugin(Render3dPlugin);
ecs.add_plugin(GizmosPlugin);
ecs.add_plugin(HidPlugin);
ecs.add_plugin(crate::dev::DevPlugin);
ecs.world.insert_resource(ScreenSize {
width: gpu.width() as f32,
height: gpu.height() as f32,
});
ecs.world.insert_resource(Keys::default());
let scenes = SceneCommands::new();
let time = Time::default();
ecs.world.insert_resource(scenes.clone());
ecs.world.insert_resource(time);
Self {
window,
gpu,
ui_renderer,
mesh_renderer,
grid_renderer,
wire_renderer,
tonemap,
clusters,
ecs,
audio: Audio::new(),
pads: ps::Hub::new(),
scenes,
clock: Clock::new(),
time,
control: Control::from_env(),
pending_capture: None,
keys_to_release: Vec::new(),
click_to_release: false,
quit: false,
}
}
fn take_control_requests(&mut self) {
let Some(control) = self.control.as_ref() else {
return;
};
while let Some(request) = control.poll() {
match request.command().clone() {
Command::Screenshot { path } => {
if let Some((_, waiting)) = self.pending_capture.take() {
waiting.answer(Err("replaced by a later screenshot".into()));
}
self.pending_capture = Some((path, request));
}
Command::Key { code } => {
self.ecs.world.resource_mut::<Keys>().press(code, false);
self.keys_to_release.push(code);
request.answer(Ok(format!("{code:?}")));
}
Command::Down { code } => {
self.ecs.world.resource_mut::<Keys>().press(code, false);
request.answer(Ok(format!("{code:?} down")));
}
Command::Up { code } => {
self.ecs.world.resource_mut::<Keys>().release(code);
request.answer(Ok(format!("{code:?} up")));
}
Command::Cursor { x, y } => {
self.ecs.world.insert_resource(CursorPosition { x, y });
request.answer(Ok(format!("{x} {y}")));
}
Command::Click { at } => {
if let Some((x, y)) = at {
self.ecs.world.insert_resource(CursorPosition { x, y });
}
let mut mouse = self.ecs.world.resource_mut::<MouseInput>();
mouse.left_down = true;
mouse.just_pressed = true;
self.click_to_release = true;
request.answer(Ok("click".into()));
}
Command::Press { at } => {
if let Some((x, y)) = at {
self.ecs.world.insert_resource(CursorPosition { x, y });
}
let mut mouse = self.ecs.world.resource_mut::<MouseInput>();
mouse.left_down = true;
mouse.just_pressed = true;
request.answer(Ok("press".into()));
}
Command::Release => {
let mut mouse = self.ecs.world.resource_mut::<MouseInput>();
mouse.left_down = false;
mouse.just_released = true;
request.answer(Ok("release".into()));
}
Command::Quit => {
self.quit = true;
request.answer(Ok("stopping".into()));
}
}
}
}
pub fn width(&self) -> f32 {
self.gpu.width() as f32
}
pub fn height(&self) -> f32 {
self.gpu.height() as f32
}
pub fn time(&self) -> Time {
self.time
}
pub fn scenes(&self) -> SceneCommands {
self.scenes.clone()
}
pub fn change_scene<S: Scene>(&self, build: impl FnOnce(&mut AppState) -> S + Send + 'static) {
self.scenes.change(build);
}
pub fn change_scene_after<S: Scene>(
&self,
seconds: f32,
build: impl FnOnce(&mut AppState) -> S + Send + 'static,
) {
self.scenes.change_after(seconds, build);
}
pub fn spawn<W: Spawn>(&mut self, widget: W) -> W::Output {
widget.spawn(&mut self.ecs.world)
}
pub fn load_meshes(&mut self, glb: &[u8]) -> Result<(), MeshError> {
let mut handles = self.ecs.world.resource::<MeshHandles>().clone();
for mesh in load_glb(glb)? {
if handles.get(&mesh.name).is_some() {
continue;
}
let id = self.mesh_renderer.upload(&self.gpu.device, &mesh);
log::debug!("loaded mesh {:?} as {id:?}", mesh.name);
handles.insert(mesh.name, id);
}
self.ecs.world.insert_resource(handles);
Ok(())
}
pub fn mesh(&self, name: &str) -> Option<MeshId> {
self.ecs.world.resource::<MeshHandles>().get(name)
}
pub fn spawn_model(&mut self, model: Model) -> Entity {
let Some(mesh) = self.mesh(&model.mesh) else {
log::error!(
"no mesh named {:?}; loaded meshes: {:?}",
model.mesh,
self.ecs
.world
.resource::<MeshHandles>()
.names()
.collect::<Vec<_>>(),
);
return self.spawn_entity(model.transform);
};
let color = model
.color
.or_else(|| self.mesh_renderer.base_color(mesh))
.unwrap_or(Color::WHITE);
let material = self.mesh_renderer.register_material(&model.surface());
self.spawn_entity((
model.transform,
crate::SceneObject::new(
model.mesh,
crate::ui::icons::path::CUBE,
crate::Category::Mesh,
),
MeshInstance {
mesh,
color,
material,
},
))
}
fn realize_primitives(&mut self) {
for (entity, shape) in self.unmeshed::<primitives::Box>() {
self.give_mesh(
entity,
shape.key(),
shape.mesh(),
shape.transform,
shape.color,
shape.object(),
);
}
for (entity, shape) in self.unmeshed::<primitives::Sphere>() {
self.give_mesh(
entity,
shape.key(),
shape.mesh(),
shape.transform,
shape.color,
shape.object(),
);
}
for (entity, shape) in self.unmeshed::<primitives::Extrusion>() {
self.give_mesh(
entity,
shape.key(),
shape.mesh(),
shape.transform,
shape.color,
shape.object(),
);
}
}
fn unmeshed<C: Component + Clone>(&mut self) -> Vec<(Entity, C)> {
self.ecs
.world
.query_filtered::<(Entity, &C), Without<MeshInstance>>()
.iter(&self.ecs.world)
.map(|(entity, shape)| (entity, shape.clone()))
.collect()
}
fn give_mesh(
&mut self,
entity: Entity,
key: String,
data: MeshData,
transform: Transform,
color: Option<Color>,
object: SceneObject,
) {
let mesh = match self.mesh(&key) {
Some(mesh) => mesh,
None => {
let mesh = self.mesh_renderer.upload(&self.gpu.device, &data);
let mut handles = self.ecs.world.resource::<MeshHandles>().clone();
handles.insert(key, mesh);
self.ecs.world.insert_resource(handles);
mesh
}
};
let color = color
.or_else(|| self.mesh_renderer.base_color(mesh))
.unwrap_or(Color::WHITE);
let material = self
.mesh_renderer
.register_material(&crate::material::Material::default().to_openpbr());
self.ecs.world.entity_mut(entity).insert((
transform,
object,
MeshInstance {
mesh,
color,
material,
},
));
}
pub fn mesh_base_color(&self, name: &str) -> Option<Color> {
self.mesh(name)
.and_then(|id| self.mesh_renderer.base_color(id))
}
pub fn set_camera(&mut self, camera: Camera) {
self.ecs.world.insert_resource(camera);
self.set_views(crate::views::Views::one(camera));
}
fn record_span(&mut self, name: &'static str, start: std::time::Instant) {
let taken = start.elapsed();
let mut profiler = self.ecs.world.resource_mut::<crate::ui::Profiler>();
let from = profiler.since_frame_start(start);
profiler.record(crate::ui::Span {
name: name.to_string(),
lane: crate::ui::Lane::Cpu,
start: from,
end: from + taken.as_secs_f64() * 1000.0,
});
}
fn timed<T>(&mut self, name: &'static str, work: impl FnOnce(&mut Self) -> T) -> T {
let start = std::time::Instant::now();
let out = work(self);
let taken = start.elapsed();
let mut profiler = self.ecs.world.resource_mut::<crate::ui::Profiler>();
let from = profiler.since_frame_start(start);
profiler.record(crate::ui::Span {
name: name.to_string(),
lane: crate::ui::Lane::Cpu,
start: from,
end: from + taken.as_secs_f64() * 1000.0,
});
out
}
pub fn set_views(&mut self, views: crate::views::Views) {
self.ecs.world.insert_resource(views);
}
pub fn views(&self) -> crate::views::Views {
self.ecs.world.resource::<crate::views::Views>().clone()
}
pub fn spawn_entity(&mut self, bundle: impl Bundle) -> Entity {
let entity = self.ecs.world.spawn((bundle, SceneEntity)).id();
self.realize_primitives();
entity
}
pub fn insert(&mut self, entity: Entity, bundle: impl Bundle) {
if let Ok(mut entity) = self.ecs.world.get_entity_mut(entity) {
entity.insert(bundle);
}
self.realize_primitives();
}
pub fn get<C: bevy_ecs::component::Component>(&self, entity: Entity) -> Option<&C> {
self.ecs.world.get::<C>(entity)
}
pub fn edit<C: bevy_ecs::component::Component<Mutability = bevy_ecs::component::Mutable>>(
&mut self,
entity: Entity,
change: impl FnOnce(&mut C),
) {
if let Some(mut component) = self.ecs.world.get_mut::<C>(entity) {
change(&mut component);
}
}
pub fn despawn(&mut self, entity: Entity) {
self.ecs.world.despawn(entity);
}
pub fn clear_scene(&mut self) {
scene::clear_scene(&mut self.ecs.world);
}
pub fn tick_time(&mut self) {
self.ecs
.world
.resource_mut::<crate::ui::Profiler>()
.begin_frame();
self.time = self.clock.tick();
self.ecs.world.insert_resource(self.time);
self.scenes.tick(self.time.delta);
Ui::begin_frame(&mut self.ecs.world);
self.timed("gamepads", |app| app.poll_gamepad());
}
fn poll_gamepad(&mut self) {
let pads: Vec<GamepadState> = self
.pads
.poll()
.iter()
.map(|pad| GamepadState {
connected: true,
model: Some(pad.model()),
state: *pad.state(),
})
.collect();
let first = pads.first().copied().unwrap_or_default();
self.ecs.world.insert_resource(Gamepads(pads));
self.ecs.world.insert_resource(first);
}
pub fn feel(&mut self, seat: usize, feedback: &crate::hid::Feedback) {
self.pads.feel(seat, feedback);
}
pub fn play_effect_on(&mut self, seat: usize, effect: Effect, speaker: f32, haptic: f32) {
let Some(key) = self.pads.key(seat) else {
return;
};
if let Some(audio) = self.audio.as_mut() {
audio.play_effect_on(&key, effect, speaker, haptic);
}
}
#[must_use = "a loop stops when its last handle is dropped"]
pub fn start_loop_on(
&mut self,
seat: usize,
voice: Voice,
speaker: f32,
haptic: f32,
) -> Option<LoopHandle> {
let key = self.pads.key(seat)?;
self.audio
.as_mut()
.and_then(|audio| audio.start_loop_on(&key, voice, speaker, haptic))
}
pub fn gamepad(&self) -> GamepadState {
*self.ecs.world.resource::<GamepadState>()
}
pub fn gamepads(&self) -> Gamepads {
self.ecs.world.resource::<Gamepads>().clone()
}
pub fn resize(&mut self, width: u32, height: u32) {
if width == 0 || height == 0 {
return;
}
self.gpu.resize(width, height);
self.ecs.world.insert_resource(ScreenSize {
width: width as f32,
height: height as f32,
});
}
pub fn set_cursor(&mut self, x: f32, y: f32) {
self.ecs.world.insert_resource(CursorPosition { x, y });
}
pub fn keys(&self) -> &Keys {
self.ecs.world.resource::<Keys>()
}
pub fn set_key(&mut self, key: KeyCode, down: bool, repeat: bool) {
let mut keys = self.ecs.world.resource_mut::<Keys>();
if down {
keys.press(key, repeat);
} else {
keys.release(key);
}
}
pub fn set_mouse_left(&mut self, down: bool) {
let mut mouse = self.ecs.world.resource_mut::<MouseInput>();
let was_down = mouse.left_down;
mouse.left_down = down;
if down {
mouse.just_pressed = true;
} else if was_down {
mouse.just_released = true;
}
}
pub fn set_mouse_middle(&mut self, down: bool) {
self.ecs.world.resource_mut::<MouseInput>().middle_down = down;
}
pub fn set_mouse_right(&mut self, down: bool) {
self.ecs.world.resource_mut::<MouseInput>().right_down = down;
if let Some(window) = self.window.as_ref() {
let grab = match down {
true => CursorGrabMode::Confined,
false => CursorGrabMode::None,
};
let _ = window.set_cursor_grab(grab);
window.set_cursor_visible(!down);
}
}
pub fn add_mouse_motion(&mut self, dx: f32, dy: f32) {
self.ecs.world.resource_mut::<MouseInput>().motion += glam::Vec2::new(dx, dy);
}
pub fn add_scroll(&mut self, clicks: f32) {
self.ecs.world.resource_mut::<MouseInput>().scroll += clicks;
}
pub fn play_sound(&mut self, set: &str) {
if let Some(audio) = self.audio.as_mut() {
audio.play(set);
}
}
pub fn load_sounds(&mut self, set: &str, clips: &[&'static [u8]]) {
if let Some(audio) = self.audio.as_mut() {
audio.load_set(set, clips);
}
}
pub fn play_effect(&mut self, effect: Effect) {
if let Some(audio) = self.audio.as_mut() {
audio.play_effect(effect);
}
}
#[must_use = "a loop stops when its last handle is dropped"]
pub fn start_loop(&mut self, voice: Voice) -> Option<LoopHandle> {
self.audio.as_mut().map(|audio| audio.start_loop(voice))
}
pub fn set_muted(&mut self, muted: bool) {
if let Some(audio) = self.audio.as_mut() {
audio.set_muted(muted);
}
}
pub fn request_redraw(&self) {
if let Some(window) = self.window.as_ref() {
window.request_redraw();
}
}
pub fn should_quit(&self) -> bool {
self.quit
}
pub fn cursor(&self) -> (f32, f32) {
let cursor = self.ecs.world.resource::<CursorPosition>();
(cursor.x, cursor.y)
}
pub fn mouse(&self) -> MouseInput {
*self.ecs.world.resource::<MouseInput>()
}
pub fn camera(&self) -> Camera {
*self.ecs.world.resource::<Camera>()
}
pub fn dev_mode(&self) -> bool {
self.ecs.world.resource::<crate::dev::DevMode>().is_on()
}
pub fn set_dev_mode(&mut self, on: bool) {
crate::dev::set(&mut self.ecs.world, on);
}
pub fn suns(&self) -> crate::sceneobjects::lights::Suns {
*self
.ecs
.world
.resource::<crate::sceneobjects::lights::Suns>()
}
pub fn ambient(&self) -> crate::sceneobjects::lights::Ambient {
*self
.ecs
.world
.resource::<crate::sceneobjects::lights::Ambient>()
}
pub fn set_ambient(&mut self, ambient: crate::sceneobjects::lights::Ambient) {
self.ecs.world.insert_resource(ambient);
}
pub fn cursor_ray(&self) -> crate::render3d::Ray {
self.camera()
.ray(self.cursor(), self.width(), self.height())
}
pub fn render(&mut self) -> Result<(), FrameError> {
self.realize_primitives();
self.timed("ecs", |app| app.ecs.update());
Ui::end_frame(&mut self.ecs.world);
let acquire_started = std::time::Instant::now();
let frame = self.gpu.begin_frame()?;
self.record_span("wait for display", acquire_started);
let (width, height) = (self.gpu.width(), self.gpu.height());
let mut views = self.ecs.world.resource::<crate::views::Views>().clone();
views.fit(width as f32, height as f32);
self.ecs.world.insert_resource(views.clone());
let camera = views.camera();
self.ecs.world.insert_resource(camera);
let suns = *self
.ecs
.world
.resource::<crate::sceneobjects::lights::Suns>();
let ambient = *self
.ecs
.world
.resource::<crate::sceneobjects::lights::Ambient>();
let lights = self
.ecs
.world
.resource::<crate::sceneobjects::lights::LightDrawList>()
.0
.clone();
let grid = self.ecs.world.resource::<ActiveGrid>().0;
let mut wires = std::mem::take(&mut *self.ecs.world.resource_mut::<WireDrawList>());
let views_started = std::time::Instant::now();
for (index, view) in views.iter().enumerate() {
let mut encoder = self.gpu.create_encoder("view encoder");
self.clusters.prepare(
&self.gpu.queue,
&lights,
view.camera.view(),
view.camera.projection(view.aspect()),
view.rect.width.max(1.0) as u32,
view.rect.height.max(1.0) as u32,
view.camera.near,
view.camera.far,
);
self.clusters.cull(&mut encoder);
self.mesh_renderer.render(
&self.gpu.device,
&self.gpu.queue,
&mut encoder,
width,
height,
BACKGROUND,
view,
index == 0,
&suns,
&ambient,
self.ecs.world.resource::<ModelDrawList>(),
&lights,
);
if let Some(grid) = grid {
let targets = self.mesh_renderer.targets(&self.gpu.device, width, height);
self.grid_renderer.render(
&self.gpu.queue,
&mut encoder,
targets.color,
targets.resolve,
targets.depth,
width,
height,
view,
&grid,
);
}
if !wires.lines.is_empty()
|| !wires.faces.is_empty()
|| !wires.images.is_empty()
|| !wires.uploads.is_empty()
{
let targets = self.mesh_renderer.targets(&self.gpu.device, width, height);
self.wire_renderer.render(
&self.gpu.device,
&self.gpu.queue,
&mut encoder,
targets.color,
targets.resolve,
targets.depth,
width,
height,
view,
&mut wires,
);
}
self.gpu.queue.submit(std::iter::once(encoder.finish()));
}
*self.ecs.world.resource_mut::<WireDrawList>() = wires;
self.record_span("views", views_started);
let mut encoder = self.gpu.create_encoder("chessrs frame encoder");
let (width, height) = (self.gpu.width(), self.gpu.height());
let hdr = self
.mesh_renderer
.targets(&self.gpu.device, width, height)
.resolve
.clone();
self.tonemap.render(
&self.gpu.device,
&mut encoder,
&hdr,
width,
height,
&frame.view,
);
{
let mut ui = self.ecs.world.non_send_mut::<crate::ui::Ui>();
self.ui_renderer.render(
&self.gpu.device,
&self.gpu.queue,
&mut encoder,
&frame.view,
&mut ui.yakui,
);
}
let staged = self.pending_capture.as_ref().map(|_| {
capture::stage(
&self.gpu.device,
&mut encoder,
frame.texture(),
self.gpu.width(),
self.gpu.height(),
)
});
let submit_started = std::time::Instant::now();
self.gpu.submit(encoder);
self.record_span("submit", submit_started);
if let (Some(staged), Some((path, request))) = (staged, self.pending_capture.take()) {
let answer = match capture::write_png(&self.gpu, staged, &path) {
Ok(()) => Ok(path.display().to_string()),
Err(error) => Err(error.to_string()),
};
request.answer(answer);
}
let present_started = std::time::Instant::now();
self.gpu.end_frame(frame);
self.record_span("present", present_started);
self.ecs
.world
.resource_mut::<crate::ui::Profiler>()
.end_frame();
{
if std::mem::take(&mut self.click_to_release) {
self.ecs.world.resource_mut::<MouseInput>().left_down = false;
}
let mut keys = self.ecs.world.resource_mut::<Keys>();
keys.end_frame();
for code in self.keys_to_release.drain(..) {
keys.release(code);
}
}
Ok(())
}
}
pub const DEFAULT_HEADLESS_SIZE: (u32, u32) = (1280, 800);
pub fn headless_from_args(args: impl IntoIterator<Item = String>) -> Option<(u32, u32)> {
let flag = args
.into_iter()
.find(|arg| arg == "--headless" || arg.starts_with("--headless="))?;
let Some((_, size)) = flag.split_once('=') else {
return Some(DEFAULT_HEADLESS_SIZE);
};
match parse_size(size) {
Some(size) => Some(size),
None => {
log::warn!("{flag:?} is not a size like 1280x800; using the default");
Some(DEFAULT_HEADLESS_SIZE)
}
}
}
fn parse_size(text: &str) -> Option<(u32, u32)> {
let (width, height) = text.split_once(['x', 'X'])?;
let width: u32 = width.trim().parse().ok()?;
let height: u32 = height.trim().parse().ok()?;
(width > 0 && height > 0).then_some((width, height))
}
pub struct App {
title: String,
start: Option<Build>,
scene: Option<Box<dyn Scene>>,
state: Option<AppState>,
headless: Option<(u32, u32)>,
}
impl App {
pub fn new(title: impl Into<String>) -> Self {
let title_string = title.into();
let title_local = title_string.clone();
crate::logging::init(&title_string.clone());
Self {
title: title_local.into(),
start: None,
scene: None,
state: None,
headless: None,
}
}
pub fn headless(mut self, size: (u32, u32)) -> Self {
self.headless = Some(size);
self
}
pub fn scene<S: Scene>(
mut self,
build: impl FnOnce(&mut AppState) -> S + Send + 'static,
) -> Self {
self.start = Some(Box::new(|app| Box::new(build(app))));
self
}
pub fn run(mut self) {
let headless = self
.headless
.or_else(|| headless_from_args(std::env::args().skip(1)));
match headless {
Some((width, height)) => self.run_headless(width, height),
None => {
let event_loop = EventLoop::new().expect("failed to create event loop");
event_loop.set_control_flow(ControlFlow::Poll);
event_loop.run_app(&mut self).expect("event loop error");
}
}
std::process::exit(0);
}
fn run_headless(&mut self, width: u32, height: u32) {
const FRAME: std::time::Duration = std::time::Duration::from_millis(16);
let mut state = AppState::offscreen(width, height);
log::info!("running headless at {width}x{height}");
if let Some(build) = self.start.take() {
self.scene = Some(build(&mut state));
}
self.state = Some(state);
self.apply_scene_change();
loop {
let started = std::time::Instant::now();
let Some(state) = self.state.as_mut() else {
return;
};
state.take_control_requests();
state.tick_time();
if state.should_quit() {
log::info!("stopping");
return;
}
if let Some(scene) = self.scene.as_mut() {
scene.update(state);
}
self.apply_scene_change();
let Some(state) = self.state.as_mut() else {
return;
};
if let Err(error) = state.render() {
log::warn!("frame failed: {error:?}");
}
self.apply_scene_change();
if let Some(rest) = FRAME.checked_sub(started.elapsed()) {
std::thread::sleep(rest);
}
}
}
fn apply_scene_change(&mut self) {
let Some(build) = self
.state
.as_mut()
.and_then(|state| state.scenes.take_ready())
else {
return;
};
let Some(state) = self.state.as_mut() else {
return;
};
state.clear_scene();
self.scene = Some(build(state));
}
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.state.is_some() {
return;
}
let window = event_loop
.create_window(Window::default_attributes().with_title(self.title.clone()))
.expect("failed to create window");
let mut state = AppState::new(Arc::new(window));
if let Some(build) = self.start.take() {
self.scene = Some(build(&mut state));
}
state.request_redraw();
self.state = Some(state);
self.apply_scene_change();
}
fn device_event(&mut self, _: &ActiveEventLoop, _: DeviceId, event: DeviceEvent) {
let DeviceEvent::MouseMotion { delta: (dx, dy) } = event else {
return;
};
if let Some(state) = self.state.as_mut() {
state.add_mouse_motion(dx as f32, dy as f32);
state.request_redraw();
}
}
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
if let Some(state) = self.state.as_ref() {
state.request_redraw();
}
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
window_id: WindowId,
event: WindowEvent,
) {
let Some(state) = self.state.as_mut() else {
return;
};
if state.window.as_ref().is_none_or(|w| window_id != w.id()) {
return;
}
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::Destroyed => event_loop.exit(),
WindowEvent::Resized(size) => {
state.resize(size.width, size.height);
state.request_redraw();
}
WindowEvent::CursorMoved { position, .. } => {
state.set_cursor(position.x as f32, position.y as f32);
state.request_redraw();
}
WindowEvent::KeyboardInput {
event:
KeyEvent {
physical_key: PhysicalKey::Code(code),
state: key_state,
repeat,
..
},
..
} => {
state.set_key(code, key_state == ElementState::Pressed, repeat);
state.request_redraw();
}
WindowEvent::MouseInput {
state: ElementState::Pressed,
button: MouseButton::Left,
..
} => {
state.set_mouse_left(true);
state.request_redraw();
}
WindowEvent::MouseInput {
state: ElementState::Released,
button: MouseButton::Left,
..
} => {
state.set_mouse_left(false);
}
WindowEvent::MouseInput {
state: button_state,
button: MouseButton::Right,
..
} => {
state.set_mouse_right(button_state == ElementState::Pressed);
state.request_redraw();
}
WindowEvent::MouseInput {
state: button_state,
button: MouseButton::Middle,
..
} => {
state.set_mouse_middle(button_state == ElementState::Pressed);
state.request_redraw();
}
WindowEvent::MouseWheel { delta, .. } => {
let clicks = match delta {
MouseScrollDelta::LineDelta(_, y) => y,
MouseScrollDelta::PixelDelta(position) => position.y as f32 / 60.0,
};
state.add_scroll(clicks);
state.request_redraw();
}
WindowEvent::RedrawRequested => {
state.take_control_requests();
state.tick_time();
if state.should_quit() {
event_loop.exit();
return;
}
if let Some(scene) = self.scene.as_mut() {
scene.update(state);
}
self.apply_scene_change();
let Some(state) = self.state.as_mut() else {
return;
};
match state.render() {
Ok(()) => {}
Err(FrameError::Skip) => {}
Err(FrameError::Lost | FrameError::Outdated) => {
let size = state
.window
.as_ref()
.map(|w| w.inner_size())
.unwrap_or_default();
state.resize(size.width, size.height);
}
Err(e) => log::warn!("surface error: {e:?}"),
}
self.apply_scene_change();
}
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn args(list: &[&str]) -> Vec<String> {
list.iter().map(|arg| (*arg).to_string()).collect()
}
#[test]
fn no_flag_means_a_window() {
assert_eq!(headless_from_args(args(&[])), None);
assert_eq!(headless_from_args(args(&["--debug", "board.glb"])), None);
assert_eq!(headless_from_args(args(&["--headlessly"])), None);
assert_eq!(headless_from_args(args(&["headless"])), None);
}
#[test]
fn the_bare_flag_takes_the_default_size() {
assert_eq!(
headless_from_args(args(&["--headless"])),
Some(DEFAULT_HEADLESS_SIZE),
);
assert_eq!(
headless_from_args(args(&["--foo", "--headless", "--bar"])),
Some(DEFAULT_HEADLESS_SIZE),
);
}
#[test]
fn a_size_can_be_asked_for() {
assert_eq!(
headless_from_args(args(&["--headless=1600x900"])),
Some((1600, 900)),
);
assert_eq!(
headless_from_args(args(&["--headless=64X48"])),
Some((64, 48))
);
}
#[test]
fn a_size_that_makes_no_sense_still_runs_headless() {
for bad in [
"--headless=",
"--headless=wide",
"--headless=1280",
"--headless=0x800",
] {
assert_eq!(
headless_from_args(args(&[bad])),
Some(DEFAULT_HEADLESS_SIZE),
"{bad}",
);
}
}
}