use std::env; use std::fs; use std::io::Cursor; use std::ops::Deref; use std::path::PathBuf; use std::sync::Arc; use ctx::{Renderer, Vertex}; use image::ImageDecoder; use image::ImageFormat; use tobj::Mesh; use tobj::{LoadOptions, Model}; use tracing::{debug, error, info, trace, warn}; use wgpu::rwh::HasWindowHandle; use winit::application::ApplicationHandler; use winit::dpi::LogicalSize; use winit::dpi::Size; use winit::event::{KeyEvent, WindowEvent}; use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; use winit::monitor::MonitorHandle; use winit::platform::windows::WindowAttributesExtWindows; use winit::window::Fullscreen; use winit::window::Icon; use winit::window::Window; use winit::window::WindowId; pub mod ctx; struct WindowContext<'window> { window: Arc, ctx: Renderer<'window>, main_window: bool, } impl Deref for WindowContext<'_> { type Target = winit::window::Window; fn deref(&self) -> &Self::Target { self.window.as_ref() } } impl WindowContext<'_> { pub fn is_main_window(&self) -> bool { self.main_window } } #[derive(Default)] pub struct App<'window> { windows: ahash::AHashMap>, } static CUBE_OBJ: &str = " # Blender 4.2.3 LTS # www.blender.org mtllib untitled.mtl o Cube v 0.645975 0.645975 -0.645975 v 0.645975 -0.645975 -0.645975 v 0.645975 0.645975 0.645975 v 0.645975 -0.645975 0.645975 v -0.645975 0.645975 -0.645975 v -0.645975 -0.645975 -0.645975 v -0.645975 0.645975 0.645975 v -0.645975 -0.645975 0.645975 vn -0.0000 1.0000 -0.0000 vn -0.0000 -0.0000 1.0000 vn -1.0000 -0.0000 -0.0000 vn -0.0000 -1.0000 -0.0000 vn 1.0000 -0.0000 -0.0000 vn -0.0000 -0.0000 -1.0000 vt 0.625000 0.500000 vt 0.875000 0.500000 vt 0.875000 0.750000 vt 0.625000 0.750000 vt 0.375000 0.750000 vt 0.625000 1.000000 vt 0.375000 1.000000 vt 0.375000 0.000000 vt 0.625000 0.000000 vt 0.625000 0.250000 vt 0.375000 0.250000 vt 0.125000 0.500000 vt 0.375000 0.500000 vt 0.125000 0.750000 s 0 usemtl Material f 1/1/1 5/2/1 7/3/1 3/4/1 f 4/5/2 3/4/2 7/6/2 8/7/2 f 8/8/3 7/9/3 5/10/3 6/11/3 f 6/12/4 2/13/4 4/5/4 8/14/4 f 2/13/5 1/1/5 3/4/5 4/5/5 f 6/11/6 5/10/6 1/1/6 2/13/6 "; impl App<'_> { fn create_main_window(&mut self, event_loop: &ActiveEventLoop) { let icon = self.load_icon_from_bytes(Self::ICON).unwrap(); let win_attr = Window::default_attributes() .with_title("Zenyx") .with_min_inner_size(Size::Logical(LogicalSize::new(100.0, 100.0))) .with_window_icon(icon.clone()) .with_taskbar_icon(icon); match event_loop.create_window(win_attr) { Ok(window) => { let window = Arc::new(window); let window_id = window.id(); match Renderer::new_blocking(window.clone()) { Ok(mut wgpu_ctx) => { let obj = match tobj::load_obj( "Pumpkin.obj", &LoadOptions { triangulate: true, single_index: true, ..Default::default() }, ) { Ok(obj) => obj, Err(e) => { error!("Failed to load Pumpkin.obj: {e}"); // Fallback to CUBE_OBJ let fallback_obj = CUBE_OBJ.to_string(); tobj::load_obj_buf( &mut fallback_obj.as_bytes(), &LoadOptions { triangulate: true, single_index: true, ..Default::default() }, |_| Ok(Default::default()), ) .expect("Failed to load fallback CUBE_OBJ") } }; let (combined_vertices, combined_indices) = parse_obj(&obj.0); wgpu_ctx.add_model(&combined_vertices, &combined_indices); self.windows.insert( window_id, WindowContext { window, ctx: wgpu_ctx, main_window: true, }, ); info!("Main window created: {:?}", window_id); } Err(e) => error!("Failed to create WGPU context: {:}", e), } } Err(e) => error!("Failed to create main window: {}", e), } } fn handle_close_requested(&mut self, window_id: WindowId) { if self.windows.remove(&window_id).is_some() { debug!("Window {:?} closed", window_id); } else { warn!("Tried to close non-existent window {:?}", window_id); } } fn handle_keyboard_input( &mut self, event_loop: &ActiveEventLoop, window_id: WindowId, key_event: KeyEvent, ) { if !key_event.state.is_pressed() || key_event.repeat { return; } match key_event.physical_key { winit::keyboard::PhysicalKey::Code(code) => match code { winit::keyboard::KeyCode::Space => { self.toggle_background(window_id); } winit::keyboard::KeyCode::Escape => { self.spawn_child_window(event_loop); } winit::keyboard::KeyCode::F11 => self.toggle_fullscreen(window_id), other => error!("Unimplemented keycode: {:?}", other), }, _ => debug!("Received a keyboard event with no physical key"), } } fn toggle_background(&mut self, window_id: WindowId) { if let Some(window_context) = self.windows.get_mut(&window_id) { let current_color = window_context.ctx.bg_color(); let new_color = match current_color { &wgpu::Color::WHITE => wgpu::Color::BLACK, &wgpu::Color::BLACK => wgpu::Color::WHITE, _ => wgpu::Color::WHITE, }; let new_text_color = match window_context.ctx.text_color() { &wgpu::Color::WHITE => wgpu::Color::BLACK, &wgpu::Color::BLACK => wgpu::Color::WHITE, _ => wgpu::Color::WHITE, }; println!("new text color {new_text_color:#?}"); window_context.ctx.set_bg_color(new_color); window_context.ctx.set_text_color(new_text_color); debug!("Toggled background color for window {:?}", window_id); } else { warn!("No window context for toggling background: {:?}", window_id); } } fn toggle_fullscreen(&mut self, window_id: WindowId) { if let Some(ctx) = self.windows.get_mut(&window_id) { let is_fullscreen = ctx.window.fullscreen().is_some(); let fullscreen_mode = if is_fullscreen { None } else { ctx.window .current_monitor() .map(|monitor| Fullscreen::Borderless(Some(monitor))) }; ctx.window.set_fullscreen(fullscreen_mode); debug!("Fullscreen toggled for window: {:?}", window_id); } else { warn!("No window found for fullscreen toggle: {:?}", window_id); } } fn load_icon_from_bytes(&self, bytes: &[u8]) -> Result, String> { const IMAGE_DIR: &str = env!("CARGO_MANIFEST_DIR"); let cursor = Cursor::new(bytes); let format = image::guess_format(bytes).map_err(|_| "Failed to guess image format")?; let decoder = match format { ImageFormat::Png => image::codecs::png::PngDecoder::new(cursor).map_err(|e| format!("Failed to decode PNG: {}", e))?, _ => { let img = image::load_from_memory(bytes).map_err(|e| format!("Failed to load image: {}", e))?.into_rgba8(); let (width, height) = img.dimensions(); return Icon::from_rgba(img.into_raw(), width, height) .map(Some) .map_err(|e| format!("Failed to create icon from bytes: {}", e)); } }; let (width, height) = decoder.dimensions(); let mut image_data = vec![0; decoder.total_bytes() as usize]; decoder.read_image(&mut image_data).map_err(|e| format!("Failed to read image data: {}", e))?; Icon::from_rgba(image_data, width, height) .map(Some) .map_err(|e| format!("Failed to create icon from bytes: {}", e)) } const ICON: &'static [u8] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/../assets/Badge.png")); fn spawn_child_window(&mut self, event_loop: &ActiveEventLoop) { if let Some(main_ctx) = self.windows.values().find(|ctx| ctx.is_main_window()) { let title = format!("Zenyx - New Window {}", self.windows.len()); let icon = self.load_icon_from_bytes(Self::ICON).unwrap(); let win_attr = unsafe { let base = Window::default_attributes() .with_title(title) .with_min_inner_size(Size::Logical(LogicalSize::new(100.0, 100.0))) .with_window_icon(icon.clone()) .with_taskbar_icon(icon); match main_ctx.window_handle() { Ok(handle) => { if !cfg!(target_os = "windows") { base.with_parent_window(Some(handle.as_raw())) } else { base } } Err(e) => { error!("{e}"); base } } }; match event_loop.create_window(win_attr) { Ok(window) => { let window = Arc::new(window); let window_id = window.id(); match Renderer::new_blocking(window.clone()) { Ok(mut wgpu_ctx) => { { let mut tmp_path: PathBuf = env::temp_dir(); tmp_path.push("cube.obj"); if let Err(e) = fs::write(&tmp_path, CUBE_OBJ) { error!("Failed to write cube OBJ to temp: {}", e); } let load_options = tobj::LoadOptions { triangulate: true, single_index: true, ..Default::default() }; match tobj::load_obj(tmp_path.to_str().unwrap(), &load_options) { Ok(cube_model) => { let (cube_vertices, cube_indices) = parse_obj(&cube_model.0); wgpu_ctx.add_model(&cube_vertices, &cube_indices); } Err(e) => { error!("Failed to load cube OBJ from temp file: {:#}", e) } } } self.windows.insert( window_id, WindowContext { window, ctx: wgpu_ctx, main_window: false, }, ); debug!("Spawned new child window: {:?}", window_id); } Err(e) => error!("Failed to create WGPU context for child window: {}", e), } } Err(e) => error!("Failed to create child window: {}", e), } } else { error!("No main window found. Cannot spawn a child window."); } } fn handle_redraw_requested(&mut self, window_id: WindowId) { if let Some(window_context) = self.windows.get_mut(&window_id) { window_context.ctx.draw(); window_context.request_redraw(); trace!( "Redrew window {:?} with title: {}", window_id, window_context.window.title() ); } else { warn!("Received redraw for unknown window {:?}", window_id); } } fn handle_resize(&mut self, window_id: WindowId, new_size: winit::dpi::PhysicalSize) { if let Some(window_context) = self.windows.get_mut(&window_id) { window_context.ctx.resize(new_size.into()); window_context.window.request_redraw(); debug!( "Resized window {:?} to {}x{}", window_id, new_size.width, new_size.height ); } else { warn!("Received resize for unknown window {:?}", window_id); } } fn handle_destroyed(&mut self, event_loop: &ActiveEventLoop) { if self.windows.is_empty() || !self.windows.iter().any(|(_, ctx)| ctx.is_main_window()) { self.windows.clear(); debug!("All main windows are closed. Exiting event loop."); event_loop.exit(); } } } fn parse_obj(obj: &Vec) -> (Vec, Vec) { let mut combined_vertices = Vec::new(); let mut combined_indices = Vec::new(); let mut vertex_offset = 0; for object in obj { let mesh: &_ = &object.mesh; let vertices: Vec = (0..mesh.positions.len() / 3) .map(|i| Vertex { position: [ mesh.positions[i * 3], mesh.positions[i * 3 + 1], mesh.positions[i * 3 + 2], ], normal: if !mesh.normals.is_empty() { [ mesh.normals[i * 3], mesh.normals[i * 3 + 1], mesh.normals[i * 3 + 2], ] } else { [0.0; 3] }, }) .collect(); combined_vertices.extend(vertices); combined_indices.extend(mesh.indices.iter().map(|&index| index + vertex_offset)); vertex_offset += (mesh.positions.len() as u32) / 3; } (combined_vertices, combined_indices) } impl ApplicationHandler for App<'_> { fn resumed(&mut self, event_loop: &ActiveEventLoop) { if self.windows.is_empty() { self.create_main_window(event_loop); } } fn window_event( &mut self, event_loop: &ActiveEventLoop, window_id: WindowId, event: WindowEvent, ) { match event { WindowEvent::CloseRequested => { self.handle_close_requested(window_id); } WindowEvent::KeyboardInput { event: key_event, .. } => { self.handle_keyboard_input(event_loop, window_id, key_event); } WindowEvent::RedrawRequested => { self.handle_redraw_requested(window_id); } WindowEvent::Resized(new_size) => { self.handle_resize(window_id, new_size); } WindowEvent::Destroyed => { self.handle_destroyed(event_loop); } _ => trace!("Unhandled window event for window {:?}", window_id), } } } pub fn init_renderer(event_loop: EventLoop<()>) { event_loop.set_control_flow(ControlFlow::Wait); let mut app = App::default(); if let Err(e) = event_loop.run_app(&mut app) { error!("Failed to run application: {}", e); error!("Failed to run application: {}", e); } }