mod camera; mod config; mod sim; mod ui; use egui::Id; use egui_wgpu::{RendererOptions, ScreenDescriptor}; use egui_winit::egui::{self, Context}; use pixels::{Pixels, ScalingMode, SurfaceTexture}; use std::time::{Duration, Instant}; use winit::{ application::ApplicationHandler, event::{ElementState, MouseButton, WindowEvent}, event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, keyboard::Key::{self}, window::Window, }; use crate::{ camera::{CameraState, screen_position_to_board, write_frame_view}, config::{PIXEL_BUFFER_HEIGHT, PIXEL_BUFFER_WIDTH, WINDOW_TITLE}, sim::{board::Board, overlay::create_compute_combined_overlay_offset}, ui::draw_egui, }; pub type Error = Box; pub type Result = std::result::Result; struct Config { fps: u16, brush_size: u8, } struct Input { last_mouse_pos_on_screen: (f64, f64), last_mouse_pos_on_board: (i32, i32), is_lmb_down: bool, } struct State { // debug red_level: u8, green_level: u8, blue_level: u8, } struct Diagnostics { fps: f32, } struct App { // core egui_renderer: Option, egui_state: Option, egui_context: Option, window: Option<&'static Window>, pixels: Option>, // input state input: Input, // camera state camera: Option, // game state board: Option, // used to wait for drawing last_frame_requested: Instant, // used to compute delta_time last_frame_real: Instant, config: Config, state: State, diagnostics: Diagnostics, } impl Default for App { fn default() -> Self { Self { egui_renderer: None, egui_state: None, egui_context: None, window: None, pixels: None, input: Input { last_mouse_pos_on_screen: (0.0, 0.0), last_mouse_pos_on_board: (0, 0), is_lmb_down: false, }, camera: Some(CameraState { x: 0.0, y: 0.0, zoom: 0.5, }), board: Some(Board::empty()), last_frame_requested: Instant::now(), last_frame_real: Instant::now(), config: Config { fps: 120, brush_size: 10, }, state: State { red_level: 0xFF, green_level: 0xFF, blue_level: 0xFF, }, diagnostics: Diagnostics { fps: 0.0 }, } } } impl ApplicationHandler for App { fn resumed(&mut self, event_loop: &ActiveEventLoop) { let window = event_loop .create_window(Window::default_attributes().with_title(WINDOW_TITLE)) .unwrap(); let size = window.inner_size(); let window_ref: &'static Window = Box::leak(Box::new(window)); let surface = SurfaceTexture::new(size.width, size.height, window_ref); let mut pixels = Pixels::new(PIXEL_BUFFER_WIDTH, PIXEL_BUFFER_HEIGHT, surface).unwrap(); pixels.set_scaling_mode(ScalingMode::Fill); let egui_context = Context::default(); let egui_state = egui_winit::State::new( egui_context.clone(), egui_context.viewport_id(), window_ref, None, None, None, ); let egui_renderer = egui_wgpu::Renderer::new(pixels.device(), pixels.render_texture_format(), { RendererOptions { msaa_samples: 1, ..RendererOptions::default() } }); self.egui_context = Some(egui_context); self.egui_state = Some(egui_state); self.egui_renderer = Some(egui_renderer); self.window = Some(window_ref); self.pixels = Some(pixels); } fn window_event( &mut self, event_loop: &ActiveEventLoop, _: winit::window::WindowId, event: WindowEvent, ) { if let Some(pixels) = &mut self.pixels && let Some(window) = self.window && let Some(egui_context) = &self.egui_context && let Some(egui_state) = &mut self.egui_state && let Some(egui_renderer) = &mut self.egui_renderer && let Some(board) = &mut self.board && let Some(camera) = &mut self.camera { let egui_response = egui_state.on_window_event(window, &event); // if egui consumed the event, it means we shouldn't treat any e.g., mouse clicks if egui_response.consumed { return; } match event { WindowEvent::KeyboardInput { event, .. } => match event.logical_key { Key::Character(char) => { if char == "z" { camera.zoom += 0.1; } } _ => {} }, WindowEvent::CursorMoved { position, .. } => { self.input.last_mouse_pos_on_screen = (position.x, position.y); self.input.last_mouse_pos_on_board = pixels .window_pos_to_pixel((position.x as f32, position.y as f32)) .map(|v| screen_position_to_board(board, camera, v.0 as f64, v.1 as f64)) .map(|v| (v.0 as i32, v.1 as i32)) .unwrap(); } WindowEvent::MouseInput { state, button, .. } => { if button == MouseButton::Left { self.input.is_lmb_down = state == ElementState::Pressed } } WindowEvent::Resized(size) => { // Important: resize the surface when the window's size change pixels.resize_surface(size.width, size.height).unwrap(); } WindowEvent::CloseRequested => { event_loop.exit(); } WindowEvent::RedrawRequested => { // compute frame delta let now = Instant::now(); let secs_since_last_frame = (now - self.last_frame_real).as_secs_f32(); let delta_time = secs_since_last_frame / (1.0 / 60.0); self.last_frame_real = now; let instantaneous_fps = 1.0 / secs_since_last_frame; // TODO: can smooth and round this self.diagnostics.fps = instantaneous_fps; // pixels/camera logic let frame = pixels.frame_mut(); frame.fill(0); // --TEST DRAWING-- if self.input.is_lmb_down { // start with the bounding box of the drawing brush circle + some margin let bb_xl = self.input.last_mouse_pos_on_board.0 - self.config.brush_size as i32 / 2 - 5; let bb_xu = self.input.last_mouse_pos_on_board.0 + self.config.brush_size as i32 / 2 + 5; let bb_yl = self.input.last_mouse_pos_on_board.1 - self.config.brush_size as i32 / 2 - 5; let bb_yu = self.input.last_mouse_pos_on_board.1 + self.config.brush_size as i32 / 2 + 5; // for each point, check if the distance is less than the brush size and write the pixel for x in bb_xl..bb_xu { for y in bb_yl..bb_yu { // brush/selection if (((x - self.input.last_mouse_pos_on_board.0).pow(2) + (y - self.input.last_mouse_pos_on_board.1).pow(2)) as f32) .sqrt() < self.config.brush_size as f32 { board.set_cell_at_position( x, y, sim::board::Cell { material: 1, velocity_x: 0, velocity_y: 0, flags: 0, }, ); } } } } let get_overlay = create_compute_combined_overlay_offset(board, &self.config, &self.input); write_frame_view(frame, board, camera, get_overlay); // egui logic let raw_input = egui_state.take_egui_input(window); let full_output = egui_context.run_ui(raw_input, |ui| { let right_panel = egui::Panel::right(Id::new("right_panel")); right_panel .resizable(false) // TODO collapse button .show_collapsible(ui, &mut true, |panel_ui| { draw_egui( panel_ui, &mut self.config, &mut self.state, camera, &self.diagnostics, &self.input, ) }); }); egui_state.handle_platform_output(window, full_output.platform_output); let clipped_primitives = egui_context.tessellate(full_output.shapes, full_output.pixels_per_point); let pixels_per_point = full_output.pixels_per_point; let size = window.inner_size(); let screen_descriptor = ScreenDescriptor { size_in_pixels: [size.width, size.height], pixels_per_point, }; let queue = pixels.queue(); let device = pixels.device(); for (id, delta) in &full_output.textures_delta.set { egui_renderer.update_texture(&device, &queue, *id, delta); } let _ = pixels.render_with(|encoder, render_target, context| { context.scaling_renderer.render(encoder, render_target); egui_renderer.update_buffers( device, &queue, encoder, &clipped_primitives, &screen_descriptor, ); let mut egui_pass = encoder .begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("egui pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: render_target, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store, }, depth_slice: None, })], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, multiview_mask: None, }) .forget_lifetime(); egui_renderer.render( &mut egui_pass, &clipped_primitives, &screen_descriptor, ); drop(egui_pass); Ok(()) }); } _ => {} } } } fn about_to_wait(&mut self, _: &ActiveEventLoop) { let now = Instant::now(); let frame_duration: Duration = Duration::from_micros(1_000_000 / self.config.fps as u64); // limit our internal redraw requests to (fps) if now - self.last_frame_requested >= frame_duration { self.last_frame_requested = now; self.window .expect("Bug - Window should exist") .request_redraw(); } } } fn main() -> Result<()> { env_logger::init(); let event_loop = EventLoop::new()?; event_loop.set_control_flow(ControlFlow::Poll); let mut app = App::default(); event_loop.run_app(&mut app)?; Ok(()) }