flake-engine 0.1.3

Immediate-mode game framework built on wgpu + winit
use crate::global::PENDING_CONFIG;
use crate::state::State;
use std::pin::Pin;
use std::sync::Arc;
use winit::application::ApplicationHandler;
use winit::dpi::PhysicalSize;
use winit::event::{KeyEvent, MouseScrollDelta, WindowEvent};
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::keyboard::PhysicalKey;
use winit::platform::windows::WindowExtWindows;
use winit::window::{Icon, Window, WindowId};

pub struct App {
    pub state: Option<State>,
    task: Option<Pin<Box<dyn Future<Output = ()>>>>,
}

impl App {
    pub fn run(fut: impl Future<Output = ()> + 'static) -> anyhow::Result<()> {
        env_logger::init();
        let event_loop = EventLoop::with_user_event().build()?;

        let mut app = App {
            state: None,
            task: Some(Box::pin(fut)),
        };
        event_loop.run_app(&mut app)?;

        Ok(())
    }

    fn set_state_prop<T>(&mut self, value: Option<T>, f: impl Fn(&T, &mut State)) {
        value
            .as_ref()
            .and_then(|a| Some(f(a, self.state.as_mut().unwrap())));
    }
}

impl ApplicationHandler<State> for App {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        if self.state.is_some() {
            return;
        }

        let cfg = PENDING_CONFIG.with_borrow(|s| s.clone());

        let default_icon = || {
            let img = image::load_from_memory(include_bytes!("flake_icon.png"))
                .unwrap()
                .into_rgba8();
            let (w, h) = img.dimensions();
            (img.into_raw(), w, h)
        };

        let (icon_data, icon_w, icon_h) = cfg.window_icon.unwrap_or_else(default_icon);
        let icon = Icon::from_rgba(icon_data, icon_w, icon_h).ok();

        let window_attributes = Window::default_attributes()
            .with_title(cfg.window_title.unwrap_or("Flake App".into()))
            .with_decorations(cfg.window_decorations.unwrap_or(true))
            .with_window_icon(icon)
            .with_inner_size(PhysicalSize::new(
                cfg.window_width.unwrap_or(800),
                cfg.window_height.unwrap_or(600),
            ))
            .with_window_level(cfg.window_level.unwrap_or_default())
            .with_visible(false);

        let window = Arc::new(event_loop.create_window(window_attributes).unwrap());
        self.state = Some(pollster::block_on(State::new(window.clone())).unwrap());

        PENDING_CONFIG.with_borrow(|s| {
            self.state.as_mut().and_then(|state| {
                Some(state.clear_color = Some(s.window_clear_color.unwrap_or_default()))
            });

            self.set_state_prop(s.window_x, |value, state| state.set_window_x(*value));
            self.set_state_prop(s.window_y, |value, state| state.set_window_y(*value));
            self.set_state_prop(s.skip_taskbar, |value, _| window.set_skip_taskbar(*value));
            self.set_state_prop(s.window_passthrough, |value, state| {
                state.set_window_passthrough(*value)
            });

            self.set_state_prop(s.fps_capped, |value, state| state.set_fps_capped(*value));
            self.set_state_prop(s.target_fps, |value, state| state.set_target_fps(*value));

            self.state
                .as_mut()
                .unwrap()
                .load_font_from_bytes(include_bytes!("Jetbrains.ttf"));
            s.fonts
                .iter()
                .for_each(|x| self.state.as_mut().unwrap().load_font(x.clone()));

            s.timers
                .iter()
                .for_each(|x| self.state.as_mut().unwrap().add_timer(x.clone()))
        });

        let state = self.state.as_mut().unwrap();
        state.task = self.task.take();
        state.initialize_state();

        window.set_visible(true);
    }

    fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: State) {
        self.state = Some(event);
    }

    fn window_event(
        &mut self,
        event_loop: &ActiveEventLoop,
        _window_id: WindowId,
        event: WindowEvent,
    ) {
        let state = match &mut self.state {
            Some(canvas) => canvas,
            None => return,
        };

        match event {
            WindowEvent::CloseRequested => event_loop.exit(),
            WindowEvent::Resized(size) => state.resize(size.width, size.height),
            WindowEvent::RedrawRequested => {
                if !state.running {
                    event_loop.exit();
                    return;
                }

                state.update();

                match state.render() {
                    Ok(_) => {}
                    Err(e) => {
                        // Log the error and exit gracefully
                        log::error!("{e}");
                        event_loop.exit();
                    }
                }
            }
            WindowEvent::KeyboardInput {
                event:
                    KeyEvent {
                        physical_key: PhysicalKey::Code(code),
                        state: key_state,
                        repeat,
                        ..
                    },
                ..
            } => {
                if !repeat {
                    state.handle_key(event_loop, code, key_state.is_pressed())
                }
            }
            WindowEvent::MouseInput {
                button,
                state: element_state,
                ..
            } => state.handle_mouse_button(button, element_state.is_pressed()),
            WindowEvent::CursorMoved { position, .. } => {
                state.handle_mouse_motion(position.x as f32, position.y as f32)
            }
            WindowEvent::MouseWheel { delta, .. } => {
                const PIXELS_PER_LINE: f32 = 40.0;

                let (x, y) = match delta {
                    MouseScrollDelta::LineDelta(x, y) => (x, y),
                    MouseScrollDelta::PixelDelta(p) => {
                        (p.x as f32 / PIXELS_PER_LINE, p.y as f32 / PIXELS_PER_LINE)
                    }
                };

                state.handle_mouse_wheel(x, y)
            }

            _ => {}
        }
    }
}