use super::decals::Decal;
use super::inputs::{self, Input, Mouse, MouseBtn, MouseWheel};
use super::Sprite;
use pixel_engine_draw::traits::SmartDrawingTrait;
use px_draw::graphics::DrawingSprite;
use pixel_engine_draw::vector2::Vu2d;
use px_backend::winit::{
self,
event::{Event, WindowEvent},
};
#[derive(Debug)]
pub struct EngineWrapper(Option<Engine>);
impl std::ops::Deref for EngineWrapper {
type Target = Engine;
fn deref(&self) -> &Self::Target {
self.0.as_ref().expect("Pannic while deref EngineWrapper")
}
}
impl std::ops::DerefMut for EngineWrapper {
fn deref_mut(&mut self) -> &mut Self::Target {
self.0
.as_mut()
.expect("Panic while deref Mut EngineWrapper")
}
}
impl EngineWrapper {
pub async fn new(title: String, size: (u32, u32, u32)) -> Self {
Self(Some(Engine::new(title, size).await))
}
#[cfg(not(target_arch = "wasm32"))]
#[must_use]
pub fn new_sync(title: String, size: (u32, u32, u32)) -> Self {
Self(Some(Engine::new_sync(title, size)))
}
pub fn run_init<G: crate::Game + 'static>(mut self) -> ! {
let game = match G::create(&mut self) {
Ok(game) => game,
Err(e) => {
if cfg!(debug_assertions) {
println!("Unable to create game instance:\n{:?}", e);
} else {
println!("Unable to create game instance:\n{}", e);
}
std::process::abort();
}
};
self.run(game);
}
#[allow(clippy::too_many_lines, clippy::missing_panics_doc)]
pub fn run<G: crate::Game + 'static>(mut self, mut game: G) -> ! {
let mut engine = self.0.take().unwrap();
let mut force_exit = false;
let event_loop = engine.event_loop.take().unwrap();
let mut redraw = true;
let mut redraw_last_frame = false;
let mut ignore_next_char = false;
event_loop.run(move |e, _, control_flow| {
*control_flow = winit::event_loop::ControlFlow::Poll;
if redraw_last_frame {
for key in &engine.k_pressed {
engine.k_held.insert(*key);
}
engine.k_pressed.clear();
engine.k_released.clear();
for i in 0..3 {
if engine.mouse.buttons[i].released {
engine.mouse.buttons[i].released = false;
}
if engine.mouse.buttons[i].pressed {
engine.mouse.buttons[i].pressed = false;
engine.mouse.buttons[i].held = true;
}
}
engine.mouse.wheel = MouseWheel::None;
redraw_last_frame = false;
crate::decals::DECAL_HANDLER.with(|key| {
key.borrow_mut()
.drain(..)
.for_each(|decal| decal.destroy(&mut engine.handler));
});
}
match e {
Event::WindowEvent {
event: e,
window_id,
} if window_id == engine.window.id() => match e {
WindowEvent::KeyboardInput { input: inp, .. } => {
ignore_next_char = false;
if !engine.input_toggle {
if let Some(key) = inp.virtual_keycode {
if inp.state == winit::event::ElementState::Released {
engine.k_pressed.remove(&key);
engine.k_held.remove(&key);
engine.k_released.insert(key);
} else if !engine.k_held.contains(&key) {
engine.k_pressed.insert(key);
}
}
} else if inp.state == px_backend::winit::event::ElementState::Pressed {
match inp.virtual_keycode {
Some(winit::event::VirtualKeyCode::Escape) => {
engine.input_toggle = false;
}
Some(winit::event::VirtualKeyCode::Back) => {
if !engine.input_buffer.is_empty() && engine.input_cursor > 0 {
engine.input_buffer.remove(engine.input_cursor - 1);
engine.input_cursor = engine.input_cursor.saturating_sub(1);
};
}
Some(winit::event::VirtualKeyCode::Return) => {
engine.finish_input = true;
let input = std::mem::replace(
&mut engine.input_buffer,
String::with_capacity(128),
);
engine.input_cursor = 0;
game.receive_input(&mut engine, input);
}
Some(winit::event::VirtualKeyCode::Left) => {
engine.input_cursor = engine.input_cursor.saturating_sub(1);
}
Some(winit::event::VirtualKeyCode::Right) => {
engine.input_cursor =
engine.input_buffer.len().min(engine.input_cursor + 1);
}
_ => {}
}
}
if let Some(key) = inp.virtual_keycode {
if engine.input_passthrough.contains(&key) {
ignore_next_char = true;
if inp.state == winit::event::ElementState::Released {
engine.k_pressed.remove(&key);
engine.k_held.remove(&key);
engine.k_released.insert(key);
} else if !engine.k_held.contains(&key) {
engine.k_pressed.insert(key);
}
}
}
}
WindowEvent::CloseRequested => {
force_exit = true;
}
WindowEvent::CursorMoved { position, .. } => {
let (x, y): (f64, f64) = position.into();
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
{
engine.mouse.pos = (
(x / f64::from(engine.scale())).trunc().abs() as u32,
(y / f64::from(engine.scale())).trunc().abs() as u32,
);
};
}
WindowEvent::MouseWheel { delta, .. } => match delta {
winit::event::MouseScrollDelta::LineDelta(x, y) => {
engine.mouse.wheel = if x.abs() > y.abs() {
if x > 0.0 {
MouseWheel::Right
} else if x < 0.0 {
MouseWheel::Left
} else {
MouseWheel::None
}
} else if y > 0.0 {
MouseWheel::Down
} else if y < 0.0 {
MouseWheel::Up
} else {
MouseWheel::None
};
}
winit::event::MouseScrollDelta::PixelDelta(lp) => {
let (x, y): (f64, f64) = lp.into();
engine.mouse.wheel = if x.abs() > y.abs() {
if x > 0.0 {
MouseWheel::Right
} else if x < 0.0 {
MouseWheel::Left
} else {
MouseWheel::None
}
} else if y > 0.0 {
MouseWheel::Down
} else if y < 0.0 {
MouseWheel::Up
} else {
MouseWheel::None
};
}
},
WindowEvent::MouseInput { button, state, .. } => {
if std::mem::discriminant(&button)
!= std::mem::discriminant(&winit::event::MouseButton::Other(0))
{
let btn = match button {
winit::event::MouseButton::Left => MouseBtn::Left,
winit::event::MouseButton::Right => MouseBtn::Right,
winit::event::MouseButton::Middle => MouseBtn::Middle,
winit::event::MouseButton::Other(_) => {
unreachable!("MouseButton::Other()")
}
};
if state == winit::event::ElementState::Pressed {
engine.mouse.buttons[match btn {
MouseBtn::Left => 0,
MouseBtn::Right => 1,
MouseBtn::Middle => 2,
}]
.pressed = true;
} else {
engine.mouse.buttons[match btn {
MouseBtn::Left => 0,
MouseBtn::Right => 1,
MouseBtn::Middle => 2,
}]
.released = true;
engine.mouse.buttons[match btn {
MouseBtn::Left => 0,
MouseBtn::Right => 1,
MouseBtn::Middle => 2,
}]
.held = false;
}
}
}
WindowEvent::ReceivedCharacter(char)
if engine.input_toggle
&& !(engine.ignore_passthrough_char && ignore_next_char)
&& char.is_ascii()
&& !char.is_control() =>
{
engine.input_buffer.insert(engine.input_cursor, char);
engine.input_cursor += 1;
}
_ => {}
},
Event::RedrawRequested(_) => {
redraw = true;
}
Event::MainEventsCleared => {
engine.window.request_redraw();
}
_ => {} }
if redraw {
engine.elapsed = (instant::Instant::now()
.checked_duration_since(engine.timer)
.expect("Error with timer"))
.as_secs_f64();
engine.timer = instant::Instant::now();
engine.frame_timer += engine.elapsed;
engine.frame_count += 1;
if engine.frame_timer > 1.0 {
engine.frame_timer -= 1.0;
engine
.window
.set_title(&format!("{} - {}fps", engine.title, engine.frame_count));
engine.frame_count = 0;
}
let r = game.update(&mut engine);
if r.is_err() || r.as_ref().ok() == Some(&false) || force_exit {
if let Err(e) = r {
if cfg!(debug_assertions) {
println!("Game Stopped:\n{:?}", e);
} else {
println!("Game Stopped:\n{}", e);
}
}
*control_flow = winit::event_loop::ControlFlow::Exit;
}
if engine.has_changed {
let (raw, readlock) = engine.screen.get_ref().get_read_lock();
engine.handler.render(raw);
drop(readlock);
engine.has_changed = false;
} else {
engine.handler.render_no_update();
}
redraw = false;
redraw_last_frame = true;
}
});
}
}
#[allow(clippy::struct_excessive_bools)]
pub struct Engine {
pub title: String,
size: (u32, u32, u32),
pub elapsed: f64,
timer: instant::Instant,
frame_count: u64,
frame_timer: f64,
pub(crate) screen: DrawingSprite<Sprite>,
pub(crate) has_changed: bool,
pub(crate) handler: px_backend::Context,
pub(crate) textsheet_decal: Decal,
input_buffer: String,
input_toggle: bool,
input_cursor: usize,
input_passthrough: std::collections::HashSet<inputs::Keycodes>,
ignore_passthrough_char: bool,
finish_input: bool,
k_pressed: std::collections::HashSet<inputs::Keycodes>,
k_held: std::collections::HashSet<inputs::Keycodes>,
k_released: std::collections::HashSet<inputs::Keycodes>,
mouse: Mouse,
event_loop: Option<winit::event_loop::EventLoop<()>>,
window: winit::window::Window,
}
impl std::fmt::Debug for Engine {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Engine")
.field("title", &self.title)
.field("size", &self.size())
.field("scale", &self.scale())
.field("elapsed", &self.elapsed)
.finish()
}
}
impl Engine {
async fn new(title: String, size: (u32, u32, u32)) -> Self {
let event_loop = winit::event_loop::EventLoop::new();
let window = winit::window::WindowBuilder::new()
.with_inner_size(
#[allow(clippy::cast_precision_loss)]
{
winit::dpi::PhysicalSize::new(
(size.0 * size.2) as f32,
(size.1 * size.2) as f32,
)
},
)
.with_title(&title)
.with_resizable(false)
.build(&event_loop)
.expect("Error when constructing window");
window.set_visible(false);
#[cfg(target_arch = "wasm32")]
{
use winit::platform::web::WindowExtWebSys;
let canvas = window.canvas();
let env = option_env!("PIXEL_ENGINE_CANVAS");
match env {
Some(id) => {
let window_sys = web_sys::window().unwrap();
let document = window_sys.document().unwrap();
let parent_canvas = document
.query_selector(&format!("#{}", id))
.expect("The given ID does not exist")
.unwrap();
parent_canvas
.append_child(&canvas)
.expect("Append canvas to HTML body");
}
None => {
let window_sys = web_sys::window().unwrap();
let document = window_sys.document().unwrap();
let body = document.body().unwrap();
body.append_child(&canvas)
.expect("Append canvas to HTML body");
}
}
}
let mut handler = px_backend::Context::new(&window, size).await;
let screen = DrawingSprite::new(Sprite::new(size.0, size.1));
let textsheet_decal =
crate::decals::Decal::new(&mut handler, SmartDrawingTrait::get_textsheet(&screen));
Engine {
size,
title,
timer: instant::Instant::now(),
frame_count: 0u64,
frame_timer: 0f64,
elapsed: 0f64,
handler,
screen,
has_changed: true,
textsheet_decal,
input_buffer: String::with_capacity(32),
input_toggle: false,
input_cursor: 0,
finish_input: false,
input_passthrough: std::collections::HashSet::new(),
ignore_passthrough_char: false,
k_pressed: std::collections::HashSet::new(),
k_held: std::collections::HashSet::new(),
k_released: std::collections::HashSet::new(),
mouse: Mouse::new(),
window: {
window.set_visible(true);
window
},
event_loop: Some(event_loop),
}
}
#[cfg(not(target_arch = "wasm32"))]
fn new_sync(title: String, size: (u32, u32, u32)) -> Self {
futures::executor::block_on(Self::new(title, size))
}
#[inline]
pub fn size(&self) -> Vu2d {
self.screen.get_size()
}
#[inline]
pub fn scale(&self) -> u32 {
self.size.2
}
#[inline]
pub fn get_key(&self, keycode: inputs::Keycodes) -> Input {
Input::new(
self.k_pressed.contains(&keycode),
self.k_held.contains(&keycode),
self.k_released.contains(&keycode),
)
}
pub fn get_mouse_btn(&self, btn: MouseBtn) -> Input {
self.mouse.buttons[match btn {
MouseBtn::Left => 0,
MouseBtn::Right => 1,
MouseBtn::Middle => 2,
}]
}
#[inline]
pub fn get_mouse_location(&self) -> Vu2d {
self.mouse.pos.into()
}
#[inline]
pub fn get_mouse_wheel(&self) -> MouseWheel {
self.mouse.wheel
}
#[inline]
pub fn get_pressed(&self) -> std::collections::HashSet<inputs::Keycodes> {
self.k_pressed.clone()
}
#[inline]
pub fn get_held(&self) -> std::collections::HashSet<inputs::Keycodes> {
self.k_held.clone()
}
#[inline]
pub fn get_released(&self) -> std::collections::HashSet<inputs::Keycodes> {
self.k_released.clone()
}
#[inline]
pub fn create_decal(&mut self, sprite: &Sprite) -> Decal {
Decal::new(&mut self.handler, sprite)
}
#[inline]
pub fn clear_input_buffer(&mut self) {
self.input_buffer.clear();
self.input_cursor = 0;
}
pub fn set_input_buffer(&mut self, value: impl AsRef<str>) {
let value = value.as_ref();
if value.is_ascii() {
self.clear_input_buffer();
self.input_buffer.push_str(value.as_ref());
self.input_cursor = self.input_buffer.len();
} else if cfg!(debug_assertions) {
eprintln!("You can only pass valid ascii string into the input");
}
}
pub fn insert_input_buffer(&mut self, value: impl AsRef<str>) {
let value = value.as_ref();
if value.is_ascii() {
self.input_buffer.insert_str(self.input_cursor, value);
self.input_cursor += value.len();
} else if cfg!(debug_assertions) {
eprintln!("You can only pass valid ascii string into the input");
}
}
pub fn append_input_buffer(&mut self, value: impl AsRef<str>) {
let value = value.as_ref();
if value.is_ascii() {
self.input_buffer.push_str(value);
self.input_cursor += value.len();
} else if cfg!(debug_assertions) {
eprintln!("You can only pass valid ascii string into the input");
}
}
#[inline]
pub fn add_input_passthrough(&mut self, iterator: impl Iterator<Item = inputs::Keycodes>) {
self.input_passthrough.extend(iterator);
}
#[inline]
pub fn remove_input_passthrough(&mut self, iterator: impl Iterator<Item = inputs::Keycodes>) {
for key in iterator {
self.input_passthrough.remove(&key);
}
}
#[inline]
pub fn set_input_passthrough(&mut self, iterator: impl Iterator<Item = inputs::Keycodes>) {
self.input_passthrough.clear();
self.input_passthrough.extend(iterator);
}
#[inline]
pub fn set_ignore_passthrough_chars(&mut self, val: bool) {
self.ignore_passthrough_char = val;
}
#[inline]
pub fn get_input_buffer(&self) -> &str {
&self.input_buffer
}
#[inline]
pub fn get_input_cursor(&self) -> usize {
self.input_cursor
}
#[inline]
pub fn start_input(&mut self) {
self.input_toggle = true;
}
#[inline]
pub fn force_stop_input_mode(&mut self) {
self.input_buffer.clear();
self.input_cursor = 0;
self.input_toggle = false;
}
#[inline]
pub fn is_in_input_mode(&self) -> bool {
self.input_toggle
}
#[inline]
pub fn end_input_mode(&mut self) -> &str {
self.input_toggle = false;
self.input_buffer.as_str()
}
}