diff options
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 293 |
1 files changed, 293 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..d818b04 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,293 @@ +use egui_wgpu::{RendererOptions, ScreenDescriptor}; +use egui_winit::egui::{self, Context}; +use pixels::{Pixels, SurfaceTexture}; +use std::time::{Duration, Instant}; +use winit::{ + application::ApplicationHandler, + event::{ElementState, MouseButton, WindowEvent}, + event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, + window::Window, +}; + +const WIDTH: u32 = 320; +const HEIGHT: u32 = 240; +const FPS: u64 = 60; +const FRAME_DURATION: Duration = Duration::from_micros(1_000_000 / FPS); + +pub type Error = Box<dyn std::error::Error>; +pub type Result<T> = std::result::Result<T, Error>; + +struct Clicked { + x: usize, + y: usize, + s: f64, +} + +struct App { + // core + egui_renderer: Option<egui_wgpu::Renderer>, + egui_state: Option<egui_winit::State>, + egui_context: Option<egui::Context>, + window: Option<&'static Window>, + pixels: Option<Pixels<'static>>, + last_frame: Instant, + + // input + last_mouse_pos: Option<(f64, f64)>, + + // experiemnts + clicked: Vec<Clicked>, + + // debug + red_level: u8, + green_level: u8, + blue_level: u8, +} + +impl App { + fn set_px(f: &mut [u8], x: u32, y: u32, r: u8, g: u8, b: u8, a: u8) { + let idx = ((y * WIDTH + x) * 4) as usize; + f[idx] = r; + f[idx + 1] = g; + f[idx + 2] = b; + f[idx + 3] = a; + } +} + +impl Default for App { + fn default() -> Self { + Self { + egui_context: None, + egui_state: None, + egui_renderer: None, + window: None, + pixels: None, + last_frame: Instant::now(), + + last_mouse_pos: Some((0.0, 0.0)), + + clicked: Vec::new(), + + red_level: 0xFF, + green_level: 0xFF, + blue_level: 0xFF, + } + } +} + +impl ApplicationHandler for App { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let window = event_loop + .create_window(Window::default_attributes().with_title("pxs")) + .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 pixels = Pixels::new(WIDTH, HEIGHT, surface).unwrap(); + + 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 + { + // TODO how to use the response? + let _ = egui_state.on_window_event(window, &event); + + match event { + WindowEvent::CursorMoved { position, .. } => { + self.last_mouse_pos = Some((position.x, position.y)); + } + WindowEvent::MouseInput { state, button, .. } => { + if state == ElementState::Pressed + && button == MouseButton::Left + && let Some(last_mouse_pos) = self.last_mouse_pos + { + let coords = pixels + .window_pos_to_pixel((last_mouse_pos.0 as f32, last_mouse_pos.1 as f32)) + .unwrap(); + self.clicked.push(Clicked { + x: coords.0, + y: coords.1, + s: 25.0, + }); + } + } + 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 => { + // pixels logic + let frame = pixels.frame_mut(); + frame.fill(0); + + for y in 0..HEIGHT { + for x in 0..WIDTH { + let active = x % 2 == 0 + && y % 2 == 0 + && self.clicked.iter().all(|c| { + (((x as i64) - (c.x as i64)).pow(2) + + ((y as i64) - (c.y as i64)).pow(2)) + .isqrt() as f64 + > c.s + }); + + Self::set_px( + frame, + x, + y, + if active { self.red_level } else { 0x00 }, + if active { self.green_level } else { 0x00 }, + if active { self.blue_level } else { 0x00 }, + 0xFF, + ); + } + } + + for i in 0..self.clicked.len() { + self.clicked[i].s -= 0.3; + if self.clicked[i].s <= 0.0 { + self.clicked.remove(i); + break; + } + } + + // egui logic + let raw_input = egui_state.take_egui_input(window); + let full_output = egui_context.run_ui(raw_input, |ui| { + ui.heading("Debug"); + ui.label( + self.clicked + .iter() + .map(|c| format!("x={x},y={y},s={s}", x = c.x, y = c.y, s = c.s)) + .collect::<Vec<String>>() + .join("\n"), + ); + ui.add(egui::Slider::new(&mut self.red_level, 0..=120).text("Red")); + ui.add(egui::Slider::new(&mut self.green_level, 0..=120).text("Green")); + ui.add(egui::Slider::new(&mut self.blue_level, 0..=120).text("Blue")); + }); + + 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| { + // 1. pixels' own blit of the framebuffer to the surface + context.scaling_renderer.render(encoder, render_target); + + // 2. prep egui's GPU-side buffers for this frame's draws + egui_renderer.update_buffers( + device, + &queue, + encoder, + &clipped_primitives, + &screen_descriptor, + ); + + // 3. open a pass on the same target, preserving the blit + let mut 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, // keep the blit, don't clear + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + multiview_mask: None, + }) + .forget_lifetime(); // bridge wgpu 30's pass-lifetime requirement + + egui_renderer.render(&mut pass, &clipped_primitives, &screen_descriptor); + drop(pass); + + Ok(()) + }); + } + _ => {} + } + } + } + + fn about_to_wait(&mut self, _: &ActiveEventLoop) { + let now = Instant::now(); + // Limit to 60 FPS + if now - self.last_frame >= FRAME_DURATION { + self.last_frame = 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(()) +} |
