use std::sync::Arc;
use unlit_ecs::{Bundle, DynamicBundle, World, bundle};
use winit::event::{ElementState, WindowEvent};
use winit::event_loop::{EventLoopProxy, OwnedDisplayHandle};
use winit::keyboard::{KeyCode, PhysicalKey};
use winit::window::{Window, WindowAttributes};
use crate::input::winit::WinitInput;
use super::event::{OnAboutToWait, OnResumed, OnWindowEvent};
#[derive(Clone)]
pub struct WindowSpec(pub WindowAttributes);
pub struct WinitWindow(pub Arc<Window>);
pub fn window(world: &World) -> Option<Arc<Window>> {
world
.root_scope()
.query::<&WinitWindow>()
.next()
.map(|winit_window| winit_window.0.clone())
}
pub struct CreateWindowRequest(pub Option<WindowAttributes>);
pub struct ExitRequest(pub bool);
pub struct Resumed(pub bool);
pub struct DisplayHandle(pub OwnedDisplayHandle);
pub struct WinitProxy<U: 'static>(pub EventLoopProxy<U>);
pub fn create_window_on_resume() -> OnResumed {
OnResumed::new(|world, entity, ()| {
if window(world).is_some() {
return;
}
let Ok(attributes) = world
.root_scope()
.query_one::<&WindowSpec>(entity)
.map(|spec| spec.0.clone())
else {
log::warn!(
"create_window_on_resume: {entity:?} carries no WindowSpec, so no window is created"
);
return;
};
let _ = world
.root_scope()
.query_one::<&mut CreateWindowRequest>(entity)
.map(|mut request| request.0 = Some(attributes));
})
}
pub fn exit_on_close_requested() -> OnWindowEvent {
OnWindowEvent::new(|world, entity, payload| {
if matches!(payload.event, WindowEvent::CloseRequested) {
let _ = world
.root_scope()
.query_one::<&mut ExitRequest>(entity)
.map(|mut request| request.0 = true);
}
})
}
pub fn exit_on_escape() -> OnWindowEvent {
OnWindowEvent::new(|world, entity, payload| {
if let WindowEvent::KeyboardInput { event, .. } = &payload.event
&& event.state == ElementState::Pressed
&& !event.repeat
&& event.physical_key == PhysicalKey::Code(KeyCode::Escape)
{
let _ = world
.root_scope()
.query_one::<&mut ExitRequest>(entity)
.map(|mut request| request.0 = true);
}
})
}
pub fn request_redraw_while_foreground() -> OnAboutToWait {
OnAboutToWait::new(|world, _entity, ()| {
let resumed = world
.root_scope()
.query::<&Resumed>()
.next()
.is_some_and(|resumed| resumed.0);
if resumed && let Some(window) = window(world) {
window.request_redraw();
}
})
}
pub fn winit_input_behaviour(input: WinitInput) -> OnWindowEvent {
OnWindowEvent::new(move |_world, _entity, payload| {
input.on_window_event(&payload.event);
})
}
pub struct WindowSetup {
pub attributes: WindowAttributes,
}
impl Bundle for WindowSetup {
fn into_bundle(self) -> DynamicBundle {
bundle![
WindowSpec(self.attributes),
CreateWindowRequest(None),
create_window_on_resume(),
]
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExitTrigger {
CloseRequested,
Escape,
}
pub struct ExitOn(pub ExitTrigger);
impl Bundle for ExitOn {
fn into_bundle(self) -> DynamicBundle {
match self.0 {
ExitTrigger::CloseRequested => bundle![ExitRequest(false), exit_on_close_requested()],
ExitTrigger::Escape => bundle![ExitRequest(false), exit_on_escape()],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::winit::event::WinitWindowEvent;
use unlit_ecs::bundle;
use winit::window::WindowId;
fn window_event(event: WindowEvent) -> WinitWindowEvent {
WinitWindowEvent {
window_id: WindowId::dummy(),
event,
}
}
#[test]
fn exit_on_close_requested_asks_the_host_to_exit() {
let mut world = World::new();
let entity = world.root_scope_mut().spawn(bundle![ExitRequest(false)]);
world.apply().unwrap();
let mut behaviour = exit_on_close_requested();
behaviour.run(&world, entity, &window_event(WindowEvent::CloseRequested));
assert_eq!(
world
.root_scope()
.query_one::<&mut ExitRequest>(entity)
.map(|request| request.0),
Ok(true)
);
}
#[test]
fn exit_on_close_requested_ignores_other_events() {
let mut world = World::new();
let entity = world.root_scope_mut().spawn(bundle![ExitRequest(false)]);
world.apply().unwrap();
let mut behaviour = exit_on_close_requested();
behaviour.run(&world, entity, &window_event(WindowEvent::Focused(true)));
assert_eq!(
world
.root_scope()
.query_one::<&mut ExitRequest>(entity)
.map(|request| request.0),
Ok(false)
);
}
#[test]
fn create_window_on_resume_copies_the_spec_into_the_request() {
let mut world = World::new();
let entity = world.root_scope_mut().spawn(WindowSetup {
attributes: Window::default_attributes().with_title("unlit3d"),
});
world.apply().unwrap();
let mut behaviour = world
.root_scope()
.query_one::<&mut OnResumed>(entity)
.expect("the behaviour is there");
behaviour.run(&world, entity, &());
let attributes = world
.root_scope()
.query_one::<&mut CreateWindowRequest>(entity)
.map(|mut request| request.0.take())
.ok()
.flatten()
.expect("the request is filled");
assert_eq!(attributes.title, "unlit3d");
}
#[test]
fn request_redraw_while_foreground_needs_the_resumed_state() {
let mut world = World::new();
let entity = world
.root_scope_mut()
.spawn(bundle![Resumed(false), request_redraw_while_foreground()]);
world.apply().unwrap();
let mut behaviour = world
.root_scope()
.query_one::<&mut OnAboutToWait>(entity)
.expect("the behaviour is there");
behaviour.run(&world, entity, &());
}
}